Skip to content

Commit 2a0550f

Browse files
goosewobblerclaude
andauthored
test(tauri): improve command test coverage and fix quality issues (#215)
* test(tauri): improve command test coverage and fix quality issues Add tests for triggerDeeplink main function (embedded/CrabNebula/standard modes), mock browser context resolution, reset/restoreAllMocks error paths, and clearAllMocks regex edge cases. Fix type safety and strengthen weak assertions in existing tests. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): enhance core module tests with error paths and edge cases Cover driver management (crabnebula/official providers, auto-install, cargo detection), Edge driver lifecycle (version detection, download, mismatch), embedded provider (port resolution, start/stop, timeouts), plugin validator (warn flows), crabnebula backend (readline, health checks), and driver pool (error propagation, mixed states). Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): add service lifecycle and window management tests Cover before/beforeTest/beforeCommand/afterSession hooks in service.spec.ts including API attachment, mock clearing, window focus, multiremote skip, and session cleanup. Add window.spec.ts tests for findActiveWindow priority logic, switchToWindowByTitle, getCurrentDevtoolsPort, and clearWindowState. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri-plugin): add first unit tests for guest-js plugin module Add 42 tests covering CleanupRegistry, getConsoleForwardingCode, execute, setupConsoleForwarding, setupInvokeInterception, init, and waitForInit. Includes vitest config with jsdom environment and test script in package.json. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): remove platform-skipped tests in driverManager Replace skipIf(process.platform !== 'linux') tests with properly mocked equivalents that run on all platforms, eliminating the 2 skipped tests. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri-plugin): fix idempotency test to actually call init() twice The test claimed to verify double-init protection but only called init() once and manually set the guard flag. Now calls init() a second time to properly exercise the _wdioInvokeInterceptor guard path. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): remove misleading mockReset props from restoreAllMocks test The inner-callback for restoreAllMocks blanks or deletes keys from __wdio_mocks__ — it never calls mockReset on them. Replace the dead vi.fn() properties with plain objects to avoid misleading readers. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): fix retry test to actually exercise retry logic The test awaited the server start before calling waitTestRunnerBackendReady, so it always connected on the first attempt. Now reserves a port, starts waiting immediately, then brings the server up after 200ms to force at least one retry cycle. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): fix promisify mock to use correct callback-to-promise wrapper The identity mock (fn => fn) caused promisified exec calls to silently return undefined instead of a Promise, hiding real behavior. Aligned with the pattern in edgeDriverManager.spec.ts. Also added exec error mock for the WebKitWebDriver-not-found test to prevent detectPackageManager from hanging. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri-plugin): fix waitForInit test and add explicit init() variant Renamed existing test to clarify it verifies auto-init on module import. Added a second test that calls init() explicitly before waitForInit() to verify the stated behavior of waiting for initialization to complete. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): fix TOCTOU port race and fragile call-count mock crabnebulaBackend: Remove 200ms sleep between port release and re-bind to minimize the window where another process could steal the port. Re-bind immediately after starting waitTestRunnerBackendReady. driverPool: Replace callCount % 2 trick with explicit mockReturnValueOnce chain so the test doesn't break if the implementation changes how many times it calls isRunning() per driver. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): add missing browser assignment in beforeTest false-options test Without the browser, beforeTest early-returns before checking the mock options, so the not.toHaveBeenCalled assertions passed trivially regardless of the option values. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): mock node:net to eliminate real network I/O and port races Replace real TCP connections and net.createServer() with a mocked node:net module using programmable socket events. This makes the waitTestRunnerBackendReady and isTestRunnerBackendHealthy tests near-instantaneous, fully deterministic, and immune to TOCTOU port races on busy CI runners. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): fix timeout test deadlock with fake timers The previous version awaited the rejection assertion before advancing timers, causing a deadlock on CI. Now attaches the rejection handler synchronously, advances timers to trigger the timeout, then awaits the assertion. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): fix timeout test to avoid fake timer deadlock Use .catch() to capture the rejection before advancing timers, then assert on the captured error. This avoids the deadlock where awaiting expect().rejects blocks before timers fire, and prevents the ESLint valid-expect rule from re-adding the problematic await. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(tauri): verify restoreAllMocks/mockStore.clear execution order The test claimed to verify ordering but only checked both were called. Now tracks call sequence with an array and asserts the exact order. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent b0dc503 commit 2a0550f

16 files changed

Lines changed: 2744 additions & 76 deletions

packages/tauri-plugin/guest-js/__tests__/index.spec.ts

Lines changed: 594 additions & 0 deletions
Large diffs are not rendered by default.

packages/tauri-plugin/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"files": ["README.md", "dist-js", "LICENSE"],
1818
"scripts": {
1919
"build": "tsx scripts/build-guest-js.ts",
20-
"build:js": "tsx scripts/build-guest-js.ts"
20+
"build:js": "tsx scripts/build-guest-js.ts",
21+
"test": "vitest run"
2122
},
2223
"keywords": ["tauri", "webdriverio", "testing", "automation"],
2324
"author": "WebDriverIO Community",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
globals: true,
6+
environment: 'jsdom',
7+
include: ['guest-js/__tests__/**/*.spec.ts'],
8+
coverage: {
9+
provider: 'v8',
10+
reporter: ['text', 'json', 'html'],
11+
include: ['guest-js/**/*.ts'],
12+
exclude: ['**/*.d.ts', 'guest-js/__tests__/**'],
13+
},
14+
},
15+
});

packages/tauri-service/test/commands/clearAllMocks.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,23 @@ describe('clearAllMocks Command', () => {
6363
expect(mockedReadClipboard.mockClear).not.toHaveBeenCalled();
6464
expect(mockedWriteClipboard.mockClear).not.toHaveBeenCalled();
6565
});
66+
67+
it('should escape regex metacharacters in prefix so dot is literal', async () => {
68+
const mockedDotCommand = {
69+
getMockName: () => 'tauri.get_platform.info',
70+
mockClear: vi.fn(),
71+
};
72+
const mockedNonDotCommand = {
73+
getMockName: () => 'tauri.get_platformXinfo',
74+
mockClear: vi.fn(),
75+
};
76+
(mockStore.getMocks as Mock).mockReturnValue([
77+
['tauri.get_platform.info', mockedDotCommand],
78+
['tauri.get_platformXinfo', mockedNonDotCommand],
79+
]);
80+
81+
await clearAllMocks('get_platform.info');
82+
expect(mockedDotCommand.mockClear).toHaveBeenCalled();
83+
expect(mockedNonDotCommand.mockClear).not.toHaveBeenCalled();
84+
});
6685
});

packages/tauri-service/test/commands/mock.spec.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ vi.mock('../../src/mockStore.js', () => ({
1313
},
1414
}));
1515

16-
let mock: any;
16+
let mock: typeof import('../../src/commands/mock.js').mock;
1717

1818
beforeEach(async () => {
1919
mock = (await import('../../src/commands/mock.js')).mock;
2020
});
2121

2222
afterEach(() => {
2323
vi.resetModules();
24+
delete (globalThis as any).browser;
2425
});
2526

2627
describe('mock Command', () => {
@@ -68,5 +69,72 @@ describe('mock Command', () => {
6869
await mock('get_platform_info');
6970
expect(mockStore.setMock).toBeCalledWith(mockedCommand);
7071
});
72+
73+
it('should use this.browser when it has tauri capabilities', async () => {
74+
(createMock as Mock).mockReturnValue(mockedCommand);
75+
76+
const browserWithTauri = {
77+
isMultiremote: false,
78+
tauri: { execute: vi.fn() },
79+
} as unknown as WebdriverIO.Browser;
80+
81+
const context = { browser: browserWithTauri };
82+
await mock.call(context, 'get_platform_info');
83+
84+
expect(createMock).toHaveBeenCalledWith('get_platform_info', browserWithTauri);
85+
});
86+
87+
it('should fall back to globalThis.browser with tauri capabilities', async () => {
88+
(createMock as Mock).mockReturnValue(mockedCommand);
89+
90+
const globalBrowser = {
91+
isMultiremote: false,
92+
tauri: { execute: vi.fn() },
93+
} as unknown as WebdriverIO.Browser;
94+
globalThis.browser = globalBrowser as any;
95+
96+
await mock.call({}, 'get_platform_info');
97+
98+
expect(createMock).toHaveBeenCalledWith('get_platform_info', globalBrowser);
99+
});
100+
101+
it('should fall back to globalThis.browser without tauri when not multiremote', async () => {
102+
(createMock as Mock).mockReturnValue(mockedCommand);
103+
104+
const globalBrowser = {
105+
isMultiremote: false,
106+
} as unknown as WebdriverIO.Browser;
107+
globalThis.browser = globalBrowser as any;
108+
109+
await mock.call({}, 'get_platform_info');
110+
111+
expect(createMock).toHaveBeenCalledWith('get_platform_info', globalBrowser);
112+
});
113+
114+
it('should skip multiremote browser on this.browser and use globalThis.browser', async () => {
115+
(createMock as Mock).mockReturnValue(mockedCommand);
116+
117+
const multiremoteBrowser = {
118+
isMultiremote: true,
119+
tauri: { execute: vi.fn() },
120+
} as unknown as WebdriverIO.MultiRemoteBrowser;
121+
122+
const globalBrowser = {
123+
isMultiremote: false,
124+
tauri: { execute: vi.fn() },
125+
} as unknown as WebdriverIO.Browser;
126+
globalThis.browser = globalBrowser as any;
127+
128+
const context = { browser: multiremoteBrowser };
129+
await mock.call(context, 'get_platform_info');
130+
131+
expect(createMock).toHaveBeenCalledWith('get_platform_info', globalBrowser);
132+
});
133+
134+
it('should propagate errors from createMock', async () => {
135+
(createMock as Mock).mockRejectedValue(new Error('browser.execute failed'));
136+
137+
await expect(mock.call({}, 'get_platform_info')).rejects.toThrow('browser.execute failed');
138+
});
71139
});
72140
});

