@wdio/nightwatch-devtools 1.3.0 → 1.4.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/dist/{bidi-OH4ZFDN5.js → bidi-FIHBBGT4.js} +17 -3
- package/dist/bidi-FIHBBGT4.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +265 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/bidi-OH4ZFDN5.js.map +0 -1
|
@@ -71,6 +71,15 @@ async function attachLogInspector(driver, factory, sinks, log2) {
|
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
75
|
+
function bidiResponseTiming(timings, startTime, timestamp) {
|
|
76
|
+
if (timings && isFiniteNumber(timings.requestTime) && isFiniteNumber(timings.responseEnd) && timings.responseEnd > timings.requestTime) {
|
|
77
|
+
const time = Math.round(timings.responseEnd - timings.requestTime);
|
|
78
|
+
return { endTime: startTime + time, time };
|
|
79
|
+
}
|
|
80
|
+
const endTime = Number(timestamp ?? Date.now());
|
|
81
|
+
return { endTime, time: Math.max(0, endTime - startTime) };
|
|
82
|
+
}
|
|
74
83
|
function handleBidiRequestSent(rawEvent, pending, sinks, log2) {
|
|
75
84
|
const event = rawEvent;
|
|
76
85
|
try {
|
|
@@ -101,14 +110,19 @@ function handleBidiResponseCompleted(rawEvent, pending, sinks, log2) {
|
|
|
101
110
|
if (!previous) {
|
|
102
111
|
return;
|
|
103
112
|
}
|
|
113
|
+
const { endTime, time } = bidiResponseTiming(
|
|
114
|
+
event?.request?.timings,
|
|
115
|
+
previous.startTime,
|
|
116
|
+
event?.timestamp
|
|
117
|
+
);
|
|
104
118
|
const finalized = {
|
|
105
119
|
...previous,
|
|
106
120
|
status: Number(event?.response?.status) || previous.status,
|
|
107
121
|
statusText: event?.response?.statusText ?? previous.statusText,
|
|
108
122
|
responseHeaders: arrayHeadersToObject(event?.response?.headers),
|
|
109
123
|
type: getRequestType(previous.url, event?.response?.mimeType),
|
|
110
|
-
endTime
|
|
111
|
-
time
|
|
124
|
+
endTime,
|
|
125
|
+
time,
|
|
112
126
|
size: Number(event?.response?.bytesReceived) || void 0
|
|
113
127
|
};
|
|
114
128
|
pending.delete(requestId);
|
|
@@ -212,4 +226,4 @@ export {
|
|
|
212
226
|
attachBidiHandlers2 as attachBidiHandlers,
|
|
213
227
|
buildBidiSinks
|
|
214
228
|
};
|
|
215
|
-
//# sourceMappingURL=bidi-
|
|
229
|
+
//# sourceMappingURL=bidi-FIHBBGT4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bidi.ts","../../core/src/bidi.ts"],"sourcesContent":["import logger from '@wdio/logger'\nimport {\n type BidiHandlerSinks,\n attachBidiHandlers as attachBidiHandlersCore\n} from '@wdio/devtools-core'\nimport type { SessionCapturer } from './session.js'\n\nconst log = logger('@wdio/nightwatch-devtools:bidi')\n\n/**\n * Nightwatch wrapper around the core BiDi attach helper. Nightwatch ships\n * selenium-webdriver under the hood (via chromedriver), so the same\n * `selenium-webdriver/bidi` inspectors selenium-devtools uses are available\n * whenever the user has set `webSocketUrl: true` in their capabilities.\n *\n * Opt-in via the plugin's `bidi: true` option. When attached, the per-command\n * Chrome performance-log network capture is gated off to avoid duplicate\n * entries in the dashboard.\n */\nexport async function attachBidiHandlers(\n driver: unknown,\n sinks: BidiHandlerSinks\n): Promise<boolean> {\n return attachBidiHandlersCore(driver, sinks, (level, message) =>\n log[level](message)\n )\n}\n\n/**\n * Build sinks that route BiDi events into the SessionCapturer's local arrays\n * and broadcast them upstream. Mirrors selenium-devtools' `buildBidiSinks` —\n * separate per-adapter because the SessionCapturer concrete types differ.\n */\nexport function buildBidiSinks(capturer: SessionCapturer): BidiHandlerSinks {\n return {\n pushConsoleLog: (entry) => {\n capturer.consoleLogs.push(entry)\n capturer.sendUpstream('consoleLogs', [entry])\n },\n pushNetworkRequest: (entry) => {\n capturer.networkRequests.push(entry)\n capturer.sendUpstream('networkRequests', [entry])\n },\n replaceNetworkRequest: (id, entry) => {\n const idx = capturer.networkRequests.findIndex(\n (r: { id?: string }) => r.id === id\n )\n if (idx !== -1) {\n capturer.networkRequests[idx] = entry\n } else {\n capturer.networkRequests.push(entry)\n }\n capturer.sendUpstream('networkRequests', [entry])\n }\n }\n}\n","import { createRequire } from 'node:module'\nimport type {\n ConsoleLog,\n LogLevel,\n NetworkRequest\n} from '@wdio/devtools-shared'\nimport {\n LOG_SOURCES,\n chromeLogLevelToLogLevel,\n type LogSource\n} from './console.js'\nimport { errorMessage } from './error.js'\nimport { getRequestType } from './net.js'\n\n/**\n * Generic sinks the BiDi handlers push into. Each adapter wires these to its\n * own SessionCapturer state — selenium's `buildBidiSinks` is the canonical\n * example; nightwatch can mirror the pattern when it wires up BiDi.\n */\nexport interface BidiHandlerSinks {\n pushConsoleLog: (entry: ConsoleLog) => void\n pushNetworkRequest: (entry: NetworkRequest) => void\n replaceNetworkRequest: (id: string, entry: NetworkRequest) => void\n}\n\n/**\n * Resolve a `selenium-webdriver/<subpath>` module from the user's install\n * (preferred) or the package's local install (fallback). Returns `null` if\n * neither resolves — caller should treat as \"BiDi not available on this\n * runtime\" and degrade gracefully.\n *\n * Used by both selenium-devtools and (when wired up) nightwatch-devtools —\n * both ship selenium-webdriver-style drivers under the hood.\n */\nexport function loadSeleniumSubmodule<T = unknown>(subpath: string): T | null {\n try {\n const userRequire = createRequire(`${process.cwd()}/`)\n return userRequire(`selenium-webdriver/${subpath}`) as T\n } catch {\n try {\n const localRequire = createRequire(import.meta.url)\n return localRequire(`selenium-webdriver/${subpath}`) as T\n } catch {\n return null\n }\n }\n}\n\ntype BidiLogger = (level: 'info' | 'warn', message: string) => void\ntype InspectorFactory = (driver: unknown) => Promise<unknown>\n\ninterface LogInspector {\n onConsoleEntry: (cb: (entry: unknown) => void) => Promise<void>\n onJavascriptException: (cb: (exc: unknown) => void) => Promise<void>\n}\n\ninterface NetworkInspector {\n beforeRequestSent: (cb: (e: unknown) => void) => Promise<void>\n responseCompleted: (cb: (e: unknown) => void) => Promise<void>\n}\n\nexport function handleBidiConsoleEntry(\n rawEntry: unknown,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): void {\n const entry = rawEntry as {\n level?: string\n type?: string\n text?: string\n message?: string\n timestamp?: number\n }\n try {\n const level = (entry?.level ?? entry?.type ?? 'info').toString()\n const text = entry?.text ?? entry?.message ?? ''\n sinks.pushConsoleLog({\n timestamp: Number(entry?.timestamp) || Date.now(),\n type: chromeLogLevelToLogLevel(level) as LogLevel,\n args: [text],\n source: LOG_SOURCES.BROWSER as LogSource\n })\n } catch (err) {\n log('warn', `onConsoleEntry handler threw: ${errorMessage(err)}`)\n }\n}\n\nexport function handleBidiJsException(\n rawExc: unknown,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): void {\n const exception = rawExc as { text?: string; message?: string }\n try {\n const text = exception?.text ?? exception?.message ?? String(rawExc)\n const trimmed = String(text).replace(/\\s+/g, ' ').slice(0, 200)\n log(\n 'warn',\n `🐛 JS error in page: ${trimmed}${String(text).length > 200 ? '…' : ''}`\n )\n sinks.pushConsoleLog({\n timestamp: Date.now(),\n type: 'error',\n args: [text],\n source: LOG_SOURCES.BROWSER as LogSource\n })\n } catch (err) {\n log('warn', `onJavascriptException handler threw: ${errorMessage(err)}`)\n }\n}\n\nasync function attachLogInspector(\n driver: unknown,\n factory: InspectorFactory,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): Promise<boolean> {\n try {\n const inspector = (await factory(driver)) as LogInspector\n await inspector.onConsoleEntry((e) => handleBidiConsoleEntry(e, sinks, log))\n await inspector.onJavascriptException((e) =>\n handleBidiJsException(e, sinks, log)\n )\n log('info', '✓ BiDi LogInspector attached (console + JS exceptions)')\n return true\n } catch (err) {\n log('warn', `BiDi LogInspector attach failed: ${errorMessage(err)}`)\n return false\n }\n}\n\n/** Subset of WebDriver BiDi `FetchTimingInfo` (selenium exposes these via\n * getters). Values are ms offsets from the request baseline `requestTime`\n * (usually 0), so `responseEnd - requestTime` is the request duration. */\ninterface FetchTimings {\n requestTime?: number\n responseEnd?: number\n}\n\nconst isFiniteNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value)\n\ninterface BeforeRequestSentEvent {\n request?: {\n request?: string\n url?: string\n method?: string\n headers?: unknown\n timings?: FetchTimings\n }\n id?: string\n timestamp?: number\n}\n\ninterface ResponseCompletedEvent {\n request?: { request?: string; timings?: FetchTimings }\n id?: string\n timestamp?: number\n response?: {\n status?: number\n statusText?: string\n headers?: unknown\n mimeType?: string\n bytesReceived?: number\n }\n}\n\n/** Request duration (ms) from the browser's FetchTimingInfo, plus a derived\n * endTime anchored to `startTime`. The browser timings are immune to BiDi\n * events arriving batched in one tick (which collapses the event-level\n * `timestamp`/`Date.now()` to a single value and yields 0-duration requests).\n * Falls back to the timestamp diff only when timings are unavailable. */\nfunction bidiResponseTiming(\n timings: FetchTimings | undefined,\n startTime: number,\n timestamp?: number\n): { endTime: number; time: number } {\n if (\n timings &&\n isFiniteNumber(timings.requestTime) &&\n isFiniteNumber(timings.responseEnd) &&\n timings.responseEnd > timings.requestTime\n ) {\n const time = Math.round(timings.responseEnd - timings.requestTime)\n return { endTime: startTime + time, time }\n }\n const endTime = Number(timestamp ?? Date.now())\n return { endTime, time: Math.max(0, endTime - startTime) }\n}\n\nexport function handleBidiRequestSent(\n rawEvent: unknown,\n pending: Map<string, NetworkRequest>,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): void {\n const event = rawEvent as BeforeRequestSentEvent\n try {\n const requestId = String(event?.request?.request ?? event?.id ?? '')\n if (!requestId) {\n return\n }\n const entry: NetworkRequest = {\n id: requestId,\n url: event?.request?.url ?? '',\n method: event?.request?.method ?? 'GET',\n requestHeaders: arrayHeadersToObject(event?.request?.headers),\n timestamp: Date.now(),\n startTime: Number(event?.timestamp ?? Date.now()),\n type: getRequestType(event?.request?.url ?? '')\n }\n pending.set(requestId, entry)\n sinks.pushNetworkRequest(entry)\n } catch (err) {\n log('warn', `beforeRequestSent threw: ${errorMessage(err)}`)\n }\n}\n\nexport function handleBidiResponseCompleted(\n rawEvent: unknown,\n pending: Map<string, NetworkRequest>,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): void {\n const event = rawEvent as ResponseCompletedEvent\n try {\n const requestId = String(event?.request?.request ?? event?.id ?? '')\n const previous = pending.get(requestId)\n if (!previous) {\n return\n }\n const { endTime, time } = bidiResponseTiming(\n event?.request?.timings,\n previous.startTime,\n event?.timestamp\n )\n const finalized: NetworkRequest = {\n ...previous,\n status: Number(event?.response?.status) || previous.status,\n statusText: event?.response?.statusText ?? previous.statusText,\n responseHeaders: arrayHeadersToObject(event?.response?.headers),\n type: getRequestType(previous.url, event?.response?.mimeType),\n endTime,\n time,\n size: Number(event?.response?.bytesReceived) || undefined\n }\n pending.delete(requestId)\n sinks.replaceNetworkRequest(requestId, finalized)\n } catch (err) {\n log('warn', `responseCompleted threw: ${errorMessage(err)}`)\n }\n}\n\nasync function attachNetworkInspector(\n driver: unknown,\n factory: InspectorFactory,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): Promise<boolean> {\n try {\n const inspector = (await factory(driver)) as NetworkInspector\n const pending = new Map<string, NetworkRequest>()\n await inspector.beforeRequestSent((e) =>\n handleBidiRequestSent(e, pending, sinks, log)\n )\n await inspector.responseCompleted((e) =>\n handleBidiResponseCompleted(e, pending, sinks, log)\n )\n log('info', '✓ BiDi NetworkInspector attached (request + response)')\n return true\n } catch (err) {\n log('warn', `BiDi NetworkInspector attach failed: ${errorMessage(err)}`)\n return false\n }\n}\n\n/**\n * Attach the selenium-webdriver BiDi LogInspector + NetworkInspector to a\n * driver and route their events into the given sinks. Returns `true` when at\n * least one inspector connected — caller can disable the equivalent\n * script-injection collectors to avoid duplicates.\n *\n * Tolerant of older / non-BiDi runtimes: if either submodule fails to load\n * or the inspector factory throws (driver session doesn't have webSocketUrl\n * capability set, etc.), the corresponding stream is silently skipped and\n * the function returns false.\n *\n * @param onLog Optional callback for adapter-side logging. Receives ('info' |\n * 'warn', message) on lifecycle events. Default: silent — adapters wire their\n * own logger when they want visibility into BiDi attach state.\n */\nexport async function attachBidiHandlers(\n driver: unknown,\n sinks: BidiHandlerSinks,\n onLog?: (level: 'info' | 'warn', message: string) => void\n): Promise<boolean> {\n const log: BidiLogger = (level, message) => onLog?.(level, message)\n const logFactory =\n loadSeleniumSubmodule<InspectorFactory>('bidi/logInspector')\n const networkFactory = loadSeleniumSubmodule<InspectorFactory>(\n 'bidi/networkInspector'\n )\n\n let attached = 0\n if (typeof logFactory === 'function') {\n if (await attachLogInspector(driver, logFactory, sinks, log)) {\n attached++\n }\n } else {\n log('info', 'selenium-webdriver/bidi/logInspector not available — skipping')\n }\n if (typeof networkFactory === 'function') {\n if (await attachNetworkInspector(driver, networkFactory, sinks, log)) {\n attached++\n }\n } else {\n log(\n 'info',\n 'selenium-webdriver/bidi/networkInspector not available — skipping'\n )\n }\n return attached > 0\n}\n\n/**\n * Flatten BiDi's `Array<{name, value:{value|type}}>` header shape to a\n * lowercased `Record<string, string>`. Exported so adapter-side helpers can\n * reuse it for their own header normalization.\n */\nexport function arrayHeadersToObject(\n headers: unknown\n): Record<string, string> | undefined {\n if (!Array.isArray(headers)) {\n return undefined\n }\n const out: Record<string, string> = {}\n for (const h of headers as Array<{\n name?: string\n value?: string | { value?: string; type?: string }\n }>) {\n const name = String(h?.name ?? '').toLowerCase()\n if (!name) {\n continue\n }\n const v = h?.value\n out[name] =\n typeof v === 'string'\n ? v\n : typeof (v as { value?: string })?.value === 'string'\n ? (v as { value: string }).value\n : JSON.stringify(v ?? '')\n }\n return out\n}\n"],"mappings":"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","names":["log","attachBidiHandlers"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ScreencastOptions, DevToolsMode, TraceFormat } from '@wdio/devtools-shared';
|
|
1
|
+
import { ScreencastOptions, DevToolsMode, TraceFormat, TraceGranularity } from '@wdio/devtools-shared';
|
|
2
2
|
|
|
3
3
|
/** Nightwatch `eventHub` shape — only `runner` + `on()` are documented; the
|
|
4
4
|
* rest of the public surface is `unknown` to us. */
|
|
@@ -30,6 +30,10 @@ interface DevToolsOptions {
|
|
|
30
30
|
/** Trace output layout — `zip` (default) writes a single archive,
|
|
31
31
|
* `ndjson-directory` unpacks into `trace-<id>/`. Only applies in trace mode. */
|
|
32
32
|
traceFormat?: TraceFormat;
|
|
33
|
+
/** Trace output granularity — `session` (default) writes one trace per
|
|
34
|
+
* worker session; `spec` writes one trace per spec file. Only applies in
|
|
35
|
+
* trace mode. */
|
|
36
|
+
traceGranularity?: TraceGranularity;
|
|
33
37
|
}
|
|
34
38
|
interface NightwatchBrowser {
|
|
35
39
|
url: (url: string) => Promise<unknown>;
|
package/dist/index.js
CHANGED
|
@@ -2060,15 +2060,46 @@ function buildContextOptions(trace, contextId, wallTime) {
|
|
|
2060
2060
|
}
|
|
2061
2061
|
};
|
|
2062
2062
|
}
|
|
2063
|
-
function buildActionEvents(commands, pageId, wallTime) {
|
|
2063
|
+
function buildActionEvents(commands, pageId, wallTime, testMetadata) {
|
|
2064
2064
|
const events = [];
|
|
2065
2065
|
let prevEndMs = 0;
|
|
2066
2066
|
let callCounter = 0;
|
|
2067
|
+
let lastTestUid;
|
|
2068
|
+
let groupCallId;
|
|
2067
2069
|
for (const cmd of commands) {
|
|
2068
2070
|
const action = mapCommandToAction(cmd.command);
|
|
2069
2071
|
if (!action) {
|
|
2070
2072
|
continue;
|
|
2071
2073
|
}
|
|
2074
|
+
if (cmd.testUid && cmd.testUid !== lastTestUid) {
|
|
2075
|
+
if (lastTestUid && groupCallId) {
|
|
2076
|
+
callCounter++;
|
|
2077
|
+
events.push({
|
|
2078
|
+
type: "after",
|
|
2079
|
+
callId: groupCallId,
|
|
2080
|
+
endTime: prevEndMs
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
callCounter++;
|
|
2084
|
+
groupCallId = `call@${callCounter}`;
|
|
2085
|
+
const meta = testMetadata?.get(cmd.testUid);
|
|
2086
|
+
const groupName = meta?.title ?? cmd.testUid;
|
|
2087
|
+
events.push({
|
|
2088
|
+
type: "before",
|
|
2089
|
+
callId: groupCallId,
|
|
2090
|
+
startTime: Math.max(
|
|
2091
|
+
prevEndMs,
|
|
2092
|
+
(cmd.startTime ?? cmd.timestamp) - wallTime
|
|
2093
|
+
),
|
|
2094
|
+
class: "Tracing",
|
|
2095
|
+
method: "tracingGroup",
|
|
2096
|
+
pageId,
|
|
2097
|
+
params: { name: groupName },
|
|
2098
|
+
title: groupName,
|
|
2099
|
+
apiName: "tracing.tracingGroup"
|
|
2100
|
+
});
|
|
2101
|
+
lastTestUid = cmd.testUid;
|
|
2102
|
+
}
|
|
2072
2103
|
callCounter++;
|
|
2073
2104
|
const callId = `call@${callCounter}`;
|
|
2074
2105
|
const rawStartMs = (cmd.startTime ?? cmd.timestamp) - wallTime;
|
|
@@ -2098,7 +2129,8 @@ function buildActionEvents(commands, pageId, wallTime) {
|
|
|
2098
2129
|
pageId,
|
|
2099
2130
|
params,
|
|
2100
2131
|
title: formatActionTitle(action, cmd.args, params),
|
|
2101
|
-
apiName: `${action.class.toLowerCase()}.${action.method}
|
|
2132
|
+
apiName: `${action.class.toLowerCase()}.${action.method}`,
|
|
2133
|
+
parentId: groupCallId
|
|
2102
2134
|
});
|
|
2103
2135
|
const afterEvent = {
|
|
2104
2136
|
type: "after",
|
|
@@ -2112,6 +2144,14 @@ function buildActionEvents(commands, pageId, wallTime) {
|
|
|
2112
2144
|
events.push(afterEvent);
|
|
2113
2145
|
prevEndMs = endMs;
|
|
2114
2146
|
}
|
|
2147
|
+
if (lastTestUid && groupCallId) {
|
|
2148
|
+
callCounter++;
|
|
2149
|
+
events.push({
|
|
2150
|
+
type: "after",
|
|
2151
|
+
callId: groupCallId,
|
|
2152
|
+
endTime: prevEndMs
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2115
2155
|
return events;
|
|
2116
2156
|
}
|
|
2117
2157
|
function buildNetworkNdjson(requests, wallTime, pageId) {
|
|
@@ -2268,7 +2308,7 @@ function buildTraceBundle(trace, opts = {}) {
|
|
|
2268
2308
|
wallTime,
|
|
2269
2309
|
viewport
|
|
2270
2310
|
),
|
|
2271
|
-
...buildActionEvents(trace.commands, pageId, wallTime)
|
|
2311
|
+
...buildActionEvents(trace.commands, pageId, wallTime, opts.testMetadata)
|
|
2272
2312
|
);
|
|
2273
2313
|
events.sort(compareEvents);
|
|
2274
2314
|
const ctxBName = ctxOptions.title;
|
|
@@ -2326,7 +2366,8 @@ async function writeTraceZip(capturer, opts) {
|
|
|
2326
2366
|
await fs.mkdir(opts.outputDir, { recursive: true });
|
|
2327
2367
|
const exportOpts = {
|
|
2328
2368
|
sessionId: opts.sessionId,
|
|
2329
|
-
wallTimeOverride: capturer.startWallTime
|
|
2369
|
+
wallTimeOverride: capturer.startWallTime,
|
|
2370
|
+
testMetadata: opts.testMetadata
|
|
2330
2371
|
};
|
|
2331
2372
|
if (opts.format === "ndjson-directory") {
|
|
2332
2373
|
const dir = path.join(opts.outputDir, `trace-${opts.sessionId}`);
|
|
@@ -2424,6 +2465,15 @@ function runFfmpeg(ffmpeg, manifestPath, outputPath) {
|
|
|
2424
2465
|
"cfr",
|
|
2425
2466
|
"-r",
|
|
2426
2467
|
"10",
|
|
2468
|
+
// Force a keyframe every frame. VP8 can only seek to keyframes, and
|
|
2469
|
+
// libvpx's default GOP places a single keyframe on near-static
|
|
2470
|
+
// screencast content — so scrubber/marker seeks snap back to the
|
|
2471
|
+
// start. At 10fps over short recordings, all-intra keeps seeking
|
|
2472
|
+
// frame-accurate at an acceptable size.
|
|
2473
|
+
"-g",
|
|
2474
|
+
"1",
|
|
2475
|
+
"-keyint_min",
|
|
2476
|
+
"1",
|
|
2427
2477
|
"-auto-alt-ref",
|
|
2428
2478
|
"0",
|
|
2429
2479
|
"-disposition:v",
|
|
@@ -2487,7 +2537,8 @@ async function finalizeScreencast({
|
|
|
2487
2537
|
videoPath,
|
|
2488
2538
|
videoFile: fileName,
|
|
2489
2539
|
frameCount: frames.length,
|
|
2490
|
-
duration: recorder.duration
|
|
2540
|
+
duration: recorder.duration,
|
|
2541
|
+
startTime: frames[0]?.timestamp
|
|
2491
2542
|
});
|
|
2492
2543
|
} catch (err) {
|
|
2493
2544
|
log12("warn", `Screencast encode failed: ${errorMessage(err)}`);
|
|
@@ -2951,9 +3002,12 @@ var SessionCapturerBase = class {
|
|
|
2951
3002
|
metadata;
|
|
2952
3003
|
// ── Construction ────────────────────────────────────────────────────────
|
|
2953
3004
|
constructor(opts = {}) {
|
|
2954
|
-
const { hostname, port } = opts;
|
|
3005
|
+
const { hostname, port, reconnect } = opts;
|
|
2955
3006
|
if (hostname && port) {
|
|
2956
|
-
|
|
3007
|
+
const query = reconnect ? "?reconnect=1" : "";
|
|
3008
|
+
this.ws = new WebSocket(
|
|
3009
|
+
`ws://${hostname}:${port}${WS_PATHS.worker}${query}`
|
|
3010
|
+
);
|
|
2957
3011
|
this.ws.on("open", () => {
|
|
2958
3012
|
this.#hasConnected = true;
|
|
2959
3013
|
this.onWsOpen();
|
|
@@ -3285,6 +3339,100 @@ function safeStringify(value) {
|
|
|
3285
3339
|
}
|
|
3286
3340
|
}
|
|
3287
3341
|
|
|
3342
|
+
// ../core/src/spec-trace-helpers.ts
|
|
3343
|
+
function sanitizeSpecName(specFile) {
|
|
3344
|
+
return specFile.replace(/^.*[/\\]/, "").replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_").replace(/^_+|_+$/g, "") || "unknown-spec";
|
|
3345
|
+
}
|
|
3346
|
+
function buildSpecSessionId(specFile, sessionId) {
|
|
3347
|
+
const base = sanitizeSpecName(specFile);
|
|
3348
|
+
const hash = deterministicUid(specFile).split("-").pop().slice(0, 8);
|
|
3349
|
+
return `${base}-${hash}-${sessionId.slice(0, 8)}`;
|
|
3350
|
+
}
|
|
3351
|
+
function buildSpecCapturer(capturer, range, nextRange) {
|
|
3352
|
+
const end = nextRange ? {
|
|
3353
|
+
commands: nextRange.commandStartIdx,
|
|
3354
|
+
console: nextRange.consoleStartIdx,
|
|
3355
|
+
network: nextRange.networkStartIdx,
|
|
3356
|
+
mutations: nextRange.mutationStartIdx,
|
|
3357
|
+
traceLogs: nextRange.traceLogStartIdx
|
|
3358
|
+
} : void 0;
|
|
3359
|
+
return {
|
|
3360
|
+
mutations: capturer.mutations.slice(range.mutationStartIdx, end?.mutations),
|
|
3361
|
+
traceLogs: capturer.traceLogs.slice(range.traceLogStartIdx, end?.traceLogs),
|
|
3362
|
+
consoleLogs: capturer.consoleLogs.slice(
|
|
3363
|
+
range.consoleStartIdx,
|
|
3364
|
+
end?.console
|
|
3365
|
+
),
|
|
3366
|
+
networkRequests: capturer.networkRequests.slice(
|
|
3367
|
+
range.networkStartIdx,
|
|
3368
|
+
end?.network
|
|
3369
|
+
),
|
|
3370
|
+
commandsLog: capturer.commandsLog.slice(
|
|
3371
|
+
range.commandStartIdx,
|
|
3372
|
+
end?.commands
|
|
3373
|
+
),
|
|
3374
|
+
sources: new Map(capturer.sources),
|
|
3375
|
+
metadata: capturer.metadata,
|
|
3376
|
+
startWallTime: capturer.startWallTime
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
function filterTestMetadataBySpec(allMetadata, specFile) {
|
|
3380
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
3381
|
+
for (const [uid, meta] of allMetadata) {
|
|
3382
|
+
if (meta.specFile === specFile) {
|
|
3383
|
+
filtered.set(uid, meta);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return filtered;
|
|
3387
|
+
}
|
|
3388
|
+
function recordSpecBoundary(ctx, specFile, traceGranularity) {
|
|
3389
|
+
if (traceGranularity !== "spec") {
|
|
3390
|
+
return null;
|
|
3391
|
+
}
|
|
3392
|
+
const lastRange = ctx.specRanges[ctx.specRanges.length - 1];
|
|
3393
|
+
if (lastRange && lastRange.specFile === specFile) {
|
|
3394
|
+
return null;
|
|
3395
|
+
}
|
|
3396
|
+
const prevRange = lastRange && !ctx.flushedSpecs.has(lastRange.specFile) ? lastRange : null;
|
|
3397
|
+
ctx.specRanges.push({
|
|
3398
|
+
specFile,
|
|
3399
|
+
commandStartIdx: ctx.capturer.commandsLog.length,
|
|
3400
|
+
consoleStartIdx: ctx.capturer.consoleLogs.length,
|
|
3401
|
+
networkStartIdx: ctx.capturer.networkRequests.length,
|
|
3402
|
+
mutationStartIdx: ctx.capturer.mutations.length,
|
|
3403
|
+
traceLogStartIdx: ctx.capturer.traceLogs.length,
|
|
3404
|
+
snapshotCount: ctx.actionSnapshots.length
|
|
3405
|
+
});
|
|
3406
|
+
return prevRange;
|
|
3407
|
+
}
|
|
3408
|
+
async function writeSpecTrace(input) {
|
|
3409
|
+
const specCapturer = buildSpecCapturer(
|
|
3410
|
+
input.capturer,
|
|
3411
|
+
input.range,
|
|
3412
|
+
input.nextRange
|
|
3413
|
+
);
|
|
3414
|
+
const specSnapshots = input.actionSnapshots.slice(
|
|
3415
|
+
input.range.snapshotCount,
|
|
3416
|
+
input.nextRange?.snapshotCount ?? input.actionSnapshots.length
|
|
3417
|
+
);
|
|
3418
|
+
const specSessionId = buildSpecSessionId(
|
|
3419
|
+
input.range.specFile,
|
|
3420
|
+
input.sessionId
|
|
3421
|
+
);
|
|
3422
|
+
const testMetadata = filterTestMetadataBySpec(
|
|
3423
|
+
input.testMetadata,
|
|
3424
|
+
input.range.specFile
|
|
3425
|
+
);
|
|
3426
|
+
return writeTraceZip(specCapturer, {
|
|
3427
|
+
outputDir: input.outputDir,
|
|
3428
|
+
sessionId: specSessionId,
|
|
3429
|
+
capabilities: input.capabilities,
|
|
3430
|
+
actionSnapshots: specSnapshots.length > 0 ? specSnapshots : void 0,
|
|
3431
|
+
format: input.format,
|
|
3432
|
+
testMetadata
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3288
3436
|
// ../core/src/suite-helpers.ts
|
|
3289
3437
|
function createSuiteStats(init) {
|
|
3290
3438
|
return {
|
|
@@ -3824,6 +3972,13 @@ async function openDevtoolsBrowser(ctx, url) {
|
|
|
3824
3972
|
"--no-first-run",
|
|
3825
3973
|
"--no-default-browser-check"
|
|
3826
3974
|
]
|
|
3975
|
+
},
|
|
3976
|
+
// Dashboard uses the Puppeteer-based 'devtools' protocol; drop the
|
|
3977
|
+
// "controlled by automated test software" infobar by ignoring
|
|
3978
|
+
// Puppeteer's --enable-automation default. (Runtime-valid capability
|
|
3979
|
+
// not present in the type.)
|
|
3980
|
+
"wdio:devtoolsOptions": {
|
|
3981
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
3827
3982
|
}
|
|
3828
3983
|
}
|
|
3829
3984
|
});
|
|
@@ -4515,18 +4670,22 @@ function applyResponseReceived(p, response) {
|
|
|
4515
4670
|
p.mimeType = response.mimeType;
|
|
4516
4671
|
p.type = getRequestType(p.url, response.mimeType);
|
|
4517
4672
|
}
|
|
4673
|
+
function eventTimeMs(params, entry) {
|
|
4674
|
+
return typeof params.timestamp === "number" ? Math.round(params.timestamp * 1e3) : entry.timestamp;
|
|
4675
|
+
}
|
|
4518
4676
|
function startPendingRequest(params, entry, pending) {
|
|
4519
4677
|
const { requestId, request: req, timestamp } = params;
|
|
4520
4678
|
if (!req || typeof timestamp !== "number") {
|
|
4521
4679
|
return;
|
|
4522
4680
|
}
|
|
4681
|
+
const startTime = eventTimeMs(params, entry);
|
|
4523
4682
|
pending.set(requestId, {
|
|
4524
4683
|
id: `${entry.timestamp}-${requestId}`,
|
|
4525
4684
|
url: req.url,
|
|
4526
4685
|
method: req.method,
|
|
4527
4686
|
requestHeaders: req.headers ?? {},
|
|
4528
|
-
timestamp:
|
|
4529
|
-
startTime
|
|
4687
|
+
timestamp: startTime,
|
|
4688
|
+
startTime
|
|
4530
4689
|
});
|
|
4531
4690
|
}
|
|
4532
4691
|
function handlePerfLogEvent(method, params, entry, pending, completed) {
|
|
@@ -4545,8 +4704,8 @@ function handlePerfLogEvent(method, params, entry, pending, completed) {
|
|
|
4545
4704
|
const p = pending.get(params.requestId);
|
|
4546
4705
|
if (p && p.status !== void 0) {
|
|
4547
4706
|
p.size = params.encodedDataLength;
|
|
4548
|
-
p.endTime = entry
|
|
4549
|
-
p.time =
|
|
4707
|
+
p.endTime = eventTimeMs(params, entry);
|
|
4708
|
+
p.time = Math.max(0, p.endTime - p.startTime);
|
|
4550
4709
|
completed.push({ ...p });
|
|
4551
4710
|
pending.delete(params.requestId);
|
|
4552
4711
|
}
|
|
@@ -4556,8 +4715,8 @@ function handlePerfLogEvent(method, params, entry, pending, completed) {
|
|
|
4556
4715
|
const p = pending.get(params.requestId);
|
|
4557
4716
|
if (p) {
|
|
4558
4717
|
p.error = params.errorText;
|
|
4559
|
-
p.endTime = entry
|
|
4560
|
-
p.time =
|
|
4718
|
+
p.endTime = eventTimeMs(params, entry);
|
|
4719
|
+
p.time = Math.max(0, p.endTime - p.startTime);
|
|
4561
4720
|
completed.push({ ...p });
|
|
4562
4721
|
pending.delete(params.requestId);
|
|
4563
4722
|
}
|
|
@@ -5754,7 +5913,7 @@ async function tryAttachBidi(ctx, browser) {
|
|
|
5754
5913
|
log10.warn("bidi:true set but browser.driver unavailable \u2014 skipping");
|
|
5755
5914
|
return;
|
|
5756
5915
|
}
|
|
5757
|
-
const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-
|
|
5916
|
+
const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-FIHBBGT4.js");
|
|
5758
5917
|
const ok = await attachBidiHandlers(
|
|
5759
5918
|
driver,
|
|
5760
5919
|
buildBidiSinks(ctx.sessionCapturer)
|
|
@@ -5790,7 +5949,13 @@ async function ensureSessionInitialized(ctx, browser, finalizeCurrentScreencast2
|
|
|
5790
5949
|
(resolve2) => setTimeout(resolve2, TIMING.INITIAL_CONNECTION_WAIT)
|
|
5791
5950
|
);
|
|
5792
5951
|
ctx.sessionCapturer = new SessionCapturer(
|
|
5793
|
-
ctx.mode === "trace" ? {} : {
|
|
5952
|
+
ctx.mode === "trace" ? {} : {
|
|
5953
|
+
port: ctx.port,
|
|
5954
|
+
hostname: ctx.hostname,
|
|
5955
|
+
// A session change reopens the WS mid-run — tell the backend to keep
|
|
5956
|
+
// the accumulated run state so earlier tests' commands survive.
|
|
5957
|
+
reconnect: Boolean(isSessionChange)
|
|
5958
|
+
},
|
|
5794
5959
|
browser
|
|
5795
5960
|
);
|
|
5796
5961
|
ctx.sessionCapturer.traceMode = ctx.mode;
|
|
@@ -5851,6 +6016,10 @@ var NightwatchDevToolsPlugin = class {
|
|
|
5851
6016
|
#skipCount = 0;
|
|
5852
6017
|
#configPath;
|
|
5853
6018
|
#srcFolders = [];
|
|
6019
|
+
/** Index ranges into the session capturer's flat arrays, one per spec file. */
|
|
6020
|
+
#specRanges = [];
|
|
6021
|
+
/** Set of spec files already flushed to disk. */
|
|
6022
|
+
#flushedSpecs = /* @__PURE__ */ new Set();
|
|
5854
6023
|
#getRerunLabel() {
|
|
5855
6024
|
return process.env[REUSE_ENV.RERUN_ENTRY_TYPE] === "test" ? process.env[REUSE_ENV.RERUN_LABEL]?.trim() : void 0;
|
|
5856
6025
|
}
|
|
@@ -5872,7 +6041,8 @@ var NightwatchDevToolsPlugin = class {
|
|
|
5872
6041
|
screencast,
|
|
5873
6042
|
bidi: options.bidi ?? false,
|
|
5874
6043
|
mode,
|
|
5875
|
-
traceFormat: options.traceFormat ?? "zip"
|
|
6044
|
+
traceFormat: options.traceFormat ?? "zip",
|
|
6045
|
+
traceGranularity: options.traceGranularity ?? "session"
|
|
5876
6046
|
};
|
|
5877
6047
|
this.#screencastOptions = { ...SCREENCAST_DEFAULTS, ...screencast };
|
|
5878
6048
|
this.#bidiEnabled = options.bidi === true;
|
|
@@ -6130,6 +6300,25 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6130
6300
|
this.sessionCapturer.captureSource(fullPath).catch(() => {
|
|
6131
6301
|
});
|
|
6132
6302
|
}
|
|
6303
|
+
if (fullPath) {
|
|
6304
|
+
const prevRange = recordSpecBoundary(
|
|
6305
|
+
{
|
|
6306
|
+
specRanges: this.#specRanges,
|
|
6307
|
+
flushedSpecs: this.#flushedSpecs,
|
|
6308
|
+
capturer: this.sessionCapturer,
|
|
6309
|
+
actionSnapshots: this.sessionCapturer.actionSnapshots
|
|
6310
|
+
},
|
|
6311
|
+
fullPath,
|
|
6312
|
+
this.options.traceGranularity
|
|
6313
|
+
);
|
|
6314
|
+
if (prevRange) {
|
|
6315
|
+
void this.#flushSpecTrace(prevRange).catch(
|
|
6316
|
+
(err) => log11.warn(
|
|
6317
|
+
`Failed to flush trace for spec "${prevRange.specFile}": ${errorMessage(err)}`
|
|
6318
|
+
)
|
|
6319
|
+
);
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6133
6322
|
await this.#closePreviousRunningTest(currentSuite, testFile, currentTest);
|
|
6134
6323
|
const processedTests = this.testManager.getProcessedTests(testFile);
|
|
6135
6324
|
const currentTestName = this.#pickCurrentTestName(
|
|
@@ -6187,6 +6376,42 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6187
6376
|
#logRunSummary() {
|
|
6188
6377
|
logRunSummary(this.#getInternals());
|
|
6189
6378
|
}
|
|
6379
|
+
async #flushSpecTrace(range, nextRange) {
|
|
6380
|
+
if (this.#flushedSpecs.has(range.specFile)) {
|
|
6381
|
+
return void 0;
|
|
6382
|
+
}
|
|
6383
|
+
this.#flushedSpecs.add(range.specFile);
|
|
6384
|
+
const sessionId = this.sessionCapturer.metadata?.sessionId;
|
|
6385
|
+
if (!sessionId) {
|
|
6386
|
+
return void 0;
|
|
6387
|
+
}
|
|
6388
|
+
const allMetadata = /* @__PURE__ */ new Map();
|
|
6389
|
+
if (this.suiteManager) {
|
|
6390
|
+
for (const suite of this.suiteManager.getAllSuites().values()) {
|
|
6391
|
+
for (const entry of suite.tests) {
|
|
6392
|
+
if (typeof entry === "string") {
|
|
6393
|
+
continue;
|
|
6394
|
+
}
|
|
6395
|
+
allMetadata.set(entry.uid, {
|
|
6396
|
+
title: entry.fullTitle,
|
|
6397
|
+
specFile: entry.file
|
|
6398
|
+
});
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
const tracePath = await writeSpecTrace({
|
|
6403
|
+
range,
|
|
6404
|
+
nextRange,
|
|
6405
|
+
capturer: this.sessionCapturer,
|
|
6406
|
+
actionSnapshots: this.sessionCapturer.actionSnapshots,
|
|
6407
|
+
sessionId,
|
|
6408
|
+
outputDir: resolveAdapterOutputDir({ configPath: this.#configPath }),
|
|
6409
|
+
format: this.options.traceFormat,
|
|
6410
|
+
testMetadata: allMetadata
|
|
6411
|
+
});
|
|
6412
|
+
log11.info(`Trace for spec "${range.specFile}" saved to ${tracePath}`);
|
|
6413
|
+
return tracePath;
|
|
6414
|
+
}
|
|
6190
6415
|
async #writeTraceZipIfNeeded() {
|
|
6191
6416
|
if (this.options.mode !== "trace" || !this.sessionCapturer) {
|
|
6192
6417
|
return;
|
|
@@ -6199,6 +6424,28 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6199
6424
|
if (this.sessionCapturer.snapshotCaptures.length) {
|
|
6200
6425
|
await Promise.allSettled(this.sessionCapturer.snapshotCaptures);
|
|
6201
6426
|
}
|
|
6427
|
+
if (this.options.traceGranularity === "spec") {
|
|
6428
|
+
for (const range of this.#specRanges) {
|
|
6429
|
+
if (!this.#flushedSpecs.has(range.specFile)) {
|
|
6430
|
+
await this.#flushSpecTrace(range);
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6433
|
+
return;
|
|
6434
|
+
}
|
|
6435
|
+
const testMetadata = /* @__PURE__ */ new Map();
|
|
6436
|
+
if (this.suiteManager) {
|
|
6437
|
+
for (const suite of this.suiteManager.getAllSuites().values()) {
|
|
6438
|
+
for (const entry of suite.tests) {
|
|
6439
|
+
if (typeof entry === "string") {
|
|
6440
|
+
continue;
|
|
6441
|
+
}
|
|
6442
|
+
testMetadata.set(entry.uid, {
|
|
6443
|
+
title: entry.fullTitle,
|
|
6444
|
+
specFile: entry.file
|
|
6445
|
+
});
|
|
6446
|
+
}
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6202
6449
|
const snapshots = this.sessionCapturer.actionSnapshots;
|
|
6203
6450
|
const tracePath = await writeTraceZip(this.sessionCapturer, {
|
|
6204
6451
|
outputDir: resolveAdapterOutputDir({
|
|
@@ -6206,7 +6453,8 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6206
6453
|
}),
|
|
6207
6454
|
sessionId,
|
|
6208
6455
|
actionSnapshots: snapshots.length ? snapshots : void 0,
|
|
6209
|
-
format: this.options.traceFormat
|
|
6456
|
+
format: this.options.traceFormat,
|
|
6457
|
+
testMetadata
|
|
6210
6458
|
});
|
|
6211
6459
|
log11.info(`Trace saved to ${tracePath}`);
|
|
6212
6460
|
} catch (err) {
|