@wdio/nightwatch-devtools 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +65 -1
  2. package/dist/bidi-OH4ZFDN5.js +215 -0
  3. package/dist/bidi-OH4ZFDN5.js.map +1 -0
  4. package/dist/chunk-DKSBYMPM.js +191 -0
  5. package/dist/chunk-DKSBYMPM.js.map +1 -0
  6. package/dist/index.d.ts +106 -16
  7. package/dist/index.js +3875 -760
  8. package/dist/index.js.map +1 -1
  9. package/package.json +18 -13
  10. package/dist/constants.d.ts +0 -55
  11. package/dist/constants.js +0 -95
  12. package/dist/constants.js.map +0 -1
  13. package/dist/helpers/browserProxy.d.ts +0 -46
  14. package/dist/helpers/browserProxy.js +0 -314
  15. package/dist/helpers/browserProxy.js.map +0 -1
  16. package/dist/helpers/capturePerformance.d.ts +0 -5
  17. package/dist/helpers/capturePerformance.js +0 -53
  18. package/dist/helpers/capturePerformance.js.map +0 -1
  19. package/dist/helpers/cucumberHooks.cjs +0 -54
  20. package/dist/helpers/cucumberHooks.cjs.map +0 -1
  21. package/dist/helpers/cucumberHooks.d.cts +0 -21
  22. package/dist/helpers/suiteManager.d.ts +0 -43
  23. package/dist/helpers/suiteManager.js +0 -143
  24. package/dist/helpers/suiteManager.js.map +0 -1
  25. package/dist/helpers/testManager.d.ts +0 -45
  26. package/dist/helpers/testManager.js +0 -143
  27. package/dist/helpers/testManager.js.map +0 -1
  28. package/dist/helpers/utils.d.ts +0 -83
  29. package/dist/helpers/utils.js +0 -447
  30. package/dist/helpers/utils.js.map +0 -1
  31. package/dist/reporter.d.ts +0 -44
  32. package/dist/reporter.js +0 -185
  33. package/dist/reporter.js.map +0 -1
  34. package/dist/session.d.ts +0 -78
  35. package/dist/session.js +0 -643
  36. package/dist/session.js.map +0 -1
  37. package/dist/types.d.ts +0 -190
  38. package/dist/types.js +0 -5
  39. package/dist/types.js.map +0 -1
