wdio-electron-service
Version:
WebdriverIO service to enable Electron testing
1,367 lines (1,342 loc) • 67.8 kB
JavaScript
import { browser as browser$2 } from '@wdio/globals';
import { SevereServiceError, remote } from 'webdriverio';
import log from '@wdio/electron-utils/log';
import { C as CUSTOM_CAPABILITY_NAME, A as APP_NOT_FOUND_ERROR } from './constants-hw-pQOyp.js';
import { fn } from '@vitest/spy';
import { parse, print } from 'recast';
import * as babelParser from '@babel/parser';
import os from 'node:os';
import { CdpBridge } from '@wdio/cdp-bridge';
import util from 'node:util';
import path from 'node:path';
import getPort from 'get-port';
import { readPackageUp } from 'read-package-up';
import { getElectronVersion, getAppBuildInfo, getBinaryPath } from '@wdio/electron-utils';
import { compareVersions } from 'compare-versions';
import { fullVersions } from 'electron-to-chromium';
class ElectronServiceMockStore {
#mockFns;
constructor() {
this.#mockFns = new Map();
}
setMock(mock) {
this.#mockFns.set(mock.getMockName(), mock);
return mock;
}
getMock(mockId) {
const mock = this.#mockFns.get(mockId);
if (!mock) {
throw new Error(`No mock registered for "${mockId}"`);
}
return mock;
}
getMocks() {
return Array.from(this.#mockFns.entries());
}
}
const mockStore = new ElectronServiceMockStore();
const puppeteerSessionManager = new Map();
const getActiveWindowHandle = async (puppeteerBrowser, currentHandle) => {
log.trace('Getting active window handle');
if (!puppeteerBrowser) {
log.trace('Puppeteer is not initialized.');
return undefined;
}
const handles = puppeteerBrowser
.targets()
.filter((target) => target.type() === 'page')
.map((target) => target._targetId);
// No windows available
if (handles.length === 0) {
log.trace('No windows found');
return undefined;
}
// If we have a current window handle and it's still valid, keep using it
if (currentHandle && handles.includes(currentHandle)) {
log.trace(`Keeping current window handle: ${currentHandle}`);
return currentHandle;
}
// Otherwise return first available window handle
log.trace(`Selecting first available window handle: ${handles[0]}`);
return handles[0];
};
const switchToActiveWindow = async (browser, puppeteerBrowser) => {
const activeHandle = await getActiveWindowHandle(puppeteerBrowser, browser.electron.windowHandle);
if (activeHandle && activeHandle !== browser.electron.windowHandle) {
log.debug('The active window has changed. Switching...', `New window handle: ${activeHandle}`, `Previous window handle: ${browser.electron.windowHandle}`);
await browser.switchToWindow(activeHandle);
browser.electron.windowHandle = activeHandle;
}
};
const ensureActiveWindowFocus = async (browser, commandName) => {
log.trace(`Ensuring active window focus before command: ${commandName}`);
if (browser.isMultiremote) {
const mrBrowser = browser;
for (const instance of mrBrowser.instances) {
const mrInstance = mrBrowser.getInstance(instance);
const mrPuppeteer = await getPuppeteer(mrInstance);
await switchToActiveWindow(mrInstance, mrPuppeteer);
}
}
else {
const puppeteer = await getPuppeteer(browser);
await switchToActiveWindow(browser, puppeteer);
}
log.trace(`Window focus check completed for command: ${commandName}`);
};
async function getPuppeteer(browser) {
const sessionId = browser.sessionId;
const puppeteer = puppeteerSessionManager.get(sessionId);
if (puppeteer) {
log.trace(`Use cached puppeteer browser.`);
return puppeteer;
}
else {
log.trace(`Get puppeteer browser.`);
const puppeteer = await browser.getPuppeteer();
puppeteerSessionManager.set(sessionId, puppeteer);
return puppeteer;
}
}
function clearPuppeteerSessions() {
log.trace(`Remove all puppeteer sessions`);
puppeteerSessionManager.clear();
}
function isMockFunction(fn) {
return (typeof fn === 'function' && '__isElectronMock' in fn && fn.__isElectronMock);
}
async function restoreElectronFunctionality(apiName, funcName) {
await browser.electron.execute((electron, apiName, funcName) => {
const electronApi = electron[apiName];
const originalApi = globalThis.originalApi;
const originalApiMethod = originalApi[apiName][funcName];
electronApi[funcName].mockImplementation(originalApiMethod);
}, apiName, funcName, { internal: true });
}
async function createMock(apiName, funcName) {
const outerMock = fn();
const outerMockImplementation = outerMock.mockImplementation;
const outerMockImplementationOnce = outerMock.mockImplementationOnce;
const outerMockClear = outerMock.mockClear;
const outerMockReset = outerMock.mockReset;
outerMock.mockName(`electron.${apiName}.${funcName}`);
const mock = outerMock;
mock.__isElectronMock = true;
// initialise inner (Electron) mock
await browser.electron.execute(async (electron, apiName, funcName) => {
const electronApi = electron[apiName];
// src/utils.ts
function assert(condition, message) {
if (!condition)
throw new Error(message);
}
function isType(type, value) {
return typeof value === type;
}
function isPromise(value) {
return value instanceof Promise;
}
function define(obj, key, descriptor) {
Object.defineProperty(obj, key, descriptor);
}
function defineValue(obj, key, value) {
define(obj, key, { value, configurable: true, writable: true });
}
// src/constants.ts
var SYMBOL_STATE = Symbol.for("tinyspy:spy");
// src/internal.ts
var spies = /* @__PURE__ */ new Set(), reset = (state) => {
state.called = false, state.callCount = 0, state.calls = [], state.results = [], state.resolves = [], state.next = [];
}, defineState = (spy2) => (define(spy2, SYMBOL_STATE, {
value: { reset: () => reset(spy2[SYMBOL_STATE]) }
}), spy2[SYMBOL_STATE]), getInternalState = (spy2) => spy2[SYMBOL_STATE] || defineState(spy2);
function createInternalSpy(cb) {
assert(
isType("function", cb) || isType("undefined", cb),
"cannot spy on a non-function value"
);
let fn = function(...args) {
let state2 = getInternalState(fn);
state2.called = true, state2.callCount++, state2.calls.push(args);
let next = state2.next.shift();
if (next) {
state2.results.push(next);
let [type2, result2] = next;
if (type2 === "ok")
return result2;
throw result2;
}
let result, type = "ok", resultIndex = state2.results.length;
if (state2.impl)
try {
new.target ? result = Reflect.construct(state2.impl, args, new.target) : result = state2.impl.apply(this, args), type = "ok";
} catch (err) {
throw result = err, type = "error", state2.results.push([type, err]), err;
}
let resultTuple = [type, result];
return isPromise(result) && result.then(
(r) => state2.resolves[resultIndex] = ["ok", r],
(e) => state2.resolves[resultIndex] = ["error", e]
), state2.results.push(resultTuple), result;
};
defineValue(fn, "_isMockFunction", true), defineValue(fn, "length", cb ? cb.length : 0), defineValue(fn, "name", cb && cb.name || "spy");
let state = getInternalState(fn);
return state.reset(), state.impl = cb, fn;
}
function isMockFunction$1(obj) {
return !!obj && obj._isMockFunction === true;
}
// src/spyOn.ts
var getDescriptor$1 = (obj, method) => {
let objDescriptor = Object.getOwnPropertyDescriptor(obj, method);
if (objDescriptor)
return [obj, objDescriptor];
let currentProto = Object.getPrototypeOf(obj);
for (; currentProto !== null; ) {
let descriptor = Object.getOwnPropertyDescriptor(currentProto, method);
if (descriptor)
return [currentProto, descriptor];
currentProto = Object.getPrototypeOf(currentProto);
}
}, setPototype = (fn, val) => {
val != null && typeof val == "function" && val.prototype != null && Object.setPrototypeOf(fn.prototype, val.prototype);
};
function internalSpyOn(obj, methodName, mock) {
assert(
!isType("undefined", obj),
"spyOn could not find an object to spy upon"
), assert(
isType("object", obj) || isType("function", obj),
"cannot spyOn on a primitive value"
);
let [accessName, accessType] = (() => {
if (!isType("object", methodName))
return [methodName, "value"];
if ("getter" in methodName && "setter" in methodName)
throw new Error("cannot spy on both getter and setter");
if ("getter" in methodName)
return [methodName.getter, "get"];
if ("setter" in methodName)
return [methodName.setter, "set"];
throw new Error("specify getter or setter to spy on");
})(), [originalDescriptorObject, originalDescriptor] = getDescriptor$1(obj, accessName) || [];
assert(
originalDescriptor || accessName in obj,
`${String(accessName)} does not exist`
);
let ssr = false;
accessType === "value" && originalDescriptor && !originalDescriptor.value && originalDescriptor.get && (accessType = "get", ssr = true, mock = originalDescriptor.get());
let original;
originalDescriptor ? original = originalDescriptor[accessType] : accessType !== "value" ? original = () => obj[accessName] : original = obj[accessName], original && isSpyFunction(original) && (original = original[SYMBOL_STATE].getOriginal());
let reassign = (cb) => {
let { value, ...desc } = originalDescriptor || {
configurable: true,
writable: true
};
accessType !== "value" && delete desc.writable, desc[accessType] = cb, define(obj, accessName, desc);
}, restore = () => {
originalDescriptorObject !== obj ? Reflect.deleteProperty(obj, accessName) : originalDescriptor && !original ? define(obj, accessName, originalDescriptor) : reassign(original);
};
mock || (mock = original);
let spy2 = wrap(createInternalSpy(mock), mock);
accessType === "value" && setPototype(spy2, original);
let state = spy2[SYMBOL_STATE];
return defineValue(state, "restore", restore), defineValue(state, "getOriginal", () => ssr ? original() : original), defineValue(state, "willCall", (newCb) => (state.impl = newCb, spy2)), reassign(
ssr ? () => (setPototype(spy2, mock), spy2) : spy2
), spies.add(spy2), spy2;
}
var ignoreProperties = /* @__PURE__ */ new Set([
"length",
"name",
"prototype"
]);
function getAllProperties(original) {
let properties = /* @__PURE__ */ new Set(), descriptors2 = {};
for (; original && original !== Object.prototype && original !== Function.prototype; ) {
let ownProperties = [
...Object.getOwnPropertyNames(original),
...Object.getOwnPropertySymbols(original)
];
for (let prop of ownProperties)
descriptors2[prop] || ignoreProperties.has(prop) || (properties.add(prop), descriptors2[prop] = Object.getOwnPropertyDescriptor(original, prop));
original = Object.getPrototypeOf(original);
}
return {
properties,
descriptors: descriptors2
};
}
function wrap(mock, original) {
if (!original || // the original is already a spy, so it has all the properties
SYMBOL_STATE in original)
return mock;
let { properties, descriptors: descriptors2 } = getAllProperties(original);
for (let key of properties) {
let descriptor = descriptors2[key];
getDescriptor$1(mock, key) || define(mock, key, descriptor);
}
return mock;
}
function isSpyFunction(obj) {
return isMockFunction$1(obj) && "getOriginal" in obj[SYMBOL_STATE];
}
const mocks = new Set();
function isMockFunction(fn) {
return typeof fn === "function" && "_isMockFunction" in fn && fn._isMockFunction;
}
function spyOn(obj, method, accessType) {
const dictionary = {
get: "getter",
set: "setter"
};
const objMethod = accessType ? { [dictionary[accessType]]: method } : method;
let state;
const descriptor = getDescriptor(obj, method);
const fn = descriptor && descriptor[accessType || "value"];
// inherit implementations if it was already mocked
if (isMockFunction(fn)) {
state = fn.mock._state();
}
const stub = internalSpyOn(obj, objMethod);
const spy = enhanceSpy(stub);
if (state) {
spy.mock._state(state);
}
return spy;
}
let callOrder = 0;
function enhanceSpy(spy) {
const stub = spy;
let implementation;
let onceImplementations = [];
let implementationChangedTemporarily = false;
let instances = [];
let contexts = [];
let invocations = [];
const state = getInternalState(spy);
const mockContext = {
get calls() {
return state.calls;
},
get contexts() {
return contexts;
},
get instances() {
return instances;
},
get invocationCallOrder() {
return invocations;
},
get results() {
return state.results.map(([callType, value]) => {
const type = callType === "error" ? "throw" : "return";
return {
type,
value
};
});
},
get settledResults() {
return state.resolves.map(([callType, value]) => {
const type = callType === "error" ? "rejected" : "fulfilled";
return {
type,
value
};
});
},
get lastCall() {
return state.calls[state.calls.length - 1];
},
_state(state) {
if (state) {
implementation = state.implementation;
onceImplementations = state.onceImplementations;
implementationChangedTemporarily = state.implementationChangedTemporarily;
}
return {
implementation,
onceImplementations,
implementationChangedTemporarily
};
}
};
function mockCall(...args) {
instances.push(this);
contexts.push(this);
invocations.push(++callOrder);
const impl = implementationChangedTemporarily ? implementation : onceImplementations.shift() || implementation || state.getOriginal() || (() => {});
return impl.apply(this, args);
}
let name = stub.name;
stub.getMockName = () => name || "vi.fn()";
stub.mockName = (n) => {
name = n;
return stub;
};
stub.mockClear = () => {
state.reset();
instances = [];
contexts = [];
invocations = [];
return stub;
};
stub.mockReset = () => {
stub.mockClear();
implementation = undefined;
onceImplementations = [];
return stub;
};
stub.mockRestore = () => {
stub.mockReset();
state.restore();
return stub;
};
if (Symbol.dispose) {
stub[Symbol.dispose] = () => stub.mockRestore();
}
stub.getMockImplementation = () => implementationChangedTemporarily ? implementation : onceImplementations.at(0) || implementation;
stub.mockImplementation = (fn) => {
implementation = fn;
state.willCall(mockCall);
return stub;
};
stub.mockImplementationOnce = (fn) => {
onceImplementations.push(fn);
return stub;
};
function withImplementation(fn, cb) {
const originalImplementation = implementation;
implementation = fn;
state.willCall(mockCall);
implementationChangedTemporarily = true;
const reset = () => {
implementation = originalImplementation;
implementationChangedTemporarily = false;
};
const result = cb();
if (typeof result === "object" && result && typeof result.then === "function") {
return result.then(() => {
reset();
return stub;
});
}
reset();
return stub;
}
stub.withImplementation = withImplementation;
stub.mockReturnThis = () => stub.mockImplementation(function() {
return this;
});
stub.mockReturnValue = (val) => stub.mockImplementation(() => val);
stub.mockReturnValueOnce = (val) => stub.mockImplementationOnce(() => val);
stub.mockResolvedValue = (val) => stub.mockImplementation(() => Promise.resolve(val));
stub.mockResolvedValueOnce = (val) => stub.mockImplementationOnce(() => Promise.resolve(val));
stub.mockRejectedValue = (val) => stub.mockImplementation(() => Promise.reject(val));
stub.mockRejectedValueOnce = (val) => stub.mockImplementationOnce(() => Promise.reject(val));
Object.defineProperty(stub, "mock", { get: () => mockContext });
state.willCall(mockCall);
mocks.add(stub);
return stub;
}
function fn(implementation) {
const enhancedSpy = enhanceSpy(internalSpyOn({ spy: implementation || function() {} }, "spy"));
if (implementation) {
enhancedSpy.mockImplementation(implementation);
}
return enhancedSpy;
}
function getDescriptor(obj, method) {
const objDescriptor = Object.getOwnPropertyDescriptor(obj, method);
if (objDescriptor) {
return objDescriptor;
}
let currentProto = Object.getPrototypeOf(obj);
while (currentProto !== null) {
const descriptor = Object.getOwnPropertyDescriptor(currentProto, method);
if (descriptor) {
return descriptor;
}
currentProto = Object.getPrototypeOf(currentProto);
}
}
const spy = { fn, isMockFunction, mocks, spyOn };
const mockFn = spy.fn();
// replace target API with mock
electronApi[funcName] = mockFn;
}, apiName, funcName, { internal: true });
mock.update = async () => {
// synchronises inner and outer mocks
const calls = await browser.electron.execute((electron, apiName, funcName) => {
const mockObj = electron[apiName][funcName];
return mockObj.mock?.calls ? JSON.parse(JSON.stringify(mockObj.mock?.calls)) : [];
}, apiName, funcName, { internal: true });
// re-apply calls from the electron main process mock to the outer one
if (mock.mock.calls.length < calls.length) {
calls.forEach((call, index) => {
if (!mock.mock.calls[index]) {
mock?.apply(mock, call);
}
});
}
return mock;
};
mock.mockImplementation = async (implFn) => {
await browser.electron.execute((electron, apiName, funcName, mockImplementationStr) => {
const electronApi = electron[apiName];
const mockImpl = new Function(`return ${mockImplementationStr}`)();
electronApi[funcName].mockImplementation(mockImpl);
}, apiName, funcName, implFn.toString(), { internal: true });
outerMockImplementation(implFn);
return mock;
};
mock.mockImplementationOnce = async (implFn) => {
await browser.electron.execute((electron, apiName, funcName, mockImplementationStr) => {
const electronApi = electron[apiName];
const mockImpl = new Function(`return ${mockImplementationStr}`)();
electronApi[funcName].mockImplementationOnce(mockImpl);
}, apiName, funcName, implFn.toString(), { internal: true });
outerMockImplementationOnce(implFn);
return mock;
};
mock.mockReturnValue = async (value) => {
await browser.electron.execute((electron, apiName, funcName, returnValue) => {
const electronApi = electron[apiName];
electronApi[funcName].mockReturnValue(returnValue);
}, apiName, funcName, value, { internal: true });
return mock;
};
mock.mockReturnValueOnce = async (value) => {
await browser.electron.execute((electron, apiName, funcName, returnValue) => {
const electronApi = electron[apiName];
electronApi[funcName].mockReturnValueOnce(returnValue);
}, apiName, funcName, value, { internal: true });
return mock;
};
mock.mockResolvedValue = async (value) => {
await browser.electron.execute((electron, apiName, funcName, resolvedValue) => {
const electronApi = electron[apiName];
electronApi[funcName].mockResolvedValue(resolvedValue);
}, apiName, funcName, value, { internal: true });
return mock;
};
mock.mockResolvedValueOnce = async (value) => {
await browser.electron.execute((electron, apiName, funcName, resolvedValue) => {
const electronApi = electron[apiName];
electronApi[funcName].mockResolvedValueOnce(resolvedValue);
}, apiName, funcName, value, { internal: true });
return mock;
};
mock.mockRejectedValue = async (value) => {
await browser.electron.execute((electron, apiName, funcName, rejectedValue) => {
const electronApi = electron[apiName];
electronApi[funcName].mockRejectedValue(rejectedValue);
}, apiName, funcName, value, { internal: true });
return mock;
};
mock.mockRejectedValueOnce = async (value) => {
await browser.electron.execute((electron, apiName, funcName, rejectedValue) => {
const electronApi = electron[apiName];
electronApi[funcName].mockRejectedValueOnce(rejectedValue);
}, apiName, funcName, value, { internal: true });
return mock;
};
mock.mockClear = async () => {
// clears mock history
await browser.electron.execute((electron, apiName, funcName) => {
electron[apiName][funcName].mockClear();
}, apiName, funcName, { internal: true });
outerMockClear();
return mock;
};
mock.mockReset = async () => {
// resets inner implementation to an empty function and clears mock history
await browser.electron.execute((electron, apiName, funcName) => {
electron[apiName][funcName].mockReset();
}, apiName, funcName, { internal: true });
outerMockReset();
// vitest mockReset doesn't clear mock history so we need to explicitly clear both mocks
await mock.mockClear();
return mock;
};
mock.mockRestore = async () => {
// restores inner mock implementation to the original function
await restoreElectronFunctionality(apiName, funcName);
// clear mocks
outerMockClear();
await mock.mockClear();
return mock;
};
mock.mockReturnThis = async () => {
return await browser.electron.execute((electron, apiName, funcName) => {
electron[apiName][funcName].mockReturnThis();
}, apiName, funcName, { internal: true });
};
mock.withImplementation = async (implFn, callbackFn) => {
return await browser.electron.execute(async (electron, apiName, funcName, implFnStr, callbackFnStr) => {
const callback = new Function(`return ${callbackFnStr}`)();
const impl = new Function(`return ${implFnStr}`)();
let result;
electron[apiName][funcName].withImplementation(impl, () => {
result = callback(electron);
});
return result?.then ? await result : result;
}, apiName, funcName, implFn.toString(), callbackFn.toString(), { internal: true });
};
return mock;
}
async function mock(apiName, funcName) {
try {
// retrieve an existing mock from the store
const existingMock = mockStore.getMock(`electron.${apiName}.${funcName}`);
await existingMock.mockReset();
return existingMock;
}
catch (_e) {
// mock doesn't exist, create a new one and store it
const newMock = await createMock(apiName, funcName);
mockStore.setMock(newMock);
return newMock;
}
}
async function mockAll(apiName) {
const apiFnNames = await browser.electron.execute((electron, apiName) => Object.keys(electron[apiName]).toString(), apiName, { internal: true });
const mockedApis = apiFnNames
.split(',')
.reduce((a, funcName) => ({ ...a, [funcName]: () => undefined }), {});
for (const funcName in mockedApis) {
mockedApis[funcName] = await mock(apiName, funcName);
}
return mockedApis;
}
async function clearAllMocks(apiName) {
for (const [mockName, mock] of mockStore.getMocks()) {
if (!apiName || mockName.match(new RegExp(`^electron.${apiName}`))) {
await mock.mockClear();
}
}
}
async function resetAllMocks(apiName) {
for (const [mockName, mock] of mockStore.getMocks()) {
if (!apiName || mockName.match(new RegExp(`^electron.${apiName}`))) {
await mock.mockReset();
}
}
}
async function restoreAllMocks(apiName) {
for (const [mockName, mock] of mockStore.getMocks()) {
if (!apiName || mockName.match(new RegExp(`^electron.${apiName}`))) {
await mock.mockRestore();
}
}
}
async function execute$1(browser, script, ...args) {
/**
* parameter check
*/
if (typeof script !== 'string' && typeof script !== 'function') {
throw new Error('Expecting script to be type of "string" or "function"');
}
if (!browser) {
throw new Error('WDIO browser is not yet initialised');
}
if (!browser.electron.bridgeActive) {
const errMessage = 'Electron context bridge not available! ' +
'Did you import the service hook scripts into your application via e.g. ' +
"`import('wdio-electron-service/main')` and `import('wdio-electron-service/preload')`?\n\n" +
'Find more information at https://webdriver.io/docs/desktop-testing/electron#api-configuration';
throw new Error(errMessage);
}
const returnValue = await browser.execute(function executeWithinElectron(script, ...args) {
return window.wdioElectron.execute(script, args);
}, `${script}`, ...args);
return returnValue ?? undefined;
}
class ServiceConfig {
#globalOptions;
#cdpOptions;
#clearMocks = false;
#resetMocks = false;
#restoreMocks = false;
#browser;
#useCdpBridge = true;
constructor(globalOptions = {}, capabilities) {
this.#globalOptions = globalOptions;
const { clearMocks, resetMocks, restoreMocks } = Object.assign({}, this.#globalOptions, capabilities[CUSTOM_CAPABILITY_NAME]);
this.#clearMocks = clearMocks ?? false;
this.#resetMocks = resetMocks ?? false;
this.#restoreMocks = restoreMocks ?? false;
const { useCdpBridge } = globalOptions;
if (typeof useCdpBridge === 'boolean' && !useCdpBridge) {
this.#useCdpBridge = useCdpBridge;
}
this.#cdpOptions = {
...(globalOptions.cdpBridgeTimeout && { timeout: globalOptions.cdpBridgeTimeout }),
...(globalOptions.cdpBridgeWaitInterval && { waitInterval: globalOptions.cdpBridgeWaitInterval }),
...(globalOptions.cdpBridgeRetryCount && { connectionRetryCount: globalOptions.cdpBridgeRetryCount }),
};
}
get globalOptions() {
return this.#globalOptions;
}
get browser() {
return this.#browser;
}
set browser(browser) {
this.#browser = browser;
}
get cdpOptions() {
return this.#cdpOptions;
}
get clearMocks() {
return this.#clearMocks;
}
get resetMocks() {
return this.#resetMocks;
}
get restoreMocks() {
return this.#restoreMocks;
}
get useCdpBridge() {
return this.#useCdpBridge;
}
}
async function execute(browser, cdpBridge, script, ...args) {
/**
* parameter check
*/
if (typeof script !== 'string' && typeof script !== 'function') {
throw new Error('Expecting script to be type of "string" or "function"');
}
if (!browser) {
throw new Error('WDIO browser is not yet initialised');
}
if (browser.isMultiremote) {
const mrBrowser = browser;
return await Promise.all(mrBrowser.instances.map(async (instance) => {
const mrInstance = mrBrowser.getInstance(instance);
return mrInstance.electron.execute(script, ...args);
}));
}
if (!cdpBridge) {
throw new Error('CDP Bridge is not yet initialised');
}
const functionDeclaration = removeFirstArg(script.toString());
const argsArray = args.map((arg) => ({ value: arg }));
const scriptLength = Buffer.byteLength(functionDeclaration, 'utf-8');
if (scriptLength > 500) {
log.debug('Script for the execute:', `${functionDeclaration.split('\n')[0]} ... [${scriptLength} bytes]`);
}
else {
log.debug('Script for the execute:', functionDeclaration);
}
const result = await cdpBridge.send('Runtime.callFunctionOn', {
functionDeclaration,
arguments: argsArray,
awaitPromise: true,
returnByValue: true,
executionContextId: cdpBridge.contextId,
});
log.debug('Return of the script:', result.result.value);
await syncMockStatus(args);
return result.result.value ?? undefined;
}
const syncMockStatus = async (args) => {
const isInternalCommand = () => Boolean(args.at(-1)?.internal);
const mocks = mockStore.getMocks();
if (mocks.length > 0 && !isInternalCommand()) {
await Promise.all(mocks.map(async ([_mockId, mock]) => mock.update()));
}
};
//remove first arg `electron`. electron can be access as global scope.
const removeFirstArg = (funcStr) => {
// generate ATS
const ast = parse(funcStr, {
parser: {
parse: (source) => babelParser.parse(source, {
sourceType: 'module',
plugins: ['typescript'],
}),
},
});
let funcNode = null;
const topLevelNode = ast.program.body[0];
if (topLevelNode.type === 'ExpressionStatement') {
// Arrow function
funcNode = topLevelNode.expression;
}
else if (topLevelNode.type === 'FunctionDeclaration') {
// Function declaration
funcNode = topLevelNode;
}
if (!funcNode) {
throw new Error('Unsupported function type');
}
// Remove first args `electron` if exists
if ('params' in funcNode && Array.isArray(funcNode.params)) {
funcNode.params.shift();
}
return print(ast).code;
};
const getDebuggerEndpoint = (capabilities) => {
log.trace('Try to detect the node debugger endpoint');
const debugArg = capabilities['goog:chromeOptions']?.args?.find((item) => item.startsWith('--inspect='));
log.trace(`Detected debugger args: ${debugArg}`);
const debugUrl = debugArg ? debugArg.split('=')[1] : undefined;
const [host, strPort] = debugUrl ? debugUrl.split(':') : [];
const result = { host, port: Number(strPort) };
if (!result.host || !result.port) {
throw new SevereServiceError(`Failed to detect the debugger endpoint.`);
}
log.trace(`Detected the node debugger endpoint: `, result);
return result;
};
class ElectronCdpBridge extends CdpBridge {
#contextId = 0;
get contextId() {
return this.#contextId;
}
async connect() {
log.debug('CdpBridge options:', this.options);
await super.connect();
const contextHandler = this.#getContextIdHandler();
await this.send('Runtime.enable');
await this.send('Runtime.disable');
this.#contextId = await contextHandler;
await this.send('Runtime.evaluate', {
expression: getInitializeScript(),
includeCommandLineAPI: true,
replMode: true,
contextId: this.#contextId,
});
}
#getContextIdHandler() {
return new Promise((resolve, reject) => {
this.on('Runtime.executionContextCreated', (params) => {
if (params.context.auxData.isDefault) {
resolve(params.context.id);
}
});
setTimeout(() => {
const err = new Error('Timeout exceeded to get the ContextId.');
log.error(err.message);
reject(err);
}, this.options.timeout);
});
}
}
function getInitializeScript() {
const scripts = [
// Add __name to the global object to work around issue with function serialization
// This enables browser.execute to work with scripts which declare functions (affects TS specs only)
// https://github.com/webdriverio-community/wdio-electron-service/issues/756
// https://github.com/privatenumber/tsx/issues/113
`globalThis.__name = globalThis.__name ?? ((func) => func);`,
// Add electron to the global object
`globalThis.electron = require('electron');`,
];
// add because windows is not exposed the process object to global scope
if (os.type().match('Windows')) {
scripts.push(`globalThis.process = require('node:process');`);
}
return scripts.join('\n');
}
async function before(capabilities, instance) {
this.browser = instance;
const cdpBridge = this.browser.isMultiremote ? undefined : await initCdpBridge(this.cdpOptions, capabilities);
/**
* Add electron API to browser object
*/
this.browser.electron = getElectronAPI.call(this, this.browser, cdpBridge);
if (isMultiremote(instance)) {
const mrBrowser = instance;
for (const instance of mrBrowser.instances) {
const mrInstance = mrBrowser.getInstance(instance);
const caps = mrInstance.requestedCapabilities.alwaysMatch ||
mrInstance.requestedCapabilities;
if (!caps[CUSTOM_CAPABILITY_NAME]) {
continue;
}
log.debug('Adding Electron API to browser object instance named: ', instance);
const mrCdpBridge = await initCdpBridge(this.cdpOptions, caps);
mrInstance.electron = getElectronAPI.call(this, mrInstance, mrCdpBridge);
const mrPuppeteer = await getPuppeteer(mrInstance);
mrInstance.electron.windowHandle = await getActiveWindowHandle(mrPuppeteer);
// wait until an Electron BrowserWindow is available
await waitUntilWindowAvailable(mrInstance);
await copyOriginalApi(mrInstance);
}
}
else {
const puppeteer = await getPuppeteer(this.browser);
this.browser.electron.windowHandle = await getActiveWindowHandle(puppeteer);
// wait until an Electron BrowserWindow is available
await waitUntilWindowAvailable(this.browser);
await copyOriginalApi(this.browser);
}
}
function isMultiremote(browser) {
return browser.isMultiremote;
}
async function initCdpBridge(cdpOptions, capabilities) {
const options = Object.assign({}, cdpOptions, getDebuggerEndpoint(capabilities));
const cdpBridge = new ElectronCdpBridge(options);
await cdpBridge.connect();
return cdpBridge;
}
const waitUntilWindowAvailable = async (browser) => {
await browser.waitUntil(async () => {
const numWindows = (await browser.getWindowHandles()).length;
return numWindows > 0;
});
};
const copyOriginalApi = async (browser) => {
await browser.electron.execute(async (electron) => {
var toStringFunction = Function.prototype.toString;
var create = Object.create;
var toStringObject = Object.prototype.toString;
/**
* @classdesc Fallback cache for when WeakMap is not natively supported
*/
var LegacyCache = /** @class */ (function () {
function LegacyCache() {
this._keys = [];
this._values = [];
}
LegacyCache.prototype.has = function (key) {
return !!~this._keys.indexOf(key);
};
LegacyCache.prototype.get = function (key) {
return this._values[this._keys.indexOf(key)];
};
LegacyCache.prototype.set = function (key, value) {
this._keys.push(key);
this._values.push(value);
};
return LegacyCache;
}());
function createCacheLegacy() {
return new LegacyCache();
}
function createCacheModern() {
return new WeakMap();
}
/**
* Get a new cache object to prevent circular references.
*/
var createCache = typeof WeakMap !== 'undefined' ? createCacheModern : createCacheLegacy;
/**
* Get an empty version of the object with the same prototype it has.
*/
function getCleanClone(prototype) {
if (!prototype) {
return create(null);
}
var Constructor = prototype.constructor;
if (Constructor === Object) {
return prototype === Object.prototype ? {} : create(prototype);
}
if (Constructor &&
~toStringFunction.call(Constructor).indexOf('[native code]')) {
try {
return new Constructor();
}
catch (_a) { }
}
return create(prototype);
}
function getRegExpFlagsLegacy(regExp) {
var flags = '';
if (regExp.global) {
flags += 'g';
}
if (regExp.ignoreCase) {
flags += 'i';
}
if (regExp.multiline) {
flags += 'm';
}
if (regExp.unicode) {
flags += 'u';
}
if (regExp.sticky) {
flags += 'y';
}
return flags;
}
function getRegExpFlagsModern(regExp) {
return regExp.flags;
}
/**
* Get the flags to apply to the copied regexp.
*/
var getRegExpFlags = /test/g.flags === 'g' ? getRegExpFlagsModern : getRegExpFlagsLegacy;
function getTagLegacy(value) {
var type = toStringObject.call(value);
return type.substring(8, type.length - 1);
}
function getTagModern(value) {
return value[Symbol.toStringTag] || getTagLegacy(value);
}
/**
* Get the tag of the value passed, so that the correct copier can be used.
*/
var getTag = typeof Symbol !== 'undefined' ? getTagModern : getTagLegacy;
var defineProperty = Object.defineProperty, getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor, getOwnPropertyNames = Object.getOwnPropertyNames, getOwnPropertySymbols = Object.getOwnPropertySymbols;
var _a = Object.prototype, hasOwnProperty = _a.hasOwnProperty, propertyIsEnumerable = _a.propertyIsEnumerable;
var SUPPORTS_SYMBOL = typeof getOwnPropertySymbols === 'function';
function getStrictPropertiesModern(object) {
return getOwnPropertyNames(object).concat(getOwnPropertySymbols(object));
}
/**
* Get the properites used when copying objects strictly. This includes both keys and symbols.
*/
var getStrictProperties = SUPPORTS_SYMBOL
? getStrictPropertiesModern
: getOwnPropertyNames;
/**
* Striclty copy all properties contained on the object.
*/
function copyOwnPropertiesStrict(value, clone, state) {
var properties = getStrictProperties(value);
for (var index = 0, length_1 = properties.length, property = void 0, descriptor = void 0; index < length_1; ++index) {
property = properties[index];
if (property === 'callee' || property === 'caller') {
continue;
}
descriptor = getOwnPropertyDescriptor(value, property);
if (!descriptor) {
// In extra edge cases where the property descriptor cannot be retrived, fall back to
// the loose assignment.
clone[property] = state.copier(value[property], state);
continue;
}
// Only clone the value if actually a value, not a getter / setter.
if (!descriptor.get && !descriptor.set) {
descriptor.value = state.copier(descriptor.value, state);
}
try {
defineProperty(clone, property, descriptor);
}
catch (error) {
// Tee above can fail on node in edge cases, so fall back to the loose assignment.
clone[property] = descriptor.value;
}
}
return clone;
}
/**
* Deeply copy the indexed values in the array.
*/
function copyArrayLoose(array, state) {
var clone = new state.Constructor();
// set in the cache immediately to be able to reuse the object recursively
state.cache.set(array, clone);
for (var index = 0, length_2 = array.length; index < length_2; ++index) {
clone[index] = state.copier(array[index], state);
}
return clone;
}
/**
* Deeply copy the indexed values in the array, as well as any custom properties.
*/
function copyArrayStrict(array, state) {
var clone = new state.Constructor();
// set in the cache immediately to be able to reuse the object recursively
state.cache.set(array, clone);
return copyOwnPropertiesStrict(array, clone, state);
}
/**
* Copy the contents of the ArrayBuffer.
*/
function copyArrayBuffer(arrayBuffer, _state) {
return arrayBuffer.slice(0);
}
/**
* Create a new Blob with the contents of the original.
*/
function copyBlob(blob, _state) {
return blob.slice(0, blob.size, blob.type);
}
/**
* Create a new DataView with the contents of the original.
*/
function copyDataView(dataView, state) {
return new state.Constructor(copyArrayBuffer(dataView.buffer));
}
/**
* Create a new Date based on the time of the original.
*/
function copyDate(date, state) {
return new state.Constructor(date.getTime());
}
/**
* Deeply copy the keys and values of the original.
*/
function copyMapLoose(map, state) {
var clone = new state.Constructor();
// set in the cache immediately to be able to reuse the object recursively
state.cache.set(map, clone);
map.forEach(function (value, key) {
clone.set(key, state.copier(value, state));
});
return clone;
}
/**
* Deeply copy the keys and values of the original, as well as any custom properties.
*/
function copyMapStrict(map, state) {
return copyOwnPropertiesStrict(map, copyMapLoose(map, state), state);
}
function copyObjectLooseLegacy(object, state) {
var clone = getCleanClone(state.prototype);
// set in the cache immediately to be able to reuse the object recursively
state.cache.set(object, clone);
for (var key in object) {
if (hasOwnProperty.call(object, key)) {
clone[key] = state.copier(object[key], state);
}
}
return clone;
}
function copyObjectLooseModern(object, state) {
var clone = getCleanClone(state.prototype);
// set in the cache immediately to be able to reuse the object recursively
state.cache.set(object, clone);
for (var key in object) {
if (hasOwnProperty.call(object, key)) {
clone[key] = state.copier(object[key], state);
}
}
var symbols = getOwnPropertySymbols(object);
for (var index = 0, length_3 = symbols.length, symbol = void 0; index < length_3; ++index) {
symbol = symbols[index];
if (propertyIsEnumerable.call(object, symbol)) {
clone[symbol] = state.copier(object[symbol], state);
}
}
return clone;
}
/**
* Deeply copy the properties (keys and symbols) and values of the original.
*/
var copyObjectLoose = SUPPORTS_SYMBOL
? copyObjectLooseModern
: copyObjectLooseLegacy;
/**
* Deeply copy the properties (keys and symbols) and values of the original, as well
* as any hidden or non-enumerable properties.
*/
function copyObjectStrict(object, state) {
var clone = getCleanClone(state.prototype);
// set in the cache immediately to be able to reuse the object recursively
state.cache.set(object, clone);
return copyOwnPropertiesStrict(object, clone, state);
}
/**
* Create a new primitive wrapper from the value of the original.
*/
function copyPrimitiveWrapper(primitiveObject, state) {
return new state.Constructor(primitiveObject.valueOf());
}
/**
* Create a new RegExp based on the value and flags of the original.
*/
function copyRegExp(regExp, state) {
var clone = new state.Constructor(regExp.source, getRegExpFlags(regExp));
clone.lastIndex = regExp.lastIndex;
return clone;
}
/**
* Return the original value (an identity function).
*
* @note
* THis is used for objects that cannot be copied, such as WeakMap.
*/
function copySelf(value, _state) {
return value;
}
/**
* Deeply copy the values of the original.
*/
function copySetLoose(set, state) {
var clone = new state.Constructor();
// set in the cache immediately to be able to reuse the object recursively
state.cache.set(set, clone);
set.forEach(function (value) {
clone.add(state.copier(value, state));
});
return clone;
}
/**
* Deeply copy the values of the original, as well as any custom properties.
*/
function copySetStrict(set, state) {
return copyOwnPropertiesStrict(set, copySetLoose(set, state), state);
}
var isArray = Array.isArray;
var assign = Object.assign;
var getPrototypeOf = Object.getPrototypeOf || (function (obj) { return obj.__proto__; });
var DEFAULT_LOOSE_OPTIONS = {
array: copyArrayLoose,
arrayBuffer: copyArrayBuffer,
blob: copyBlob,
dataView: copyDataView,
date: copyDate,
error: copySelf,
map: copyMapLoose,
object: copyObjectLoose,
regExp: copyRegExp,
set: copySetLoose,
};
var DEFAULT_STRICT_OPTIONS = assign({}, DEFAULT_LOOSE_OPTIONS, {
array: copyArrayStrict,
map: copyMapStrict,
object: copyObjectStrict,
set: copySetStrict,
});
/**
* Get the copiers used for each specific object tag.
*/
function getTagSpecificCopiers(options) {
return {
Arguments: options.object,
Array: options.array,
ArrayBuffer: options.arrayBuffer,
Blob: options.blob,
Boolean: copyPrimitiveWrapper,
DataView: options.dataView,
Date: options.date,
Error: options.error,
Float32Array: options.arrayBuffer,
Float64Array: options.arrayBuffer,
Int8Array: options.arrayBuffer,
Int16Array: options.arrayBuffer,
Int32Array: options.arrayBuffer,
Map: options.map,
Number: copyPrimitiveWrapper,
Object: options.object,
Promise: copySelf,
RegExp: options.regExp,
Set: options.set,
String: copyPrimitiveWrapper,
WeakMap: copySelf,
WeakSet: copySelf,
Uint8Array: options.arrayBuffer,
Uint8ClampedArray: options.arrayBuffer,
Uint16Array: options.arrayBuffer,
Uint32Array: options.arrayBuffer,
Uint64Array: options.arrayBuffer,
};
}
/**
* Create a custom copier based on the object-specific copy methods passed.
*/
function createCopier(options) {
var normalizedOptions = assign({}, DEFAULT_LOOSE_OPTIONS, options);
var tagSpecificCopiers = getTagSpecificCopiers(normalizedOptions);
var array = tagSpecificCopiers.Array, object = tagSpecificCopiers.Object;
function copier(value, state) {
state.prototype = state.Constructor = undefined;
if (!value || typeof value !== 'object') {
return value;
}
if (state.cache.has(value)) {
return state.cache.get(value);
}
state.prototype = getPrototypeOf(value);
state.Constructor = state.prototype && state.prototype.constructor;
// plain objects
if (!state.Constructor || state.Constructor === Object) {
return object(value, state);
}
// arrays
if (isArray(value)) {
return array(value, state);
}
var tagSpecificCopier = tagSpecificCopiers[getTag(value)];
if (tagSpecificCopier) {
return tagSpecificCopier(value, state);
}
return typeof value.then === 'function' ? value : object(value, state);
}
return function copy(value) {
return copier(value, {
Constructor: undefined,
cache: createCache(),
copier: copier,
prototype: undefined,
});
};
}
/**
* Create a custom copier based on the object-specific copy methods passed, defaulting to the
* same internals as `copyStrict`.
*/
function createStrictCopier(options) {
return createCopier(assign({}, DEFAULT_STRICT_OPTIONS, options));
}
/**
* Copy an value deeply as much as possible, where strict recreation of object properties
* are maintained. All properties (including non-enumerable ones) are copied with their
* original property descriptors on both objects and arrays.
*/
createStrictCopier({});
/**
* Copy an value deeply as much as possible.
*/
var index = createCopier({});
const { default: copy } = { default: index };
globalThis.originalApi = {};
for (const api in electron) {
const apiName = api;
globalThis.originalApi[apiName] = {};
for (const apiElement in electron[apiName]) {
const apiElementName = apiElement;
globalThis.originalApi[apiName][apiElementName] = copy(electron[apiName][apiElementName]);
}
}
}, { internal: true });
};
function getElectronAPI(browser, cdpBridge) {
const api = {
clearAllMocks: clearAllMocks.bind(this),
execute: (script, ...args) => execute.apply(this, [browser, cdpBridge, script, ...args]),
isMockFunction: isMockFunction.bind(this),
mock: mock.bind(this),
mockAll: mockAll.bind(this),
resetAllMocks: resetAllMocks.bind(this),
restoreAllMocks: restoreAllMocks.bind(this),
};
return Object.assign({}, api);
}
// TODO: This file should be remove at V9
/* v8 ignore start */
const yellow = '\u001b[33m';
const reset = '\u001b[0m';
const LINE00 = '* WARNING -------------------------------------------------------------------- *';
const LINE01 = '| You can remove importing main/preload scripts provided by this service. |';
const LINE02 = '| Because the wdio-electron-service no longer requires the IPC-Bridge. |';
const LINE03 = '| Those scripts will be completely removed with v9. |';
const LINE05 = '* ---------------------------------------------------------------------------- *';
const LINE11 = '| The IPC-Bridge is deprecated. |';
const LINE12 = '| Please consider migrating to the `CDP-Bridge` |';
const LINE13 = '| by changing the value of `useCdpBridge` to `true` or removing it. |';
const LINE14 = '| The legacy IPC-Bridge and related functionality will be removed with v9. |';
const colourise = (str) => {
if (str === LINE00 || str === LINE05) {
return `${yellow}${str}${reset}`;
}
else {
return str.replace(/\|/g, `${yellow}|${reset}`);
}
};
async function isActive(browser) {
return await browser.execute(function executeWithinElectron() {
return window.wdioElectron !== undefined;
});
}
async function ipcBridgeCheck(browser) {
const result = browser.isMultiremote
? (await Promise.all(browser.instances.map(async (mrBrowser) => await isActive(browser.getInstance(mrBrowser))))).filter((result) => result).length > 0
: