-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathservice.ts
More file actions
380 lines (335 loc) · 14.2 KB
/
service.ts
File metadata and controls
380 lines (335 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
import type { TauriAPIs, TauriServiceAPI } from '@wdio/native-types';
import { createLogger, waitUntilWindowAvailable } from '@wdio/native-utils';
import { execute } from './commands/execute.js';
import { clearAllMocks, isMockFunction, mock, resetAllMocks, restoreAllMocks } from './commands/mock.js';
import { triggerDeeplink } from './commands/triggerDeeplink.js';
import mockStore from './mockStore.js';
import { CONSOLE_WRAPPER_SCRIPT } from './scripts/console-wrapper.js';
import type { TauriCapabilities, TauriServiceGlobalOptions, TauriServiceOptions } from './types.js';
import { clearWindowState, ensureActiveWindowFocus } from './window.js';
const log = createLogger('tauri-service', 'service');
const EXECUTE_PATCHED = Symbol('wdio-tauri-execute-patched');
/**
* Tauri worker service
*/
type ElementCommands = 'click' | 'doubleClick' | 'setValue' | 'clearValue';
export default class TauriWorkerService {
private browser?: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser;
private clearMocks: boolean;
private clearMocksPrefix?: string;
private resetMocks: boolean;
private resetMocksPrefix?: string;
private restoreMocks: boolean;
private restoreMocksPrefix?: string;
private driverProvider?: 'official' | 'crabnebula' | 'embedded';
constructor(options: TauriServiceOptions & TauriServiceGlobalOptions, _capabilities: TauriCapabilities) {
this.clearMocks = options.clearMocks ?? false;
this.clearMocksPrefix = options.clearMocksPrefix;
this.resetMocks = options.resetMocks ?? false;
this.resetMocksPrefix = options.resetMocksPrefix;
this.restoreMocks = options.restoreMocks ?? false;
this.restoreMocksPrefix = options.restoreMocksPrefix;
this.driverProvider = options.driverProvider;
log.debug('TauriWorkerService initialized');
}
/**
* Initialize the service
*/
async before(
_capabilities: TauriCapabilities,
_specs: string[],
browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser,
): Promise<void> {
log.debug('Initializing Tauri worker service');
this.browser = browser;
if (browser.isMultiremote) {
const mrBrowser = browser as WebdriverIO.MultiRemoteBrowser;
log.info(`Initializing ${mrBrowser.instances.length} multiremote instances`);
// Add Tauri API to the root multiremote object first
this.addTauriApi(browser as unknown as WebdriverIO.Browser);
// Add Tauri API to each instance and wait for readiness
for (const instanceName of mrBrowser.instances) {
const mrInstance = mrBrowser.getInstance(instanceName);
log.debug(`Initializing instance: ${instanceName}`);
this.addTauriApi(mrInstance);
this.patchBrowserExecute(mrInstance);
await waitUntilWindowAvailable(mrInstance);
log.debug(`Instance ${instanceName} ready`);
// Wait for plugin initialization on this instance
// Skip for CrabNebula - browser.execute() not supported
if (this.driverProvider !== 'crabnebula') {
log.debug(`Waiting for Tauri plugin initialization on ${instanceName}...`);
try {
await mrInstance.execute(async function checkMultiremotePluginInit() {
// @ts-expect-error - window exists in browser context
if (typeof window.wdioTauri !== 'undefined' && typeof window.wdioTauri.waitForInit === 'function') {
// @ts-expect-error - window exists in browser context
await window.wdioTauri.waitForInit();
return true;
}
return false;
});
log.debug(`Tauri plugin initialization complete for ${instanceName}`);
} catch (error) {
log.warn(`Failed to wait for plugin initialization on ${instanceName}:`, error);
}
}
}
} else {
log.debug('Initializing standard browser');
this.addTauriApi(browser as WebdriverIO.Browser);
this.patchBrowserExecute(browser as WebdriverIO.Browser);
await waitUntilWindowAvailable(browser as WebdriverIO.Browser);
log.debug('Standard browser ready');
}
// Wait for the plugin to fully initialize (specifically attachConsole())
// This ensures frontend console logs will be captured
// Skip for CrabNebula - browser.execute() not supported
if (this.driverProvider !== 'crabnebula') {
log.debug('Waiting for Tauri plugin initialization...');
try {
await (browser as WebdriverIO.Browser).execute(async function checkPluginInit() {
// @ts-expect-error - window exists in browser context
if (typeof window.wdioTauri !== 'undefined' && typeof window.wdioTauri.waitForInit === 'function') {
// @ts-expect-error - window exists in browser context
await window.wdioTauri.waitForInit();
}
});
log.debug('Tauri plugin initialization complete');
} catch (error) {
log.error('Failed to wait for plugin initialization — tauri.execute() and mocking may not work:', error);
}
}
// Frontend log capture is handled automatically by the @wdio/tauri-plugin
// The plugin calls attachConsole() during initialization to forward console logs
// to the Tauri log plugin, which outputs to stdout for capture by the launcher
// Install command overrides to trigger mock updates after DOM interactions
this.installCommandOverrides();
}
async beforeTest(_test: unknown, _context: unknown): Promise<void> {
if (this.clearMocks) {
await clearAllMocks.call({ browser: this.browser }, this.clearMocksPrefix);
}
if (this.resetMocks) {
await resetAllMocks.call({ browser: this.browser }, this.resetMocksPrefix);
}
if (this.restoreMocks) {
await restoreAllMocks.call({ browser: this.browser }, this.restoreMocksPrefix);
}
}
async beforeCommand(commandName: string, _args: unknown[]): Promise<void> {
if (!this.browser || this.browser.isMultiremote) {
return;
}
const browser = this.browser as WebdriverIO.Browser;
try {
// Generic window focus detection like Electron - no app-specific knowledge
await ensureActiveWindowFocus(browser, commandName);
} catch (error) {
log.warn('Failed to ensure window focus before command:', error);
}
}
async afterTest(_test: unknown, _context: unknown, _results: unknown): Promise<void> {
// Post-test logic if needed
}
async after(_results: unknown, _capabilities: TauriCapabilities, _specs: string[]): Promise<void> {
// Cleanup if needed
}
/**
* Clean up session after tests complete
* This is critical for retry functionality - without explicit session deletion,
* retries fail with "invalid session id" errors
*/
async afterSession(_config: unknown, _capabilities: TauriCapabilities, _specs: string[]): Promise<void> {
log.debug('Cleaning up session...');
// Restore and clear mocks to prevent memory leaks
try {
if (this.browser) {
await restoreAllMocks.call({ browser: this.browser });
}
mockStore.clear();
log.debug('Mock store cleared');
} catch (error) {
log.warn('Failed to clear mock store:', error);
}
if (!this.browser) {
log.warn('No browser instance available for session cleanup');
clearWindowState();
return;
}
try {
// Delete WebDriver session explicitly for clean retry handling
if (!this.browser.isMultiremote) {
const stdBrowser = this.browser as WebdriverIO.Browser;
clearWindowState(stdBrowser.sessionId);
if (stdBrowser.sessionId) {
log.debug(`Deleting session: ${stdBrowser.sessionId}`);
await stdBrowser.deleteSession();
log.debug('Session deleted successfully');
}
} else {
// Handle multiremote cleanup
const mrBrowser = this.browser as WebdriverIO.MultiRemoteBrowser;
const sessionIds: (string | undefined)[] = [];
for (const instanceName of mrBrowser.instances) {
try {
const instance = mrBrowser.getInstance(instanceName);
sessionIds.push(instance.sessionId);
if (instance.sessionId) {
log.debug(`Deleting session for instance ${instanceName}: ${instance.sessionId}`);
await instance.deleteSession();
log.debug(`Session deleted for instance ${instanceName}`);
}
} catch (error) {
log.warn(`Failed to delete session for instance ${instanceName}:`, error);
}
}
// Clear all session IDs from cache
for (const sid of sessionIds) {
clearWindowState(sid);
}
}
} catch (error) {
log.warn('Failed to delete session:', error);
// Don't throw - allow cleanup to continue
}
}
/**
* Add Tauri API to browser object
* Matches the Electron service API surface exactly
*/
private addTauriApi(browser: WebdriverIO.Browser): void {
(browser as WebdriverIO.Browser & { tauri: TauriServiceAPI }).tauri = this.getTauriAPI(browser);
}
/**
* Get Tauri API object for a browser instance
* Handles both standard and multiremote browsers
*/
private getTauriAPI(browser: WebdriverIO.Browser): TauriServiceAPI {
return {
execute: <ReturnValue, InnerArguments extends unknown[]>(
script: string | ((tauri: TauriAPIs, ...innerArgs: InnerArguments) => ReturnValue),
...args: InnerArguments
): Promise<ReturnValue> => {
return execute<ReturnValue, InnerArguments>(browser, script, ...args);
},
clearAllMocks: async (commandPrefix?: string): Promise<void> => {
return clearAllMocks.call({ browser }, commandPrefix);
},
isMockFunction: (fn: unknown) => {
return isMockFunction(fn);
},
mock: async (command: string) => {
return mock.call({ browser }, command);
},
resetAllMocks: async (commandPrefix?: string): Promise<void> => {
return resetAllMocks.call({ browser }, commandPrefix);
},
restoreAllMocks: async (commandPrefix?: string): Promise<void> => {
return restoreAllMocks.call({ browser }, commandPrefix);
},
triggerDeeplink: async (url: string): Promise<void> => {
return triggerDeeplink.call({ browser }, url);
},
};
}
/**
* Install command overrides to trigger mock updates after DOM interactions
*/
private installCommandOverrides() {
const commandsToOverride: ElementCommands[] = ['click', 'doubleClick', 'setValue', 'clearValue'];
commandsToOverride.forEach((commandName) => {
this.overrideElementCommand(commandName);
});
}
/**
* Override an element-level command to add mock update after execution
*/
private overrideElementCommand(commandName: ElementCommands) {
const browser = this.browser as WebdriverIO.Browser;
try {
const testOverride = async function (
this: WebdriverIO.Element,
originalCommand: (...args: readonly unknown[]) => Promise<unknown>,
...args: readonly unknown[]
): Promise<unknown> {
const result = await Reflect.apply(originalCommand, this, args as unknown[]);
await updateAllMocks();
return result;
} as Parameters<typeof browser.overwriteCommand>[1];
browser.overwriteCommand(commandName, testOverride, true);
} catch (error) {
log.warn(`Failed to override element command '${commandName}':`, error);
}
}
/**
* Patch browser.execute() to automatically inject console forwarding code
* This ensures console logs from browser.execute() contexts are captured
*/
private patchBrowserExecute(browser: WebdriverIO.Browser): void {
interface PatchedBrowser extends WebdriverIO.Browser {
[EXECUTE_PATCHED]?: boolean;
}
const patchedBrowser = browser as PatchedBrowser;
if (patchedBrowser[EXECUTE_PATCHED]) {
log.debug('browser.execute already patched, skipping');
return;
}
const originalExecute = browser.execute.bind(browser);
const isEmbedded = this.driverProvider === 'embedded';
const patchedExecute = async function patchedExecute<ReturnValue, InnerArguments extends unknown[]>(
script: string | ((...args: InnerArguments) => ReturnValue),
...args: InnerArguments
): Promise<ReturnValue> {
// For functions: use .toString() - produces valid JS function source
// For strings: pass as-is (wrapper template wraps in parentheses to make callable)
const scriptString = typeof script === 'function' ? script.toString() : script;
if (isEmbedded) {
// For embedded WebDriver: skip console wrapper as console forwarding
// is handled by tauri-plugin-webdriver.
return originalExecute(scriptString, ...args) as Promise<ReturnValue>;
}
// For tauri-driver: use sync execute with console wrapper
// Note: scriptString is passed as-is - wrap in IIFE to make both strings and functions callable
// For string scripts: "return x" -> "(() => return x)()" - wraps statement as expression
// For function scripts: "(a,b) => a+b" -> "((a,b) => a+b)()" - works as IIFE
const wrappedScript = `
${CONSOLE_WRAPPER_SCRIPT}
return ((${scriptString})()).apply(null, arguments);
`;
return originalExecute(wrappedScript, ...args) as Promise<ReturnValue>;
};
Object.defineProperty(browser, 'execute', {
value: patchedExecute,
writable: true,
configurable: true,
});
patchedBrowser[EXECUTE_PATCHED] = true;
log.debug('browser.execute() patched with console forwarding');
}
}
/**
* Update all existing mocks by syncing inner (browser) mock state to outer (test) mocks
*/
async function updateAllMocks() {
log.debug('updateAllMocks called');
const mocks = mockStore.getMocks();
log.debug(`Found ${mocks.length} mocks to update`);
if (mocks.length === 0) {
log.debug('No mocks to update, returning');
return;
}
try {
log.debug('Starting mock update batch');
await Promise.all(
mocks.map(async ([mockId, mockInstance]) => {
log.debug(`Updating mock: ${mockId}`);
await mockInstance.update();
log.debug(`Mock update completed: ${mockId}`);
}),
);
log.debug('All mock updates completed successfully');
} catch (error) {
log.warn('Mock update batch failed:', error);
}
}