package/dist/session.js DELETED
@@ -1,643 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import http from 'node:http';
3
- import path from 'node:path';
4
- import { createRequire } from 'node:module';
5
- import logger from '@wdio/logger';
6
- import { WebSocket } from 'ws';
7
- import { CONSOLE_METHODS, LOG_SOURCES, NAVIGATION_COMMANDS, SPINNER_RE } from './constants.js';
8
- import { stripAnsiCodes, detectLogLevel, createConsoleLogEntry, chromeLogLevelToLogLevel, getRequestType } from './helpers/utils.js';
9
- import { CAPTURE_PERFORMANCE_SCRIPT } from './helpers/capturePerformance.js';
10
- const require = createRequire(import.meta.url);
11
- const log = logger('@wdio/nightwatch-devtools:SessionCapturer');
12
- export class SessionCapturer {
13
- #ws;
14
- #originalConsoleMethods;
15
- #originalProcessMethods;
16
- #isCapturingConsole = false;
17
- #browser;
18
- #commandCounter = 0;
19
- #sentCommandIds = new Set();
20
- commandsLog = [];
21
- sources = new Map();
22
- consoleLogs = [];
23
- mutations = [];
24
- traceLogs = [];
25
- networkRequests = [];
26
- metadata;
27
- constructor(devtoolsOptions = {}, browser) {
28
- const { port, hostname } = devtoolsOptions;
29
- this.#browser = browser;
30
- if (hostname && port) {
31
- this.#ws = new WebSocket(`ws://${hostname}:${port}/worker`);
32
- this.#ws.on('open', () => {
33
- this.#hasConnected = true;
34
- log.info('✓ Worker WebSocket connected to backend');
35
- });
36
- this.#ws.on('error', (err) => log.error(`Couldn't connect to devtools backend: ${err.message}`));
37
- this.#ws.on('close', () => {
38
- log.info('Worker WebSocket disconnected');
39
- });
40
- }
41
- this.#originalConsoleMethods = {
42
- log: console.log,
43
- info: console.info,
44
- warn: console.warn,
45
- error: console.error
46
- };
47
- this.#originalProcessMethods = {
48
- stdoutWrite: process.stdout.write.bind(process.stdout),
49
- stderrWrite: process.stderr.write.bind(process.stderr)
50
- };
51
- this.#patchConsole();
52
- this.#interceptProcessStreams();
53
- }
54
- #patchConsole() {
55
- CONSOLE_METHODS.forEach((method) => {
56
- const originalMethod = this.#originalConsoleMethods[method];
57
- console[method] = (...consoleArgs) => {
58
- this.#isCapturingConsole = true;
59
- const result = originalMethod.apply(console, consoleArgs);
60
- this.#isCapturingConsole = false;
61
- // Capture all console output; strip ANSI codes for clean display in UI
62
- const rawText = consoleArgs
63
- .map((a) => typeof a === 'object' && a !== null ? JSON.stringify(a) : String(a))
64
- .join(' ');
65
- const cleanText = stripAnsiCodes(rawText).trim();
66
- if (!cleanText) {
67
- return result;
68
- }
69
- const logEntry = createConsoleLogEntry(method, [cleanText], LOG_SOURCES.TEST);
70
- this.consoleLogs.push(logEntry);
71
- this.sendUpstream('consoleLogs', [logEntry]);
72
- return result;
73
- };
74
- });
75
- }
76
- #isInternalStreamLine(line) {
77
- const t = line.trim();
78
- return (t.startsWith('{"') ||
79
- t.includes('@wdio/devtools-backend') ||
80
- t.startsWith('[SESSION]'));
81
- }
82
- #hasConnected = false;
83
- #isCapturingStream = false;
84
- #interceptProcessStreams() {
85
- const captureTerminalOutput = (outputData) => {
86
- if (this.#isCapturingStream) {
87
- return;
88
- }
89
- const outputText = typeof outputData === 'string' ? outputData : outputData.toString();
90
- if (!outputText?.trim()) {
91
- return;
92
- }
93
- this.#isCapturingStream = true;
94
- try {
95
- const linesToCapture = [];
96
- for (const rawLine of outputText.split('\n')) {
97
- const segments = rawLine.split('\r').filter((s) => s.trim());
98
- const lastSegment = segments[segments.length - 1] ?? rawLine;
99
- const clean = stripAnsiCodes(lastSegment).trim();
100
- if (!clean ||
101
- this.#isInternalStreamLine(clean) ||
102
- SPINNER_RE.test(clean)) {
103
- continue;
104
- }
105
- linesToCapture.push(clean);
106
- }
107
- for (const clean of linesToCapture) {
108
- const logEntry = createConsoleLogEntry(detectLogLevel(clean), [clean], LOG_SOURCES.TERMINAL);
109
- this.consoleLogs.push(logEntry);
110
- this.sendUpstream('consoleLogs', [logEntry]);
111
- }
112
- }
113
- finally {
114
- this.#isCapturingStream = false;
115
- }
116
- };
117
- const interceptStreamWrite = (stream, originalWriteMethod) => {
118
- const capturer = this;
119
- stream.write = function (chunk, ...additionalArgs) {
120
- const writeResult = originalWriteMethod.call(stream, chunk, ...additionalArgs);
121
- if (chunk && !capturer.#isCapturingConsole) {
122
- captureTerminalOutput(chunk);
123
- }
124
- return writeResult;
125
- };
126
- };
127
- interceptStreamWrite(process.stdout, this.#originalProcessMethods.stdoutWrite);
128
- interceptStreamWrite(process.stderr, this.#originalProcessMethods.stderrWrite);
129
- }
130
- #restoreConsole() {
131
- CONSOLE_METHODS.forEach((method) => {
132
- console[method] = this.#originalConsoleMethods[method];
133
- });
134
- }
135
- #restoreProcessStreams() {
136
- process.stdout.write = this.#originalProcessMethods.stdoutWrite;
137
- process.stderr.write = this.#originalProcessMethods.stderrWrite;
138
- }
139
- cleanup() {
140
- this.#restoreConsole();
141
- this.#restoreProcessStreams();
142
- }
143
- get isReportingUpstream() {
144
- return Boolean(this.#ws) && this.#ws?.readyState === WebSocket.OPEN;
145
- }
146
- /**
147
- * Wait for WebSocket to connect
148
- */
149
- async waitForConnection(timeoutMs = 5000) {
150
- if (!this.#ws) {
151
- return false;
152
- }
153
- if (this.#ws.readyState === WebSocket.OPEN) {
154
- return true;
155
- }
156
- return new Promise((resolve) => {
157
- const timeout = setTimeout(() => {
158
- log.warn(`WebSocket connection timeout after ${timeoutMs}ms`);
159
- resolve(false);
160
- }, timeoutMs);
161
- this.#ws.once('open', () => {
162
- clearTimeout(timeout);
163
- resolve(true);
164
- });
165
- this.#ws.once('error', () => {
166
- clearTimeout(timeout);
167
- resolve(false);
168
- });
169
- });
170
- }
171
- #serializeError(error) {
172
- return error
173
- ? { name: error.name, message: error.message, stack: error.stack }
174
- : undefined;
175
- }
176
- async captureCommand(command, args, result, error, testUid, callSource, timestamp) {
177
- // Serialize error properly (Error objects don't JSON.stringify well)
178
- const serializedError = this.#serializeError(error);
179
- const commandId = this.#commandCounter++;
180
- const commandLogEntry = {
181
- _id: commandId,
182
- command,
183
- args,
184
- result,
185
- error: serializedError,
186
- timestamp: timestamp || Date.now(),
187
- callSource,
188
- testUid
189
- };
190
- this.commandsLog.push(commandLogEntry);
191
- const isNavigationCommand = NAVIGATION_COMMANDS.some((cmd) => command.toLowerCase().includes(cmd.toLowerCase()));
192
- if (isNavigationCommand && this.#browser && !error) {
193
- this.#capturePerformanceData(commandLogEntry, args).catch((err) => {
194
- log.warn(`Failed to capture performance data: ${err.message}`);
195
- });
196
- }
197
- return true;
198
- }
199
- async #capturePerformanceData(commandLogEntry, args) {
200
- await new Promise((resolve) => setTimeout(resolve, 500));
201
- const performanceData = await this.#browser.execute(CAPTURE_PERFORMANCE_SCRIPT);
202
- let data;
203
- if (performanceData && typeof performanceData === 'object') {
204
- data =
205
- 'value' in performanceData
206
- ? performanceData.value
207
- : performanceData;
208
- }
209
- if (data && data.navigation) {
210
- commandLogEntry.performance = {
211
- navigation: data.navigation,
212
- resources: data.resources
213
- };
214
- commandLogEntry.cookies = data.cookies;
215
- commandLogEntry.documentInfo = data.documentInfo;
216
- commandLogEntry.result = {
217
- url: args[0],
218
- loadTime: data.navigation?.timing?.loadTime,
219
- resources: data.resources,
220
- resourceCount: data.resources?.length,
221
- cookies: data.cookies,
222
- title: data.documentInfo?.title
223
- };
224
- }
225
- }
226
- /** Send a command to the UI (only if not already sent) */
227
- sendCommand(command) {
228
- if (command._id !== undefined && !this.#sentCommandIds.has(command._id)) {
229
- this.#sentCommandIds.add(command._id);
230
- // Remove internal ID before sending
231
- const commandToSend = { ...command };
232
- delete commandToSend._id;
233
- this.sendUpstream('commands', [commandToSend]);
234
- }
235
- }
236
- /**
237
- * Replace an already-captured command entry (used for retried commands so
238
- * only the final execution result is shown in the UI).
239
- * Removes the old entry from commandsLog, revokes its sent-status so the
240
- * replacement can be sent, and returns the new entry together with the
241
- * old entry's timestamp (so the UI can locate and replace it in-place).
242
- */
243
- replaceCommand(oldId, command, args, result, error, testUid, callSource, timestamp) {
244
- // Remove the superseded entry and capture its timestamp for the UI
245
- const idx = this.commandsLog.findIndex((c) => c._id === oldId);
246
- const oldTimestamp = idx !== -1 ? (this.commandsLog[idx].timestamp ?? 0) : 0;
247
- if (idx !== -1) {
248
- this.commandsLog.splice(idx, 1);
249
- }
250
- // Allow the slot to be re-used by a new entry
251
- this.#sentCommandIds.delete(oldId);
252
- const serializedError = this.#serializeError(error);
253
- const commandId = this.#commandCounter++;
254
- const entry = {
255
- _id: commandId,
256
- command,
257
- args,
258
- result,
259
- error: serializedError,
260
- timestamp: timestamp || Date.now(),
261
- callSource,
262
- testUid
263
- };
264
- this.commandsLog.push(entry);
265
- return { entry, oldTimestamp };
266
- }
267
- /** Send a replace-command event to the UI (swaps old entry in-place) */
268
- sendReplaceCommand(oldTimestamp, command) {
269
- const commandToSend = { ...command };
270
- delete commandToSend._id;
271
- this.sendUpstream('replaceCommand', {
272
- oldTimestamp,
273
- command: commandToSend
274
- });
275
- }
276
- /**
277
- * Take a screenshot by calling the WebDriver HTTP endpoint directly.
278
- * This completely bypasses Nightwatch's command queue so there is no risk
279
- * of the request being appended after `end()` / `quit()`.
280
- */
281
- takeScreenshotViaHttp(browser) {
282
- const browserAny = browser;
283
- const sessionId = browserAny.sessionId;
284
- if (!sessionId) {
285
- return Promise.resolve(null);
286
- }
287
- const pick = (obj, ...keys) => {
288
- if (!obj || typeof obj !== 'object') {
289
- return undefined;
290
- }
291
- for (const k of keys) {
292
- const val = obj[k];
293
- if (val !== undefined && val !== null) {
294
- return val;
295
- }
296
- }
297
- return undefined;
298
- };
299
- const transportSettings = browserAny.transport?.settings?.webdriver ||
300
- browserAny.queue?.transport?.settings?.webdriver ||
301
- browserAny.nightwatchInstance?.transport?.settings?.webdriver ||
302
- {};
303
- const opts = browserAny.options || {};
304
- const nightwatchSettings = browserAny.nightwatchInstance?.settings ||
305
- browserAny.globals?.nightwatchInstance?.settings ||
306
- {};
307
- const driverHost = pick(transportSettings, 'host', 'server_address') ||
308
- pick(opts.webdriver, 'host') ||
309
- pick(nightwatchSettings.webdriver, 'host') ||
310
- 'localhost';
311
- const driverPort = pick(transportSettings, 'port') ||
312
- pick(opts.webdriver, 'port') ||
313
- pick(nightwatchSettings.webdriver, 'port') ||
314
- 9515;
315
- const endpoint = `http://${driverHost}:${driverPort}/session/${sessionId}/screenshot`;
316
- return new Promise((resolve) => {
317
- const req = http.get(endpoint, (res) => {
318
- let body = '';
319
- res.on('data', (chunk) => {
320
- body += chunk;
321
- });
322
- res.on('end', () => {
323
- try {
324
- const value = JSON.parse(body).value || null;
325
- if (!value) {
326
- log.warn(`[screenshot] Empty response from ${endpoint}`);
327
- }
328
- resolve(value);
329
- }
330
- catch {
331
- log.warn(`[screenshot] Failed to parse response from ${endpoint}`);
332
- resolve(null);
333
- }
334
- });
335
- });
336
- req.on('error', (err) => {
337
- log.warn(`[screenshot] HTTP request failed (${endpoint}): ${err.message}`);
338
- resolve(null);
339
- });
340
- req.setTimeout(5000, () => {
341
- log.warn(`[screenshot] Request timed out (${endpoint})`);
342
- req.destroy();
343
- resolve(null);
344
- });
345
- });
346
- }
347
- /** Capture test source code */
348
- async captureSource(filePath) {
349
- if (!this.sources.has(filePath)) {
350
- try {
351
- const sourceCode = await fs.readFile(filePath, 'utf-8');
352
- this.sources.set(filePath, sourceCode.toString());
353
- this.sendUpstream('sources', { [filePath]: sourceCode.toString() });
354
- }
355
- catch (err) {
356
- log.warn(`Failed to read source file ${filePath}: ${err.message}`);
357
- }
358
- }
359
- }
360
- /** Send data upstream to backend */
361
- sendUpstream(event, data) {
362
- if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) {
363
- if (this.#hasConnected) {
364
- log.warn(`[upstream] WebSocket not open — dropping "${event}" event`);
365
- }
366
- return;
367
- }
368
- try {
369
- this.#ws.send(JSON.stringify({ scope: event, data }));
370
- }
371
- catch (err) {
372
- log.warn(`[upstream] Failed to send "${event}": ${err.message}`);
373
- }
374
- }
375
- /** Returns true when the WebSocket is open. */
376
- isConnected() {
377
- return this.#ws?.readyState === WebSocket.OPEN;
378
- }
379
- /**
380
- * Gracefully close the WebSocket, waiting for any buffered messages to flush.
381
- * Call this before process exit in reuse mode to prevent data loss.
382
- */
383
- async closeWebSocket() {
384
- if (!this.#ws || this.#ws.readyState === WebSocket.CLOSED) {
385
- return;
386
- }
387
- return new Promise((resolve) => {
388
- const timeout = setTimeout(resolve, 2000);
389
- this.#ws.once('close', () => {
390
- clearTimeout(timeout);
391
- resolve();
392
- });
393
- this.#ws.close();
394
- });
395
- }
396
- /**
397
- * Inject the WDIO devtools script into the browser page
398
- */
399
- async injectScript(browser) {
400
- try {
401
- // Load the preload script
402
- const scriptPath = require.resolve('@wdio/devtools-script');
403
- const scriptDir = path.dirname(scriptPath);
404
- const preloadScriptPath = path.join(scriptDir, 'script.js');
405
- let scriptContent = await fs.readFile(preloadScriptPath, 'utf-8');
406
- // The script contains top-level await - wrap the entire script in async IIFE before injection
407
- scriptContent = `(async function() { ${scriptContent} })()`;
408
- // Inject using script element - synchronous check after timeout
409
- const injectionScript = `
410
- const script = document.createElement('script');
411
- script.textContent = arguments[0];
412
- document.head.appendChild(script);
413
- return true;
414
- `;
415
- await browser.execute(injectionScript, [scriptContent]);
416
- // Poll for collector — the async IIFE may take a moment to initialise
417
- let hasCollector = false;
418
- for (let attempt = 0; attempt < 5; attempt++) {
419
- await new Promise((resolve) => setTimeout(resolve, 200));
420
- const checkResult = await browser.execute('return typeof window.wdioTraceCollector !== "undefined"');
421
- hasCollector = (checkResult?.value ?? checkResult) === true;
422
- if (hasCollector) {
423
- break;
424
- }
425
- }
426
- if (hasCollector) {
427
- log.info('✓ Script injected and collector ready');
428
- }
429
- else {
430
- log.warn('Script injection may have failed — collector not found');
431
- }
432
- }
433
- catch (err) {
434
- log.error(`Failed to inject script: ${err.message}`);
435
- throw err;
436
- }
437
- }
438
- /**
439
- * Capture Chrome DevTools browser console logs via WebDriver log API.
440
- * Requires loggingPrefs: { browser: 'ALL' } in Chrome capabilities.
441
- */
442
- async captureBrowserLogs(browser) {
443
- try {
444
- const rawLogs = await browser.getLog('browser');
445
- const logs = (rawLogs?.value ?? rawLogs);
446
- if (!Array.isArray(logs) || logs.length === 0) {
447
- return;
448
- }
449
- const entries = logs.map((entry) => ({
450
- timestamp: entry.timestamp,
451
- type: chromeLogLevelToLogLevel(entry.level),
452
- args: [entry.message],
453
- source: LOG_SOURCES.BROWSER
454
- }));
455
- this.consoleLogs.push(...entries);
456
- this.sendUpstream('consoleLogs', entries);
457
- }
458
- catch {
459
- // Browser log capture not available (loggingPrefs not set or not supported)
460
- }
461
- }
462
- /**
463
- * Parse Chrome performance logs to extract network request entries.
464
- * Requires loggingPrefs: { performance: 'ALL' } in Chrome capabilities.
465
- */
466
- async captureNetworkFromPerformanceLogs(browser) {
467
- try {
468
- const rawLogs = await browser.getLog('performance');
469
- const logs = (rawLogs?.value ?? rawLogs);
470
- if (!Array.isArray(logs) || logs.length === 0) {
471
- return;
472
- }
473
- // Parse CDP Network.* events from the performance log
474
- const pendingRequests = new Map();
475
- const networkEntries = [];
476
- for (const entry of logs) {
477
- try {
478
- const msg = JSON.parse(entry.message);
479
- const { method, params } = msg.message;
480
- if (method === 'Network.requestWillBeSent') {
481
- const { requestId, request: req, timestamp } = params;
482
- pendingRequests.set(requestId, {
483
- id: `${entry.timestamp}-${requestId}`,
484
- url: req.url,
485
- method: req.method,
486
- requestHeaders: req.headers,
487
- timestamp: Math.round(timestamp * 1000),
488
- startTime: entry.timestamp
489
- });
490
- }
491
- else if (method === 'Network.responseReceived') {
492
- const { requestId, response } = params;
493
- const pending = pendingRequests.get(requestId);
494
- if (pending) {
495
- const responseHeaders = {};
496
- for (const [k, v] of Object.entries(response.headers || {})) {
497
- responseHeaders[k.toLowerCase()] = String(v);
498
- }
499
- pending.status = response.status;
500
- pending.statusText = response.statusText;
501
- pending.responseHeaders = responseHeaders;
502
- pending.mimeType = response.mimeType;
503
- pending.type = getRequestType(pending.url, response.mimeType);
504
- }
505
- }
506
- else if (method === 'Network.loadingFinished') {
507
- const { requestId, encodedDataLength } = params;
508
- const pending = pendingRequests.get(requestId);
509
- if (pending && pending.status !== undefined) {
510
- pending.size = encodedDataLength;
511
- pending.endTime = entry.timestamp;
512
- pending.time = entry.timestamp - pending.startTime;
513
- networkEntries.push({ ...pending });
514
- pendingRequests.delete(requestId);
515
- }
516
- }
517
- else if (method === 'Network.loadingFailed') {
518
- const { requestId, errorText } = params;
519
- const pending = pendingRequests.get(requestId);
520
- if (pending) {
521
- pending.error = errorText;
522
- pending.endTime = entry.timestamp;
523
- pending.time = entry.timestamp - pending.startTime;
524
- networkEntries.push({ ...pending });
525
- pendingRequests.delete(requestId);
526
- }
527
- }
528
- }
529
- catch {
530
- // skip malformed entries
531
- }
532
- }
533
- if (networkEntries.length > 0) {
534
- // Helper: for failed requests strip query string so that parallel
535
- // autocomplete/prefetch requests to the same path (e.g. /search?q=W,
536
- // /search?q=We, /search?q=Web…) collapse to a single entry.
537
- const failedKey = (entry) => {
538
- try {
539
- const u = new URL(entry.url);
540
- return `err:${entry.method}:${u.origin}${u.pathname}`;
541
- }
542
- catch {
543
- return `err:${entry.method}:${entry.url}`;
544
- }
545
- };
546
- const alreadySeen = new Set(this.networkRequests.map((r) => r.error !== undefined
547
- ? failedKey(r)
548
- : `ok:${r.method}:${r.url}:${r.timestamp}`));
549
- const deduped = [];
550
- const seenFailedInBatch = new Map();
551
- for (const entry of networkEntries) {
552
- if (entry.error !== undefined) {
553
- const key = failedKey(entry);
554
- if (alreadySeen.has(key)) {
555
- continue;
556
- }
557
- const existing = seenFailedInBatch.get(key);
558
- if (existing !== undefined) {
559
- deduped[existing] = entry; // replace with latest failure
560
- }
561
- else {
562
- seenFailedInBatch.set(key, deduped.length);
563
- deduped.push(entry);
564
- }
565
- }
566
- else {
567
- const key = `ok:${entry.method}:${entry.url}:${entry.timestamp}`;
568
- if (!alreadySeen.has(key)) {
569
- deduped.push(entry);
570
- }
571
- }
572
- }
573
- this.networkRequests.push(...deduped);
574
- this.sendUpstream('networkRequests', deduped);
575
- }
576
- }
577
- catch (err) {
578
- const msg = err.message ?? '';
579
- // Silently skip when performance logging was not enabled in capabilities
580
- if (!msg.includes('log type') && !msg.includes('performance')) {
581
- log.warn(`Performance log capture failed: ${msg}`);
582
- }
583
- }
584
- }
585
- /**
586
- * Capture trace data from the browser (network requests, console logs, etc.)
587
- */
588
- async captureTrace(browser) {
589
- // Capture network requests from Chrome performance logs
590
- await this.captureNetworkFromPerformanceLogs(browser);
591
- // Also try the injected wdioTraceCollector script for XHR/fetch and mutations
592
- try {
593
- const checkResult = await browser.execute('return typeof window.wdioTraceCollector !== "undefined"');
594
- const collectorExists = (checkResult?.value ?? checkResult) === true;
595
- if (!collectorExists) {
596
- return;
597
- }
598
- const result = await browser.execute(`
599
- if (typeof window.wdioTraceCollector === 'undefined') {
600
- return null;
601
- }
602
- return window.wdioTraceCollector.getTraceData();
603
- `);
604
- const traceData = result?.value ?? result;
605
- if (!traceData) {
606
- return;
607
- }
608
- const { mutations, traceLogs, consoleLogs, networkRequests, metadata } = traceData;
609
- if (metadata) {
610
- this.metadata = { ...this.metadata, ...metadata };
611
- this.sendUpstream('metadata', this.metadata);
612
- }
613
- if (Array.isArray(consoleLogs) && consoleLogs.length > 0) {
614
- const tagged = consoleLogs.map((e) => ({
615
- ...e,
616
- source: LOG_SOURCES.BROWSER
617
- }));
618
- this.consoleLogs.push(...tagged);
619
- this.sendUpstream('consoleLogs', tagged);
620
- }
621
- if (Array.isArray(networkRequests) && networkRequests.length > 0) {
622
- this.networkRequests.push(...networkRequests);
623
- this.sendUpstream('networkRequests', networkRequests);
624
- }
625
- if (Array.isArray(mutations) && mutations.length > 0) {
626
- this.mutations.push(...mutations);
627
- this.sendUpstream('mutations', mutations);
628
- log.info(`[trace] Captured ${mutations.length} DOM mutation(s)`);
629
- }
630
- if (Array.isArray(traceLogs) && traceLogs.length > 0) {
631
- this.traceLogs.push(...traceLogs);
632
- this.sendUpstream('logs', traceLogs);
633
- }
634
- if (Array.isArray(networkRequests) && networkRequests.length > 0) {
635
- log.info(`[trace] Captured ${networkRequests.length} network request(s)`);
636
- }
637
- }
638
- catch (err) {
639
- log.error(`Failed to capture trace from injected script: ${err.message}`);
640
- }
641
- }
642
- }
643
- //# sourceMappingURL=session.js.map