Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat): jest/vitest like API mocking #255

Merged
merged 59 commits into from
Dec 2, 2023
Merged
Changes from 49 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
34a1f76
(feat): support function execution in main process
christian-bromann Oct 31, 2023
114bebc
(feat): jest/vitest like API mocking
christian-bromann Nov 1, 2023
1bd6dbb
move npmrc to root
goosewobbler Nov 8, 2023
b3ac35a
fix test
goosewobbler Nov 8, 2023
aa12125
ignore fixtures lockfiles
goosewobbler Nov 8, 2023
a09e586
add filter to init-dev
goosewobbler Nov 8, 2023
4016091
update lockfiles
goosewobbler Nov 8, 2023
7d7aad7
use WebdriverClientFunc type
goosewobbler Nov 8, 2023
63d2e2a
use @vitest/spy
goosewobbler Nov 8, 2023
2118363
Merge branch 'main' into cb/better-mock
goosewobbler Nov 8, 2023
b7178f6
move npmrc back to example-cjs
goosewobbler Nov 8, 2023
0a9b505
use @vitest/spy
goosewobbler Nov 8, 2023
5bbd5a7
remove commonjs plugin from esm bundle
goosewobbler Nov 8, 2023
a79ecd6
Merge branch 'cb/better-mock' into sm/better-mock-updates
goosewobbler Nov 8, 2023
e861e45
update @vitest/spy, update lockfiles
goosewobbler Nov 8, 2023
35c9840
update deps
goosewobbler Nov 14, 2023
70c23e9
fix test
goosewobbler Nov 14, 2023
7923e8f
update @vitest/spy
goosewobbler Nov 18, 2023
553a6d8
update electron-forge
goosewobbler Nov 18, 2023
f43d9cd
chore: update deps
goosewobbler Nov 21, 2023
d0289f0
chore: update missing types, add debug
goosewobbler Nov 21, 2023
f94fdbe
chore: update mock tests for new approach
goosewobbler Nov 21, 2023
72b2c1a
feat: update mock
goosewobbler Nov 23, 2023
58715f8
chore: update lockfiles
goosewobbler Nov 23, 2023
8941c5f
test: update e2es for new mock
goosewobbler Nov 23, 2023
2b6849b
chore: fix linting
goosewobbler Nov 24, 2023
78e78ae
chore: update types
goosewobbler Nov 28, 2023
475ff96
test: expand mocking tests, misc fixes
goosewobbler Nov 28, 2023
2775064
feat: mockReturnValue
goosewobbler Nov 28, 2023
b8b0f00
chore: formatting
goosewobbler Nov 28, 2023
c9ffd74
chore: update lockfiles
goosewobbler Nov 28, 2023
98f885b
fix: type for internal globalThis.fn
goosewobbler Nov 28, 2023
e94c6c0
chore: update Electron
goosewobbler Nov 28, 2023
d9b122a
chore: standardise test imports
goosewobbler Nov 28, 2023
643477c
chore: rename clearMocks => removeMocks
goosewobbler Nov 28, 2023
bd17446
test: add removeMock units
goosewobbler Nov 28, 2023
8b62f23
chore: update imports
goosewobbler Nov 28, 2023
4bd0dcf
test: add execute units
goosewobbler Nov 28, 2023
dfede3a
test: temporarily reduce coverage
goosewobbler Nov 28, 2023
e054c03
chore: update dependencies
goosewobbler Nov 29, 2023
42dad2f
feat: single line mocking
goosewobbler Nov 29, 2023
0924870
refactor: streamlining mock API
goosewobbler Nov 29, 2023
f3e187f
feat: mockAll
goosewobbler Nov 30, 2023
75ceb33
chore: update lockfiles
goosewobbler Nov 30, 2023
60f4994
chore: update coverage
goosewobbler Nov 30, 2023
b13b0d1
Merge pull request #269 from webdriverio-community/sm/better-mock-upd…
goosewobbler Nov 30, 2023
30d0194
Merge branch 'main' into cb/better-mock
goosewobbler Nov 30, 2023
25e94e3
docs: remove old API functions
goosewobbler Nov 30, 2023
c88e46a
Merge branch 'main' into cb/better-mock
goosewobbler Nov 30, 2023
2dadcae
refactor: remove old api methods
goosewobbler Nov 30, 2023
2f79be1
test: update e2es for apis removal
goosewobbler Nov 30, 2023
4ca698c
test: update units for apis removal
goosewobbler Nov 30, 2023
0154a17
refactor: update & extract types
goosewobbler Nov 30, 2023
ac9f0cd
chore: update lockfiles
goosewobbler Nov 30, 2023
abe59cb
Merge pull request #310 from webdriverio-community/sm/remove-apis
goosewobbler Nov 30, 2023
c2808f1
test: update for strict equality check
goosewobbler Nov 30, 2023
5b3f4cf
docs: update for new mocking
goosewobbler Dec 1, 2023
ad134e9
refactor: standardise on runtime privacy
goosewobbler Dec 1, 2023
95f8cda
chore: update deps
goosewobbler Dec 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -121,3 +121,6 @@ wdio-logs

