diff --git a/package.json b/package.json index 063497ee..32e79829 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,14 @@ "scripts": { "init-dev": "pnpm i --filter=\\!fixture-\\* -r && husky install", "ci": "pnpm i && pnpm build && pnpm test && pnpm lint && pnpm format:check", - "clean": "pnpx rimraf ./node_modules pnpm-lock.yaml ./dist", + "clean": "pnpx rimraf ./node_modules pnpm-lock.yaml ./dist ./src/cjs/preload.ts ./src/cjs/main.ts ./src/cjs/constants.ts ./src/cjs/types.ts", "clean:dist": "pnpx rimraf ./dist", "clean:all": "pnpm clean && pnpm -r --reverse clean", "build": "pnpm build:esm && pnpm build:cjs", "build:esm": "tsc", - "build:cjs": "tsc --build --verbose tsconfig.cjs.json", + "build:cjs": "pnpm build:cjs:copy && pnpm build:cjs:compile", + "build:cjs:copy": "cp -f ./src/preload.ts ./src/cjs/preload.ts && cp -f ./src/main.ts ./src/cjs/main.ts && cp -f ./src/preload.ts ./src/cjs/preload.ts && cp -f ./src/constants.ts ./src/cjs/constants.ts && cp -f ./src/types.ts ./src/cjs/types.ts", + "build:cjs:compile": "tsc --build --verbose tsconfig.cjs.json", "lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint \"**/*.{j,mj,cj,t}s\"", "lint:fix": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint \"**/*.{j,mj,cj,t}s\" --fix", "format": "prettier --write \"**/*.{j,t}s\" \"**/*.{yml,md,json}\"", diff --git a/src/cjs/constants.ts b/src/cjs/constants.ts deleted file mode 120000 index 3f3521e9..00000000 --- a/src/cjs/constants.ts +++ /dev/null @@ -1 +0,0 @@ -../constants.ts \ No newline at end of file diff --git a/src/cjs/main.ts b/src/cjs/main.ts deleted file mode 120000 index cd4c0cc5..00000000 --- a/src/cjs/main.ts +++ /dev/null @@ -1 +0,0 @@ -../main.ts \ No newline at end of file diff --git a/src/cjs/preload.ts b/src/cjs/preload.ts deleted file mode 120000 index 45d101df..00000000 --- a/src/cjs/preload.ts +++ /dev/null @@ -1 +0,0 @@ -../preload.ts \ No newline at end of file diff --git a/src/cjs/types.ts b/src/cjs/types.ts deleted file mode 100644 index d64190b0..00000000 --- a/src/cjs/types.ts +++ /dev/null @@ -1,519 +0,0 @@ -import type * as Electron from 'electron'; -import type { Mock } from '@vitest/spy'; - -/** - * set this environment variable so that the preload script can be loaded - */ -process.env.WDIO_ELECTRON = 'true'; - -export type Fn = (...args: unknown[]) => unknown; -export type AsyncFn = (...args: unknown[]) => Promise; -export type AbstractFn = Fn | AsyncFn; -export type ElectronApiFn = ElectronType[ElectronInterface][keyof ElectronType[ElectronInterface]]; - -export interface ElectronServiceAPI { - /** - * Mock a function from the Electron API. - * @param apiName name of the API to mock - * @param funcName name of the function to mock - * @param mockReturnValue value to return when the mocked function is called - * @returns a {@link Promise} that resolves once the mock is registered - * - * @example - * ```js - * // mock the dialog API showOpenDialog method - * const showOpenDialog = await browser.electron.mock('dialog', 'showOpenDialog'); - * await browser.electron.execute( - * async (electron) => - * await electron.dialog.showOpenDialog({ - * properties: ['openFile', 'openDirectory'], - * }), - * ); - * - * expect(showOpenDialog).toHaveBeenCalledTimes(1); - * expect(showOpenDialog).toHaveBeenCalledWith({ - * properties: ['openFile', 'openDirectory'], - * }); - * ``` - */ - mock: ( - apiName: Interface, - funcName?: string, - returnValue?: unknown, - ) => Promise; - /** - * Mock all functions from an Electron API. - * @param apiName name of the API to mock - * @returns a {@link Promise} that resolves once the mock is registered - * - * @example - * ```js - * // mock multiple functions from the app API - * const app = await browser.electron.mockAll('app'); - * await app.getName.mockReturnValue('mocked-app'); - * await app.getVersion.mockReturnValue('1.0.0-mocked.12'); - * const result = await browser.electron.execute((electron) => `${electron.app.getName()}::${electron.app.getVersion()}`); - * expect(result).toEqual('mocked-app::1.0.0-mocked.12'); - * ``` - */ - mockAll: (apiName: Interface) => Promise>; - /** - * Execute a function within the Electron main process. - * - * @example - * ```js - * await browser.electron.execute((electron, param1, param2, param3) => { - * const appWindow = electron.BrowserWindow.getFocusedWindow(); - * electron.dialog.showMessageBox(appWindow, { - * message: 'Hello World!', - * detail: `${param1} + ${param2} + ${param3} = ${param1 + param2 + param3}` - * }); - * }, 1, 2, 3) - * ``` - * - * @param script function to execute - * @param args function arguments - */ - execute( - script: string | ((electron: typeof Electron, ...innerArgs: InnerArguments) => ReturnValue), - ...args: InnerArguments - ): Promise; - /** - * Clear mocked Electron API function(s) - * - * @example - * ```js - * // clears all mocked functions - * await browser.electron.clearAllMocks() - * // clears all mocked functions of dialog API - * await browser.electron.clearAllMocks('dialog') - * ``` - * - * @param apiName mocked api to clear - */ - clearAllMocks: (apiName?: string) => Promise; - /** - * Reset mocked Electron API function(s) - * - * @example - * ```js - * // resets all mocked functions - * await browser.electron.resetAllMocks() - * // resets all mocked functions of dialog API - * await browser.electron.resetAllMocks('dialog') - * ``` - * - * @param apiName mocked api to reset - */ - resetAllMocks: (apiName?: string) => Promise; - /** - * Restore mocked Electron API function(s) - * - * @example - * ```js - * // restores all mocked functions - * await browser.electron.restoreAllMocks() - * // restores all mocked functions of dialog API - * await browser.electron.restoreAllMocks('dialog') - * ``` - * - * @param apiName mocked api to remove - */ - restoreAllMocks: (apiName?: string) => Promise; -} - -/** - * The options for the Electron Service. - */ -export interface ElectronServiceOptions { - /** - * The path to the electron binary of the app for testing. - */ - appBinaryPath?: string; - /** - * An array of string arguments to be passed through to the app on execution of the test run. - * Electron [command line switches](https://www.electronjs.org/docs/latest/api/command-line-switches) - * and some [Chromium switches](https://peter.sh/experiments/chromium-command-line-switches) can be - * used here. - */ - appArgs?: string[]; - /** - * Calls .mockClear() on all mocked APIs before each test. This will clear mock history, but not reset its implementation. - */ - clearMocks?: boolean; - /** - * Calls .mockReset() on all mocked APIs before each test. This will clear mock history and reset its implementation to an empty function (will return undefined). - */ - resetMocks?: boolean; - /** - * Calls .mockRestore() on all mocked APIs before each test. This will restore the original API function, the mock will be removed. - */ - restoreMocks?: boolean; -} - -export type ApiCommand = { name: string; bridgeProp: string }; -export type WebdriverClientFunc = (this: WebdriverIO.Browser, ...args: unknown[]) => Promise; - -export type ElectronType = typeof Electron; -export type ElectronInterface = keyof ElectronType; - -export type ElectronBuilderConfig = { - productName?: string; - directories?: { output?: string }; -}; - -export type ElectronForgeConfig = { - buildIdentifier: string; - packagerConfig: { name: string }; -}; - -export type AppBuildInfo = { - appName: string; - config: string | ElectronForgeConfig | ElectronBuilderConfig; - isBuilder: boolean; - isForge: boolean; -}; - -export type ExecuteOpts = { - internal?: boolean; -}; - -export type WdioElectronWindowObj = { - execute: (script: string, args?: unknown[]) => unknown; -}; - -type Override = - | 'mockImplementation' - | 'mockImplementationOnce' - | 'mockReturnValue' - | 'mockReturnValueOnce' - | 'mockResolvedValue' - | 'mockResolvedValueOnce' - | 'mockRejectedValue' - | 'mockRejectedValueOnce' - | 'mockClear' - | 'mockReset' - | 'mockReturnThis' - | 'mockName' - | 'withImplementation'; - -interface ElectronMockInstance extends Omit { - /** - * Accepts a function that will be used as an implementation of the mock. - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * let callsCount = 0; - * await mockGetName.mockImplementation(() => { - * // callsCount is not accessible in the electron context so we need to guard it - * if (typeof callsCount !== 'undefined') { - * callsCount++; - * } - * return 'mocked value'; - * }); - * - * const name = await browser.electron.execute(async (electron) => await electron.app.getName()); - * expect(callsCount).toBe(1); - * expect(name).toBe('mocked value'); - * ``` - */ - mockImplementation(fn: AbstractFn): Promise; - /** - * Accepts a function that will be used as the mock's implementation during the next call. If chained, every consecutive call will produce different results. - * - * When the mocked function runs out of implementations, it will invoke the default implementation set with `mockImplementation`. - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * await mockGetName.mockImplementation(() => 'default mock'); - * await mockGetName.mockImplementationOnce(() => 'first mock'); - * await mockGetName.mockImplementationOnce(() => 'second mock'); - * - * let name = await browser.electron.execute((electron) => electron.app.getName()); - * expect(name).toBe('first mock'); - * name = await browser.electron.execute((electron) => electron.app.getName()); - * expect(name).toBe('second mock'); - * name = await browser.electron.execute((electron) => electron.app.getName()); - * expect(name).toBe('default mock'); - * ``` - */ - mockImplementationOnce(fn: AbstractFn): Promise; - /** - * Accepts a value that will be returned whenever the mock function is called. - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * await mockGetName.mockReturnValue('mocked name'); - * - * const name = await browser.electron.execute((electron) => electron.app.getName()); - * expect(name).toBe('mocked name'); - * ``` - */ - mockReturnValue(obj: unknown): Promise; - /** - * Accepts a value that will be returned during the next function call. If chained, every consecutive call will return the specified value. - * - * When there are no more `mockReturnValueOnce` values to use, the mock will fall back to the previously defined implementation if there is one. - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName') - * await mockGetName.mockReturnValue('default mock'); - * await mockGetName.mockReturnValueOnce('first mock'); - * await mockGetName.mockReturnValueOnce('second mock'); - * - * let name = await browser.electron.execute((electron) => electron.app.getName()); - * expect(name).toBe('first mock'); - * name = await browser.electron.execute((electron) => electron.app.getName()); - * expect(name).toBe('second mock'); - * name = await browser.electron.execute((electron) => electron.app.getName()); - * expect(name).toBe('default mock'); - * ``` - */ - mockReturnValueOnce(obj: unknown): Promise; - /** - * Accepts a value that will be resolved when an async function is called. - * - * @example - * ```js - * const mockGetFileIcon = await browser.electron.mock('app', 'getFileIcon'); - * await mockGetFileIcon.mockResolvedValue('This is a mock'); - * - * const fileIcon = await browser.electron.execute( - * async (electron) => await electron.app.getFileIcon('/path/to/icon'), - * ); - * - * expect(fileIcon).toBe('This is a mock'); - * ``` - */ - mockResolvedValue(obj: unknown): Promise; - /** - * Accepts a value that will be resolved during the next function call. If chained, every consecutive call will resolve the specified value. - * - * @example - * ```js - * const mockGetFileIcon = await browser.electron.mock('app', 'getFileIcon'); - * await mockGetFileIcon.mockResolvedValue('default mock') - * await mockGetFileIcon.mockResolvedValueOnce('first mock'); - * await mockGetFileIcon.mockResolvedValueOnce('second mock'); - * - * let fileIcon = await browser.electron.execute( - * async (electron) => await electron.app.getFileIcon('/path/to/icon'), - * ); - * expect(fileIcon).toBe('first mock'); - * fileIcon = await browser.electron.execute( - * async (electron) => await electron.app.getFileIcon('/path/to/icon'), - * ); - * expect(fileIcon).toBe('second mock'); - * fileIcon = await browser.electron.execute( - * async (electron) => await electron.app.getFileIcon('/path/to/icon'), - * ); - * expect(fileIcon).toBe('default mock'); - * ``` - */ - mockResolvedValueOnce(obj: unknown): Promise; - /** - * Accepts a value that will be rejected when an async function is called. - * - * @example - * ```js - * const mockGetFileIcon = await browser.electron.mock('app', 'getFileIcon'); - * await mockGetFileIcon.mockRejectedValue('This is a mock error'); - * - * const fileIconError = await browser.electron.execute(async (electron) => { - * try { - * await electron.app.getFileIcon('/path/to/icon'); - * } catch (e) { - * return e; - * } - * }); - * - * expect(fileIconError).toBe('This is a mock error'); - * ``` - */ - mockRejectedValue(obj: unknown): Promise; - /** - * Accepts a value that will be rejected during the next function call. If chained, every consecutive call will resolve the specified value. - * - * @example - * ```js - * const mockGetFileIcon = await browser.electron.mock('app', 'getFileIcon'); - * await mockGetFileIcon.mockRejectedValue('default mocked icon error') - * await mockGetFileIcon.mockRejectedValueOnce('first mocked icon error'); - * await mockGetFileIcon.mockRejectedValueOnce('second mocked icon error'); - * - * const getFileIcon = async () => - * await browser.electron.execute(async (electron) => { - * try { - * await electron.app.getFileIcon('/path/to/icon'); - * } catch (e) { - * return e; - * } - * }); - * - * let fileIcon = await getFileIcon(); - * expect(fileIcon).toBe('first mocked icon error'); - * fileIcon = await getFileIcon(); - * expect(fileIcon).toBe('second mocked icon error'); - * fileIcon = await getFileIcon(); - * expect(fileIcon).toBe('default mocked icon error'); - * ``` - */ - mockRejectedValueOnce(obj: unknown): Promise; - /** - * Clears the history of the mocked Electron API function. The mock implementation will not be reset. - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * await browser.electron.execute((electron) => electron.app.getName()); - * - * await mockGetName.mockClear(); - * - * await browser.electron.execute((electron) => electron.app.getName()); - * expect(mockGetName).toHaveBeenCalledTimes(1); - * ``` - */ - mockClear(): Promise; - /** - * Resets the mocked Electron API function. The mock history will be cleared and the implementation will be reset to an empty function (returning `undefined`). - * - * This also resets all "once" implementations. - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * await mockGetName.mockReturnValue('mocked name'); - * await browser.electron.execute((electron) => electron.app.getName()); - * - * await mockGetName.mockReset(); - * - * const name = await browser.electron.execute((electron) => electron.app.getName()); - * expect(name).toBeUndefined(); - * expect(mockGetName).toHaveBeenCalledTimes(1); - * ``` - */ - mockReset(): Promise; - /** - * Restores the original implementation to the Electron API function. - * - * @example - * ```js - * const appName = await browser.electron.execute((electron) => electron.app.getName()); - * const mockGetName = await browser.electron.mock('app', 'getName'); - * await mockGetName.mockReturnValue('mocked name'); - * - * await mockGetName.mockRestore(); - * - * const name = await browser.electron.execute((electron) => electron.app.getName()); - * expect(name).toBe(appName); - * ``` - */ - mockRestore(): Promise; - /** - * Useful if you need to return the `this` context from the method without invoking implementation. This is a shorthand for: - * - * ```js - * await spy.mockImplementation(function () { - * return this; - * }); - * ``` - * - * ...which enables API functions to be chained: - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * const mockGetVersion = await browser.electron.mock('app', 'getVersion'); - * await mockGetName.mockReturnThis(); - * await browser.electron.execute((electron) => - * electron.app.getName().getVersion() - * ); - * - * expect(mockGetVersion).toHaveBeenCalled(); - * ``` - */ - mockReturnThis(): Promise; - /** - * Overrides the original mock implementation temporarily while the callback is being executed. - * The electron object is passed into the callback in the same way as for `execute`. - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * const withImplementationResult = await mockGetName.withImplementation( - * () => 'temporary mock name', - * (electron) => electron.app.getName(), - * ); - * - * expect(withImplementationResult).toBe('temporary mock name'); - * ``` - * - * It can also be used with an asynchronous callback: - * - * @example - * ```js - * const mockGetFileIcon = await browser.electron.mock('app', 'getFileIcon'); - * const withImplementationResult = await mockGetFileIcon.withImplementation( - * () => Promise.resolve('temporary mock icon'), - * async (electron) => await electron.app.getFileIcon('/path/to/icon'), - * ); - * - * expect(withImplementationResult).toBe('temporary mock icon'); - * ``` - * - */ - withImplementation( - implFn: AbstractFn, - callbackFn: (electron: typeof Electron, ...innerArgs: InnerArguments) => ReturnValue, - ): Promise; - /** - * Assigns a name to the mock. Useful to see the name of the mock if an assertion fails. - * The name can be retrieved via `getMockName`. - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * mockGetName.mockName('test mock'); - * - * expect(mockGetName.getMockName()).toBe('test mock'); - * ``` - */ - mockName(name: string): ElectronMock; - /** - * Returns the assigned name of the mock. Defaults to `electron..`. - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * - * expect(mockGetName.getMockName()).toBe('electron.app.getName'); - * ``` - */ - getMockName(): string; - /** - * Returns the current mock implementation. The default implementation is an empty function (returns `undefined`). - * - * @example - * ```js - * const mockGetName = await browser.electron.mock('app', 'getName'); - * await mockGetName.mockImplementation(() => 'mocked name'); - * const mockImpl = mockGetName.getMockImplementation(); - * - * expect(mockImpl()).toBe('mocked name'); - * ``` - */ - getMockImplementation(): AbstractFn; - /** - * Used internally to update the outer mock function with calls from the inner (Electron context) mock. - */ - update(): Promise; -} - -export interface ElectronMock extends ElectronMockInstance { - new (...args: TArgs): TReturns; - (...args: TArgs): TReturns; -}