@wdio/nightwatch-devtools 1.1.1 → 1.3.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.
- package/README.md +66 -1
- package/dist/bidi-OH4ZFDN5.js +215 -0
- package/dist/bidi-OH4ZFDN5.js.map +1 -0
- package/dist/chunk-DKSBYMPM.js +191 -0
- package/dist/chunk-DKSBYMPM.js.map +1 -0
- package/dist/index.d.ts +111 -16
- package/dist/index.js +6253 -770
- package/dist/index.js.map +1 -1
- package/package.json +20 -13
- package/dist/constants.d.ts +0 -55
- package/dist/constants.js +0 -95
- package/dist/constants.js.map +0 -1
- package/dist/helpers/browserProxy.d.ts +0 -46
- package/dist/helpers/browserProxy.js +0 -314
- package/dist/helpers/browserProxy.js.map +0 -1
- package/dist/helpers/capturePerformance.d.ts +0 -5
- package/dist/helpers/capturePerformance.js +0 -53
- package/dist/helpers/capturePerformance.js.map +0 -1
- package/dist/helpers/cucumberHooks.cjs +0 -54
- package/dist/helpers/cucumberHooks.cjs.map +0 -1
- package/dist/helpers/cucumberHooks.d.cts +0 -21
- package/dist/helpers/suiteManager.d.ts +0 -43
- package/dist/helpers/suiteManager.js +0 -143
- package/dist/helpers/suiteManager.js.map +0 -1
- package/dist/helpers/testManager.d.ts +0 -45
- package/dist/helpers/testManager.js +0 -143
- package/dist/helpers/testManager.js.map +0 -1
- package/dist/helpers/utils.d.ts +0 -83
- package/dist/helpers/utils.js +0 -447
- package/dist/helpers/utils.js.map +0 -1
- package/dist/reporter.d.ts +0 -44
- package/dist/reporter.js +0 -185
- package/dist/reporter.js.map +0 -1
- package/dist/session.d.ts +0 -78
- package/dist/session.js +0 -643
- package/dist/session.js.map +0 -1
- package/dist/types.d.ts +0 -190
- package/dist/types.js +0 -5
- 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
|