packages/tauri-service/test/commands/resetAllMocks.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,28 @@ describe('resetAllMocks Command', () => {
3838

3939
afterEach(() => {
4040
vi.resetAllMocks();
41+
delete (globalThis as any).browser;
4142
});
4243

4344
it('should reset all mocks in the injection script when no prefix is provided', async () => {
4445
await resetAllMocks();
4546
expect((globalThis.browser as any).execute).toHaveBeenCalledWith(expect.any(Function), undefined);
47+
48+
const callback = (globalThis.browser as any).execute.mock.calls[0][0];
49+
const mockResetFn = vi.fn();
50+
const mockWindow = {
51+
__wdio_mocks__: {
52+
get_platform_info: { mockReset: mockResetFn },
53+
read_clipboard: { mockReset: mockResetFn },
54+
},
55+
};
56+
(globalThis as any).window = mockWindow;
57+
try {
58+
callback(undefined);
59+
expect(mockResetFn).toHaveBeenCalledTimes(2);
60+
} finally {
61+
delete (globalThis as any).window;
62+
}
4663
});
4764

4865
it('should reset all outer mock functions when no prefix is provided', async () => {
@@ -62,6 +79,24 @@ describe('resetAllMocks Command', () => {
6279
it('should pass prefix to browser execute for inner mock filtering', async () => {
6380
await resetAllMocks('write');
6481
expect((globalThis.browser as any).execute).toHaveBeenCalledWith(expect.any(Function), 'write');
82+
83+
const callback = (globalThis.browser as any).execute.mock.calls[0][0];
84+
const resetPlatform = vi.fn();
85+
const resetWrite = vi.fn();
86+
const mockWindow = {
87+
__wdio_mocks__: {
88+
get_platform_info: { mockReset: resetPlatform },
89+
write_clipboard: { mockReset: resetWrite },
90+
},
91+
};
92+
(globalThis as any).window = mockWindow;
93+
try {
94+
callback('write');
95+
expect(resetPlatform).not.toHaveBeenCalled();
96+
expect(resetWrite).toHaveBeenCalledTimes(1);
97+
} finally {
98+
delete (globalThis as any).window;
99+
}
65100
});
66101

67102
it('should handle prefix that matches no mocks', async () => {
@@ -70,4 +105,19 @@ describe('resetAllMocks Command', () => {
70105
expect(mockedReadClipboard.mockReset).not.toHaveBeenCalled();
71106
expect(mockedWriteClipboard.mockReset).not.toHaveBeenCalled();
72107
});
108+
109+
it('should throw when no browser context is available', async () => {
110+
delete (globalThis as any).browser;
111+
await expect(resetAllMocks.call({})).rejects.toThrow('resetAllMocks requires a valid browser context');
112+
});
113+
114+
it('should throw when browser is multiremote', async () => {
115+
const multiremoteBrowser = {
116+
isMultiremote: true,
117+
execute: vi.fn(),
118+
} as unknown as WebdriverIO.MultiRemoteBrowser;
119+
globalThis.browser = multiremoteBrowser as any;
120+
121+
await expect(resetAllMocks.call({})).rejects.toThrow('resetAllMocks requires a valid browser context');
122+
});
73123
});

packages/tauri-service/test/commands/restoreAllMocks.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,27 @@ describe('restoreAllMocks Command', () => {
3838

3939
afterEach(() => {
4040
vi.resetAllMocks();
41+
delete (globalThis as any).browser;
4142
});
4243

4344
it('should restore all mocks in the injection script when no prefix is provided', async () => {
4445
await restoreAllMocks();
4546
expect((globalThis.browser as any).execute).toHaveBeenCalledWith(expect.any(Function), undefined);
47+
48+
const callback = (globalThis.browser as any).execute.mock.calls[0][0];
49+
const mockWindow = {
50+
__wdio_mocks__: {
51+
get_platform_info: { implementation: () => {} },
52+
read_clipboard: { implementation: () => {} },
53+
},
54+
} as any;
55+
(globalThis as any).window = mockWindow;
56+
try {
57+
callback(undefined);
58+
expect(mockWindow.__wdio_mocks__).toEqual({});
59+
} finally {
60+
delete (globalThis as any).window;
61+
}
4662
});
4763

4864
it('should restore all outer mock functions when no prefix is provided', async () => {
@@ -62,6 +78,22 @@ describe('restoreAllMocks Command', () => {
6278
it('should pass prefix to browser execute for inner mock filtering', async () => {
6379
await restoreAllMocks('write');
6480
expect((globalThis.browser as any).execute).toHaveBeenCalledWith(expect.any(Function), 'write');
81+
82+
const callback = (globalThis.browser as any).execute.mock.calls[0][0];
83+
const mockWindow = {
84+
__wdio_mocks__: {
85+
get_platform_info: { implementation: () => {} },
86+
write_clipboard: { implementation: () => {} },
87+
},
88+
} as any;
89+
(globalThis as any).window = mockWindow;
90+
try {
91+
callback('write');
92+
expect(mockWindow.__wdio_mocks__.get_platform_info).toBeDefined();
93+
expect(mockWindow.__wdio_mocks__.write_clipboard).toBeUndefined();
94+
} finally {
95+
delete (globalThis as any).window;
96+
}
6597
});
6698

6799
it('should handle prefix that matches no mocks', async () => {
@@ -70,4 +102,19 @@ describe('restoreAllMocks Command', () => {
70102
expect(mockedReadClipboard.mockRestore).not.toHaveBeenCalled();
71103
expect(mockedWriteClipboard.mockRestore).not.toHaveBeenCalled();
72104
});
105+
106+
it('should throw when no browser context is available', async () => {
107+
delete (globalThis as any).browser;
108+
await expect(restoreAllMocks.call({})).rejects.toThrow('restoreAllMocks requires a valid browser context');
109+
});
110+
111+
it('should throw when browser is multiremote', async () => {
112+
const multiremoteBrowser = {
113+
isMultiremote: true,
114+
execute: vi.fn(),
115+
} as unknown as WebdriverIO.MultiRemoteBrowser;
116+
globalThis.browser = multiremoteBrowser as any;
117+
118+
await expect(restoreAllMocks.call({})).rejects.toThrow('restoreAllMocks requires a valid browser context');
119+
});
73120
});

0 commit comments

Comments
 (0)