# Electron Forge output
out

# Fixtures lockfiles - installation shouldn't happen for fixtures
test/fixtures/**/pnpm-lock.yaml
62 changes: 17 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
@@ -108,42 +108,34 @@ if (process.env.NODE_ENV === 'test') {
}
```

The APIs should not work outside of WDIO but for security reasons it is encouraged to use dynamic imports wrapped in conditionals to ensure the APIs are only exposed when the app is being tested.

After importing the scripts the APIs should now be available in tests.

Currently available APIs: [`app`](https://www.electronjs.org/docs/latest/api/app), [`browserWindow`](https://www.electronjs.org/docs/latest/api/browser-window), [`dialog`](https://www.electronjs.org/docs/latest/api/dialog), [`mainProcess`](https://www.electronjs.org/docs/latest/api/process).

The service re-exports the WDIO browser object with the `.electron` namespace for API usage in your tests:

```ts
import { browser } from 'wdio-electron-service';

// in a test
const appName = await browser.electron.app('getName');
```
For security reasons it is encouraged to use dynamic imports wrapped in conditionals to ensure electron main process access is only available when the app is being tested.

### Execute Electron Scripts

You can execute arbitrary scripts within the context of your Electron application main process using `browser.electron.execute(...)`. This allows you to potentially change the behavior of your application at runtime or trigger certain events.
You can execute arbitrary scripts within the context of your Electron application main process using `browser.electron.execute(...)`. This allows you to access the Electron APIs in a fluid way, in case you wish to manipulate your application at runtime or trigger certain events.

For example you can trigger an message modal from your test via:
For example, you can trigger an message modal from your test via:

```ts
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)
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,
);
```

which will make the application trigger the following alert:

![Execute Demo](./.github/assets/execute-demo.png "Execute Demo")
![Execute Demo](./.github/assets/execute-demo.png 'Execute Demo')

__Note:__ The first argument of the function will be always the default export of the `electron` package that contains the [Electron API](https://www.electronjs.org/docs/latest/api/app).
**Note:** The first argument of the function will be always the default export of the `electron` package that contains the [Electron API](https://www.electronjs.org/docs/latest/api/app).

### Mocking Electron APIs

@@ -155,26 +147,6 @@ const result = await browser.electron.dialog('showOpenDialog');
console.log(result); // 'dialog opened!'
```

### Custom Electron API

You can also implement a custom API if you wish. To do this you will need to define a handler in your main process:

```ts
import { ipcMain } from 'electron';

ipcMain.handle('wdio-electron', () => {
// access some Electron or Node things on the main process
return 'such api';
});
```

The custom API can then be called in a spec file:

```ts
const someValue = await browser.electron.api('wow'); // default
const someValue = await browser.electron.myCustomAPI('wow'); // configured using `customApiBrowserCommand`
```

## Example

Check out our [Electron boilerplate](https://github.com/webdriverio/electron-boilerplate) project that showcases how to integrate WebdriverIO in an example application. You can also have a look at the [Example App](./example/app/) and [E2Es](./example/e2e/) in this repository.
2 changes: 1 addition & 1 deletion example-cjs/.npmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Electron Forge doesn't tolerate PNPM symlinks
# can be removed when https://github.com/electron/forge/pull/3351 is merged
# can be removed when https://github.com/electron/forge/issues/2633 is fixed
node-linker=hoisted
174 changes: 145 additions & 29 deletions example-cjs/e2e/api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import fs from 'node:fs';
import path from 'node:path';
import { browser } from '@wdio/globals';

import { expect } from '@wdio/globals';
import { browser } from 'wdio-electron-service';

const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), { encoding: 'utf-8' }));
const { name, version } = packageJson;
const { name: appName, version: appVersion } = packageJson as { name: string; version: string };

