UNPKG

wdio-electron-service

Version:

WebdriverIO service to enable Electron testing

1,367 lines (1,342 loc) 67.8 kB
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 :