describe('electron APIs', () => {
describe('app', () => {
it('should retrieve app metadata through the electron API', async () => {
const appName = await browser.electron.app('getName');
expect(appName).toEqual(name);
const appVersion = await browser.electron.app('getVersion');
expect(appVersion).toEqual(version);
const name = await browser.electron.app('getName');
expect(name).toEqual(appName);
const version = await browser.electron.app('getVersion');
expect(version).toEqual(appVersion);
});
});

@@ -48,36 +50,150 @@ describe('electron APIs', () => {
expect(processType).toEqual('browser');
});
});
});

describe('mocking', () => {
afterEach(async () => {
await browser.electron.removeMocks();
});

describe('mock', () => {
it('should mock the expected electron API function', async () => {
await browser.electron.mock('dialog', 'showOpenDialog', 'I opened a dialog!');
const result = await browser.electron.dialog('showOpenDialog');
expect(result).toEqual('I opened a dialog!');
it('should mock an electron API function', async () => {
const showOpenDialog = await browser.electron.mock('dialog', 'showOpenDialog');
await browser.electron.execute(
async (electron) =>
await electron.dialog.showOpenDialog({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
}),
);

const mockedShowOpenDialog = await showOpenDialog.update();
expect(mockedShowOpenDialog).toHaveBeenCalledTimes(1);
expect(mockedShowOpenDialog).toHaveBeenCalledWith({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
});
});

it('should mock the expected synchronous electron API function', async () => {
await browser.electron.mock('dialog', 'showOpenDialogSync', 'I opened a dialog!');
const result = await browser.electron.dialog('showOpenDialogSync');
expect(result).toEqual('I opened a dialog!');
it('should mock a synchronous electron API function', async () => {
const showOpenDialogSync = await browser.electron.mock('dialog', 'showOpenDialogSync');
await browser.electron.execute((electron) =>
electron.dialog.showOpenDialogSync({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
}),
);

const mockedShowOpenDialogSync = await showOpenDialogSync.update();
expect(mockedShowOpenDialogSync).toHaveBeenCalledTimes(1);
expect(mockedShowOpenDialogSync).toHaveBeenCalledWith({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
});
});
});

describe('execute', () => {
it('should allow to execute an arbitrary function in the main process', async () => {
expect(
describe('mockImplementation', () => {
it('should mock an electron API function', async () => {
const showOpenDialog = await browser.electron.mock('dialog', 'showOpenDialog');
let callsCount = 0;
await showOpenDialog.mockImplementation(() => callsCount++);
await browser.electron.execute(
(electron, a, b, c) => {
const win = electron.BrowserWindow.getFocusedWindow();
return [typeof win, a + b + c];
},
1,
2,
3,
),
).toEqual(['object', 6]);

expect(await browser.electron.execute('return 1 + 2 + 3')).toBe(6);
async (electron) =>
await electron.dialog.showOpenDialog({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
}),
);

const mockedShowOpenDialog = await showOpenDialog.update();
expect(mockedShowOpenDialog).toHaveBeenCalledTimes(1);
expect(mockedShowOpenDialog).toHaveBeenCalledWith({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
});
expect(callsCount).toBe(1);
});
});

describe('mockReturnValue', () => {
it('should return the expected value from the mock API', async () => {
const mockGetName = await browser.electron.mock('app', 'getName');
await mockGetName.mockReturnValue('This is a mock');

const name = await browser.electron.execute((electron) => electron.app.getName());

expect(name).toBe('This is a mock');
});
});
});

describe('mockAll', () => {
it('should mock all functions on an API', async () => {
const mockedDialog = await browser.electron.mockAll('dialog');
await browser.electron.execute(
async (electron) =>
await electron.dialog.showOpenDialog({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
}),
);
await browser.electron.execute((electron) =>
electron.dialog.showOpenDialogSync({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
}),
);

const mockedShowOpenDialog = await mockedDialog.showOpenDialog.update();
expect(mockedShowOpenDialog).toHaveBeenCalledTimes(1);
expect(mockedShowOpenDialog).toHaveBeenCalledWith({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
});
const mockedShowOpenDialogSync = await mockedDialog.showOpenDialogSync.update();
expect(mockedShowOpenDialogSync).toHaveBeenCalledTimes(1);
expect(mockedShowOpenDialogSync).toHaveBeenCalledWith({
title: 'my dialog',
properties: ['openFile', 'openDirectory'],
});
});
});

describe('unMock', () => {
it('should remove an existing mock', async () => {
const getVersion = await browser.electron.mock('app', 'getVersion', 'mocked version');
let version = await browser.electron.execute((electron) => electron.app.getVersion());
expect(version).toBe('mocked version');

await getVersion.unMock();

expect(async () => await getVersion.update()).rejects.toThrowError(
'No mock registered for "electron.app.getVersion"',
);

version = await browser.electron.execute((electron) => electron.app.getVersion());
expect(version).toBe(appVersion);
});
});
});

describe('execute', () => {
it('should execute an arbitrary function in the main process', async () => {
expect(
await browser.electron.execute(
(electron, a, b, c) => {
const version = electron.app.getVersion();
return [version, a + b + c];
},
1,
2,
3,
),
).toEqual([appVersion, 6]);
});

it('should execute a string-based function in the main process', async () => {
expect(await browser.electron.execute('return 1 + 2 + 3')).toBe(6);
});
});
3 changes: 3 additions & 0 deletions example-cjs/e2e/application.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { expect } from '@wdio/globals';
import { browser } from 'wdio-electron-service';

describe('application loading', () => {
describe('App', () => {
it('should launch the application', async () => {
2 changes: 2 additions & 0 deletions example-cjs/e2e/dom.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { expect } from '@wdio/globals';
import { browser } from 'wdio-electron-service';
import { setupBrowser, type WebdriverIOQueries } from '@testing-library/webdriverio';

describe('application loading', () => {
2 changes: 2 additions & 0 deletions example-cjs/e2e/interaction.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { expect } from '@wdio/globals';
import { browser } from 'wdio-electron-service';
import { setupBrowser, type WebdriverIOQueries } from '@testing-library/webdriverio';

describe('application loading', () => {
28 changes: 14 additions & 14 deletions example-cjs/package.json
Original file line number Diff line number Diff line change
@@ -12,29 +12,29 @@
"test": "wdio run ./wdio.conf.ts"
},
"devDependencies": {
"@electron-forge/cli": "^6.4.2",
"@electron-forge/core": "^6.4.2",
"@electron-forge/core-utils": "^6.4.2",
"@electron-forge/maker-dmg": "^6.4.2",
"@electron-forge/maker-squirrel": "^6.4.2",
"@electron-forge/maker-zip": "^6.4.2",
"@electron-forge/shared-types": "^6.4.2",
"@electron-forge/cli": "^7.1.0",
"@electron-forge/core": "^7.1.0",
"@electron-forge/core-utils": "^7.1.0",
"@electron-forge/maker-dmg": "^7.1.0",
"@electron-forge/maker-squirrel": "^7.1.0",
"@electron-forge/maker-zip": "^7.1.0",
"@electron-forge/shared-types": "^7.1.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.1",
"@types/node": "^20.8.9",
"@wdio/cli": "^8.20.5",
"@wdio/globals": "^8.20.5",
"@wdio/local-runner": "^8.20.5",
"@types/node": "^20.9.3",
"@wdio/cli": "^8.23.3",
"@wdio/globals": "^8.23.3",
"@wdio/local-runner": "^8.23.3",
"@wdio/mocha-framework": "^8.20.3",
"electron": "^27.0.2",
"global-jsdom": "^9.1.0",
"jsdom": "^22.1.0",
"jsdom": "^23.0.0",
"rollup": "^4.1.5",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"typescript": "^5.3.2",
"wdio-electron-service": "file:../",
"webdriverio": "^8.20.4"
"webdriverio": "^8.23.3"
},
"peerDependencies": {
"typescript": "5.2.2"
Loading