@wdio/nightwatch-devtools 1.3.0 → 1.5.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 +372 -117
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/dist/bidi-OH4ZFDN5.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -17,7 +17,72 @@ import {
|
|
|
17
17
|
// src/index.ts
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
19
|
|
|
20
|
-
// ../
|
|
20
|
+
// ../shared/src/files.ts
|
|
21
|
+
var SPEC_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/i;
|
|
22
|
+
|
|
23
|
+
// ../shared/src/routes.ts
|
|
24
|
+
var WS_PATHS = {
|
|
25
|
+
/** Adapter session upgrade endpoint. One socket per running adapter. */
|
|
26
|
+
worker: "/worker",
|
|
27
|
+
/** App/UI client upgrade endpoint. Multiple browser tabs may connect. */
|
|
28
|
+
client: "/client"
|
|
29
|
+
};
|
|
30
|
+
var WS_SCOPE = {
|
|
31
|
+
/** Backend → worker: a dashboard client has subscribed. Wakes up the
|
|
32
|
+
* adapter's `await UI ready` gate before tests start. */
|
|
33
|
+
clientConnected: "clientConnected",
|
|
34
|
+
/** Backend → worker: the last dashboard tab closed. Triggers the
|
|
35
|
+
* interactive shutdown flow (close WS, exit, pkill the dashboard). */
|
|
36
|
+
clientDisconnected: "clientDisconnected",
|
|
37
|
+
/** Worker → backend → clients (or app-local): wipe the visualization data
|
|
38
|
+
* for a specific test/suite uid (or the whole tree). */
|
|
39
|
+
clearExecutionData: "clearExecutionData",
|
|
40
|
+
/** Worker → backend: clear-by-test-uid request (drives clearExecutionData). */
|
|
41
|
+
clearCommands: "clearCommands",
|
|
42
|
+
/** Backend → clients: signal that the active run was stopped by the UI. */
|
|
43
|
+
testStopped: "testStopped",
|
|
44
|
+
/** Worker → backend → clients: swap an earlier captured command for an
|
|
45
|
+
* updated entry (used when a retry coalesces commands). */
|
|
46
|
+
replaceCommand: "replaceCommand",
|
|
47
|
+
/** Worker → backend: register the config file path so reruns can spawn
|
|
48
|
+
* with the same config. */
|
|
49
|
+
config: "config"
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ../shared/src/runner.ts
|
|
53
|
+
var REUSE_ENV = {
|
|
54
|
+
REUSE: "DEVTOOLS_APP_REUSE",
|
|
55
|
+
HOST: "DEVTOOLS_APP_HOST",
|
|
56
|
+
PORT: "DEVTOOLS_APP_PORT",
|
|
57
|
+
RERUN_LABEL: "DEVTOOLS_RERUN_LABEL",
|
|
58
|
+
RERUN_ENTRY_TYPE: "DEVTOOLS_RERUN_ENTRY_TYPE"
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ../shared/src/timing.ts
|
|
62
|
+
var TIMING_BASE = {
|
|
63
|
+
/** ms to let the dashboard render between rapid state updates. */
|
|
64
|
+
UI_RENDER_DELAY: 150,
|
|
65
|
+
/** ms to wait after a test starts before flushing initial state. */
|
|
66
|
+
TEST_START_DELAY: 100,
|
|
67
|
+
/** ms gap between consecutive suite-finalize broadcasts. */
|
|
68
|
+
SUITE_COMPLETE_DELAY: 200,
|
|
69
|
+
/** ms allowed for the browser-close handshake before forcing teardown. */
|
|
70
|
+
BROWSER_CLOSE_WAIT: 2e3,
|
|
71
|
+
/** ms before first worker WS connection attempt — lets backend bind. */
|
|
72
|
+
INITIAL_CONNECTION_WAIT: 500,
|
|
73
|
+
/** ms between browser-window-alive polls. */
|
|
74
|
+
BROWSER_POLL_INTERVAL: 1e3
|
|
75
|
+
};
|
|
76
|
+
var DEFAULTS_BASE = {
|
|
77
|
+
/** Synthetic capability ID — present on every CommandLog / TestStats. */
|
|
78
|
+
CID: "0-0",
|
|
79
|
+
/** TestStats.retries default — adapters that surface retry counts override. */
|
|
80
|
+
RETRIES: 0,
|
|
81
|
+
/** _duration placeholder for not-yet-finalized stats. */
|
|
82
|
+
DURATION: 0
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ../shared/src/trace-actions.ts
|
|
21
86
|
var ACTION_MAP = {
|
|
22
87
|
// WDIO browser-level
|
|
23
88
|
url: { class: "Page", method: "navigate" },
|
|
@@ -48,6 +113,25 @@ var ACTION_MAP = {
|
|
|
48
113
|
switchToFrame: { class: "Frame", method: "goto" },
|
|
49
114
|
touchAction: { class: "Element", method: "tap" }
|
|
50
115
|
};
|
|
116
|
+
|
|
117
|
+
// ../shared/src/types.ts
|
|
118
|
+
var TEST_STATE = {
|
|
119
|
+
PENDING: "pending",
|
|
120
|
+
RUNNING: "running",
|
|
121
|
+
PASSED: "passed",
|
|
122
|
+
FAILED: "failed",
|
|
123
|
+
SKIPPED: "skipped"
|
|
124
|
+
};
|
|
125
|
+
var SCREENCAST_DEFAULTS = {
|
|
126
|
+
enabled: false,
|
|
127
|
+
captureFormat: "jpeg",
|
|
128
|
+
quality: 70,
|
|
129
|
+
maxWidth: 1280,
|
|
130
|
+
maxHeight: 720,
|
|
131
|
+
pollIntervalMs: 200
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ../core/src/action-mapping.ts
|
|
51
135
|
function mapCommandToAction(command) {
|
|
52
136
|
return ACTION_MAP[command] ?? null;
|
|
53
137
|
}
|
|
@@ -1206,21 +1290,8 @@ function serializeWebSnapshot(nodes, context, options = {}) {
|
|
|
1206
1290
|
}
|
|
1207
1291
|
const indent = " ".repeat(node.depth + 1);
|
|
1208
1292
|
const isInteractive = INTERACTIVE_ROLES.has(node.role);
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1211
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
1212
|
-
if (nodes[j].depth < node.depth) {
|
|
1213
|
-
const parentRole = nodes[j].role;
|
|
1214
|
-
const parentName = nodes[j].name;
|
|
1215
|
-
if (INTERACTIVE_ROLES.has(parentRole) && parentName && parentName.includes(node.name)) {
|
|
1216
|
-
echoedByParent = true;
|
|
1217
|
-
}
|
|
1218
|
-
break;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
if (echoedByParent) {
|
|
1222
|
-
continue;
|
|
1223
|
-
}
|
|
1293
|
+
if (isStatictextEchoedByParent(nodes, i)) {
|
|
1294
|
+
continue;
|
|
1224
1295
|
}
|
|
1225
1296
|
const roleLabel = node.role === "heading" && node.level ? `heading[${node.level}]` : node.role;
|
|
1226
1297
|
if (isInteractive) {
|
|
@@ -1245,6 +1316,22 @@ function serializeWebSnapshot(nodes, context, options = {}) {
|
|
|
1245
1316
|
}
|
|
1246
1317
|
return lines.join("\n");
|
|
1247
1318
|
}
|
|
1319
|
+
function isStatictextEchoedByParent(nodes, index) {
|
|
1320
|
+
const node = nodes[index];
|
|
1321
|
+
if (node.role !== "statictext" || !node.name) {
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
1325
|
+
if (nodes[j].depth < node.depth) {
|
|
1326
|
+
const parent = nodes[j];
|
|
1327
|
+
if (INTERACTIVE_ROLES.has(parent.role) && parent.name && parent.name.includes(node.name)) {
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1248
1335
|
function simplifyTag(tagName) {
|
|
1249
1336
|
const dot = tagName.lastIndexOf(".");
|
|
1250
1337
|
if (dot !== -1) {
|
|
@@ -1412,7 +1499,8 @@ function collectMobileNodes(element, platform, depth, nodes, walkOpts) {
|
|
|
1412
1499
|
depth,
|
|
1413
1500
|
isInteractive: false,
|
|
1414
1501
|
isExplicitInteractive: false,
|
|
1415
|
-
isInViewport: false
|
|
1502
|
+
isInViewport: false,
|
|
1503
|
+
tagName: element.tagName
|
|
1416
1504
|
});
|
|
1417
1505
|
return;
|
|
1418
1506
|
}
|
|
@@ -1445,7 +1533,8 @@ function collectMobileNodes(element, platform, depth, nodes, walkOpts) {
|
|
|
1445
1533
|
depth,
|
|
1446
1534
|
isInteractive: interactive,
|
|
1447
1535
|
isExplicitInteractive: explicit,
|
|
1448
|
-
isInViewport: inViewport
|
|
1536
|
+
isInViewport: inViewport,
|
|
1537
|
+
tagName: element.tagName
|
|
1449
1538
|
});
|
|
1450
1539
|
for (const child of element.children || []) {
|
|
1451
1540
|
collectMobileNodes(child, platform, depth + 1, nodes, walkOpts);
|
|
@@ -2060,15 +2149,46 @@ function buildContextOptions(trace, contextId, wallTime) {
|
|
|
2060
2149
|
}
|
|
2061
2150
|
};
|
|
2062
2151
|
}
|
|
2063
|
-
function buildActionEvents(commands, pageId, wallTime) {
|
|
2152
|
+
function buildActionEvents(commands, pageId, wallTime, testMetadata) {
|
|
2064
2153
|
const events = [];
|
|
2065
2154
|
let prevEndMs = 0;
|
|
2066
2155
|
let callCounter = 0;
|
|
2156
|
+
let lastTestUid;
|
|
2157
|
+
let groupCallId;
|
|
2067
2158
|
for (const cmd of commands) {
|
|
2068
2159
|
const action = mapCommandToAction(cmd.command);
|
|
2069
2160
|
if (!action) {
|
|
2070
2161
|
continue;
|
|
2071
2162
|
}
|
|
2163
|
+
if (cmd.testUid && cmd.testUid !== lastTestUid) {
|
|
2164
|
+
if (lastTestUid && groupCallId) {
|
|
2165
|
+
callCounter++;
|
|
2166
|
+
events.push({
|
|
2167
|
+
type: "after",
|
|
2168
|
+
callId: groupCallId,
|
|
2169
|
+
endTime: prevEndMs
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
callCounter++;
|
|
2173
|
+
groupCallId = `call@${callCounter}`;
|
|
2174
|
+
const meta = testMetadata?.get(cmd.testUid);
|
|
2175
|
+
const groupName = meta?.title ?? cmd.testUid;
|
|
2176
|
+
events.push({
|
|
2177
|
+
type: "before",
|
|
2178
|
+
callId: groupCallId,
|
|
2179
|
+
startTime: Math.max(
|
|
2180
|
+
prevEndMs,
|
|
2181
|
+
(cmd.startTime ?? cmd.timestamp) - wallTime
|
|
2182
|
+
),
|
|
2183
|
+
class: "Tracing",
|
|
2184
|
+
method: "tracingGroup",
|
|
2185
|
+
pageId,
|
|
2186
|
+
params: { name: groupName },
|
|
2187
|
+
title: groupName,
|
|
2188
|
+
apiName: "tracing.tracingGroup"
|
|
2189
|
+
});
|
|
2190
|
+
lastTestUid = cmd.testUid;
|
|
2191
|
+
}
|
|
2072
2192
|
callCounter++;
|
|
2073
2193
|
const callId = `call@${callCounter}`;
|
|
2074
2194
|
const rawStartMs = (cmd.startTime ?? cmd.timestamp) - wallTime;
|
|
@@ -2098,7 +2218,8 @@ function buildActionEvents(commands, pageId, wallTime) {
|
|
|
2098
2218
|
pageId,
|
|
2099
2219
|
params,
|
|
2100
2220
|
title: formatActionTitle(action, cmd.args, params),
|
|
2101
|
-
apiName: `${action.class.toLowerCase()}.${action.method}
|
|
2221
|
+
apiName: `${action.class.toLowerCase()}.${action.method}`,
|
|
2222
|
+
parentId: groupCallId
|
|
2102
2223
|
});
|
|
2103
2224
|
const afterEvent = {
|
|
2104
2225
|
type: "after",
|
|
@@ -2112,6 +2233,14 @@ function buildActionEvents(commands, pageId, wallTime) {
|
|
|
2112
2233
|
events.push(afterEvent);
|
|
2113
2234
|
prevEndMs = endMs;
|
|
2114
2235
|
}
|
|
2236
|
+
if (lastTestUid && groupCallId) {
|
|
2237
|
+
callCounter++;
|
|
2238
|
+
events.push({
|
|
2239
|
+
type: "after",
|
|
2240
|
+
callId: groupCallId,
|
|
2241
|
+
endTime: prevEndMs
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2115
2244
|
return events;
|
|
2116
2245
|
}
|
|
2117
2246
|
function buildNetworkNdjson(requests, wallTime, pageId) {
|
|
@@ -2268,7 +2397,7 @@ function buildTraceBundle(trace, opts = {}) {
|
|
|
2268
2397
|
wallTime,
|
|
2269
2398
|
viewport
|
|
2270
2399
|
),
|
|
2271
|
-
...buildActionEvents(trace.commands, pageId, wallTime)
|
|
2400
|
+
...buildActionEvents(trace.commands, pageId, wallTime, opts.testMetadata)
|
|
2272
2401
|
);
|
|
2273
2402
|
events.sort(compareEvents);
|
|
2274
2403
|
const ctxBName = ctxOptions.title;
|
|
@@ -2326,7 +2455,8 @@ async function writeTraceZip(capturer, opts) {
|
|
|
2326
2455
|
await fs.mkdir(opts.outputDir, { recursive: true });
|
|
2327
2456
|
const exportOpts = {
|
|
2328
2457
|
sessionId: opts.sessionId,
|
|
2329
|
-
wallTimeOverride: capturer.startWallTime
|
|
2458
|
+
wallTimeOverride: capturer.startWallTime,
|
|
2459
|
+
testMetadata: opts.testMetadata
|
|
2330
2460
|
};
|
|
2331
2461
|
if (opts.format === "ndjson-directory") {
|
|
2332
2462
|
const dir = path.join(opts.outputDir, `trace-${opts.sessionId}`);
|
|
@@ -2424,6 +2554,15 @@ function runFfmpeg(ffmpeg, manifestPath, outputPath) {
|
|
|
2424
2554
|
"cfr",
|
|
2425
2555
|
"-r",
|
|
2426
2556
|
"10",
|
|
2557
|
+
// Force a keyframe every frame. VP8 can only seek to keyframes, and
|
|
2558
|
+
// libvpx's default GOP places a single keyframe on near-static
|
|
2559
|
+
// screencast content — so scrubber/marker seeks snap back to the
|
|
2560
|
+
// start. At 10fps over short recordings, all-intra keeps seeking
|
|
2561
|
+
// frame-accurate at an acceptable size.
|
|
2562
|
+
"-g",
|
|
2563
|
+
"1",
|
|
2564
|
+
"-keyint_min",
|
|
2565
|
+
"1",
|
|
2427
2566
|
"-auto-alt-ref",
|
|
2428
2567
|
"0",
|
|
2429
2568
|
"-disposition:v",
|
|
@@ -2487,7 +2626,8 @@ async function finalizeScreencast({
|
|
|
2487
2626
|
videoPath,
|
|
2488
2627
|
videoFile: fileName,
|
|
2489
2628
|
frameCount: frames.length,
|
|
2490
|
-
duration: recorder.duration
|
|
2629
|
+
duration: recorder.duration,
|
|
2630
|
+
startTime: frames[0]?.timestamp
|
|
2491
2631
|
});
|
|
2492
2632
|
} catch (err) {
|
|
2493
2633
|
log12("warn", `Screencast encode failed: ${errorMessage(err)}`);
|
|
@@ -2644,88 +2784,6 @@ var RetryTracker = class {
|
|
|
2644
2784
|
}
|
|
2645
2785
|
};
|
|
2646
2786
|
|
|
2647
|
-
// ../shared/src/files.ts
|
|
2648
|
-
var SPEC_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/i;
|
|
2649
|
-
|
|
2650
|
-
// ../shared/src/routes.ts
|
|
2651
|
-
var WS_PATHS = {
|
|
2652
|
-
/** Adapter session upgrade endpoint. One socket per running adapter. */
|
|
2653
|
-
worker: "/worker",
|
|
2654
|
-
/** App/UI client upgrade endpoint. Multiple browser tabs may connect. */
|
|
2655
|
-
client: "/client"
|
|
2656
|
-
};
|
|
2657
|
-
var WS_SCOPE = {
|
|
2658
|
-
/** Backend → worker: a dashboard client has subscribed. Wakes up the
|
|
2659
|
-
* adapter's `await UI ready` gate before tests start. */
|
|
2660
|
-
clientConnected: "clientConnected",
|
|
2661
|
-
/** Backend → worker: the last dashboard tab closed. Triggers the
|
|
2662
|
-
* interactive shutdown flow (close WS, exit, pkill the dashboard). */
|
|
2663
|
-
clientDisconnected: "clientDisconnected",
|
|
2664
|
-
/** Worker → backend → clients (or app-local): wipe the visualization data
|
|
2665
|
-
* for a specific test/suite uid (or the whole tree). */
|
|
2666
|
-
clearExecutionData: "clearExecutionData",
|
|
2667
|
-
/** Worker → backend: clear-by-test-uid request (drives clearExecutionData). */
|
|
2668
|
-
clearCommands: "clearCommands",
|
|
2669
|
-
/** Backend → clients: signal that the active run was stopped by the UI. */
|
|
2670
|
-
testStopped: "testStopped",
|
|
2671
|
-
/** Worker → backend → clients: swap an earlier captured command for an
|
|
2672
|
-
* updated entry (used when a retry coalesces commands). */
|
|
2673
|
-
replaceCommand: "replaceCommand",
|
|
2674
|
-
/** Worker → backend: register the config file path so reruns can spawn
|
|
2675
|
-
* with the same config. */
|
|
2676
|
-
config: "config"
|
|
2677
|
-
};
|
|
2678
|
-
|
|
2679
|
-
// ../shared/src/runner.ts
|
|
2680
|
-
var REUSE_ENV = {
|
|
2681
|
-
REUSE: "DEVTOOLS_APP_REUSE",
|
|
2682
|
-
HOST: "DEVTOOLS_APP_HOST",
|
|
2683
|
-
PORT: "DEVTOOLS_APP_PORT",
|
|
2684
|
-
RERUN_LABEL: "DEVTOOLS_RERUN_LABEL",
|
|
2685
|
-
RERUN_ENTRY_TYPE: "DEVTOOLS_RERUN_ENTRY_TYPE"
|
|
2686
|
-
};
|
|
2687
|
-
|
|
2688
|
-
// ../shared/src/timing.ts
|
|
2689
|
-
var TIMING_BASE = {
|
|
2690
|
-
/** ms to let the dashboard render between rapid state updates. */
|
|
2691
|
-
UI_RENDER_DELAY: 150,
|
|
2692
|
-
/** ms to wait after a test starts before flushing initial state. */
|
|
2693
|
-
TEST_START_DELAY: 100,
|
|
2694
|
-
/** ms gap between consecutive suite-finalize broadcasts. */
|
|
2695
|
-
SUITE_COMPLETE_DELAY: 200,
|
|
2696
|
-
/** ms allowed for the browser-close handshake before forcing teardown. */
|
|
2697
|
-
BROWSER_CLOSE_WAIT: 2e3,
|
|
2698
|
-
/** ms before first worker WS connection attempt — lets backend bind. */
|
|
2699
|
-
INITIAL_CONNECTION_WAIT: 500,
|
|
2700
|
-
/** ms between browser-window-alive polls. */
|
|
2701
|
-
BROWSER_POLL_INTERVAL: 1e3
|
|
2702
|
-
};
|
|
2703
|
-
var DEFAULTS_BASE = {
|
|
2704
|
-
/** Synthetic capability ID — present on every CommandLog / TestStats. */
|
|
2705
|
-
CID: "0-0",
|
|
2706
|
-
/** TestStats.retries default — adapters that surface retry counts override. */
|
|
2707
|
-
RETRIES: 0,
|
|
2708
|
-
/** _duration placeholder for not-yet-finalized stats. */
|
|
2709
|
-
DURATION: 0
|
|
2710
|
-
};
|
|
2711
|
-
|
|
2712
|
-
// ../shared/src/types.ts
|
|
2713
|
-
var TEST_STATE = {
|
|
2714
|
-
PENDING: "pending",
|
|
2715
|
-
RUNNING: "running",
|
|
2716
|
-
PASSED: "passed",
|
|
2717
|
-
FAILED: "failed",
|
|
2718
|
-
SKIPPED: "skipped"
|
|
2719
|
-
};
|
|
2720
|
-
var SCREENCAST_DEFAULTS = {
|
|
2721
|
-
enabled: false,
|
|
2722
|
-
captureFormat: "jpeg",
|
|
2723
|
-
quality: 70,
|
|
2724
|
-
maxWidth: 1280,
|
|
2725
|
-
maxHeight: 720,
|
|
2726
|
-
pollIntervalMs: 200
|
|
2727
|
-
};
|
|
2728
|
-
|
|
2729
2787
|
// ../core/src/screencast.ts
|
|
2730
2788
|
var ScreencastRecorderBase = class {
|
|
2731
2789
|
buffer = [];
|
|
@@ -2951,9 +3009,12 @@ var SessionCapturerBase = class {
|
|
|
2951
3009
|
metadata;
|
|
2952
3010
|
// ── Construction ────────────────────────────────────────────────────────
|
|
2953
3011
|
constructor(opts = {}) {
|
|
2954
|
-
const { hostname, port } = opts;
|
|
3012
|
+
const { hostname, port, reconnect } = opts;
|
|
2955
3013
|
if (hostname && port) {
|
|
2956
|
-
|
|
3014
|
+
const query = reconnect ? "?reconnect=1" : "";
|
|
3015
|
+
this.ws = new WebSocket(
|
|
3016
|
+
`ws://${hostname}:${port}${WS_PATHS.worker}${query}`
|
|
3017
|
+
);
|
|
2957
3018
|
this.ws.on("open", () => {
|
|
2958
3019
|
this.#hasConnected = true;
|
|
2959
3020
|
this.onWsOpen();
|
|
@@ -3285,6 +3346,100 @@ function safeStringify(value) {
|
|
|
3285
3346
|
}
|
|
3286
3347
|
}
|
|
3287
3348
|
|
|
3349
|
+
// ../core/src/spec-trace-helpers.ts
|
|
3350
|
+
function sanitizeSpecName(specFile) {
|
|
3351
|
+
return specFile.replace(/^.*[/\\]/, "").replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_").replace(/^_+|_+$/g, "") || "unknown-spec";
|
|
3352
|
+
}
|
|
3353
|
+
function buildSpecSessionId(specFile, sessionId) {
|
|
3354
|
+
const base = sanitizeSpecName(specFile);
|
|
3355
|
+
const hash = deterministicUid(specFile).split("-").pop().slice(0, 8);
|
|
3356
|
+
return `${base}-${hash}-${sessionId.slice(0, 8)}`;
|
|
3357
|
+
}
|
|
3358
|
+
function buildSpecCapturer(capturer, range, nextRange) {
|
|
3359
|
+
const end = nextRange ? {
|
|
3360
|
+
commands: nextRange.commandStartIdx,
|
|
3361
|
+
console: nextRange.consoleStartIdx,
|
|
3362
|
+
network: nextRange.networkStartIdx,
|
|
3363
|
+
mutations: nextRange.mutationStartIdx,
|
|
3364
|
+
traceLogs: nextRange.traceLogStartIdx
|
|
3365
|
+
} : void 0;
|
|
3366
|
+
return {
|
|
3367
|
+
mutations: capturer.mutations.slice(range.mutationStartIdx, end?.mutations),
|
|
3368
|
+
traceLogs: capturer.traceLogs.slice(range.traceLogStartIdx, end?.traceLogs),
|
|
3369
|
+
consoleLogs: capturer.consoleLogs.slice(
|
|
3370
|
+
range.consoleStartIdx,
|
|
3371
|
+
end?.console
|
|
3372
|
+
),
|
|
3373
|
+
networkRequests: capturer.networkRequests.slice(
|
|
3374
|
+
range.networkStartIdx,
|
|
3375
|
+
end?.network
|
|
3376
|
+
),
|
|
3377
|
+
commandsLog: capturer.commandsLog.slice(
|
|
3378
|
+
range.commandStartIdx,
|
|
3379
|
+
end?.commands
|
|
3380
|
+
),
|
|
3381
|
+
sources: new Map(capturer.sources),
|
|
3382
|
+
metadata: capturer.metadata,
|
|
3383
|
+
startWallTime: capturer.startWallTime
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
function filterTestMetadataBySpec(allMetadata, specFile) {
|
|
3387
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
3388
|
+
for (const [uid, meta] of allMetadata) {
|
|
3389
|
+
if (meta.specFile === specFile) {
|
|
3390
|
+
filtered.set(uid, meta);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
return filtered;
|
|
3394
|
+
}
|
|
3395
|
+
function recordSpecBoundary(ctx, specFile, traceGranularity) {
|
|
3396
|
+
if (traceGranularity !== "spec") {
|
|
3397
|
+
return null;
|
|
3398
|
+
}
|
|
3399
|
+
const lastRange = ctx.specRanges[ctx.specRanges.length - 1];
|
|
3400
|
+
if (lastRange && lastRange.specFile === specFile) {
|
|
3401
|
+
return null;
|
|
3402
|
+
}
|
|
3403
|
+
const prevRange = lastRange && !ctx.flushedSpecs.has(lastRange.specFile) ? lastRange : null;
|
|
3404
|
+
ctx.specRanges.push({
|
|
3405
|
+
specFile,
|
|
3406
|
+
commandStartIdx: ctx.capturer.commandsLog.length,
|
|
3407
|
+
consoleStartIdx: ctx.capturer.consoleLogs.length,
|
|
3408
|
+
networkStartIdx: ctx.capturer.networkRequests.length,
|
|
3409
|
+
mutationStartIdx: ctx.capturer.mutations.length,
|
|
3410
|
+
traceLogStartIdx: ctx.capturer.traceLogs.length,
|
|
3411
|
+
snapshotCount: ctx.actionSnapshots.length
|
|
3412
|
+
});
|
|
3413
|
+
return prevRange;
|
|
3414
|
+
}
|
|
3415
|
+
async function writeSpecTrace(input) {
|
|
3416
|
+
const specCapturer = buildSpecCapturer(
|
|
3417
|
+
input.capturer,
|
|
3418
|
+
input.range,
|
|
3419
|
+
input.nextRange
|
|
3420
|
+
);
|
|
3421
|
+
const specSnapshots = input.actionSnapshots.slice(
|
|
3422
|
+
input.range.snapshotCount,
|
|
3423
|
+
input.nextRange?.snapshotCount ?? input.actionSnapshots.length
|
|
3424
|
+
);
|
|
3425
|
+
const specSessionId = buildSpecSessionId(
|
|
3426
|
+
input.range.specFile,
|
|
3427
|
+
input.sessionId
|
|
3428
|
+
);
|
|
3429
|
+
const testMetadata = filterTestMetadataBySpec(
|
|
3430
|
+
input.testMetadata,
|
|
3431
|
+
input.range.specFile
|
|
3432
|
+
);
|
|
3433
|
+
return writeTraceZip(specCapturer, {
|
|
3434
|
+
outputDir: input.outputDir,
|
|
3435
|
+
sessionId: specSessionId,
|
|
3436
|
+
capabilities: input.capabilities,
|
|
3437
|
+
actionSnapshots: specSnapshots.length > 0 ? specSnapshots : void 0,
|
|
3438
|
+
format: input.format,
|
|
3439
|
+
testMetadata
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3288
3443
|
// ../core/src/suite-helpers.ts
|
|
3289
3444
|
function createSuiteStats(init) {
|
|
3290
3445
|
return {
|
|
@@ -3824,6 +3979,13 @@ async function openDevtoolsBrowser(ctx, url) {
|
|
|
3824
3979
|
"--no-first-run",
|
|
3825
3980
|
"--no-default-browser-check"
|
|
3826
3981
|
]
|
|
3982
|
+
},
|
|
3983
|
+
// Dashboard uses the Puppeteer-based 'devtools' protocol; drop the
|
|
3984
|
+
// "controlled by automated test software" infobar by ignoring
|
|
3985
|
+
// Puppeteer's --enable-automation default. (Runtime-valid capability
|
|
3986
|
+
// not present in the type.)
|
|
3987
|
+
"wdio:devtoolsOptions": {
|
|
3988
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
3827
3989
|
}
|
|
3828
3990
|
}
|
|
3829
3991
|
});
|
|
@@ -4515,18 +4677,22 @@ function applyResponseReceived(p, response) {
|
|
|
4515
4677
|
p.mimeType = response.mimeType;
|
|
4516
4678
|
p.type = getRequestType(p.url, response.mimeType);
|
|
4517
4679
|
}
|
|
4680
|
+
function eventTimeMs(params, entry) {
|
|
4681
|
+
return typeof params.timestamp === "number" ? Math.round(params.timestamp * 1e3) : entry.timestamp;
|
|
4682
|
+
}
|
|
4518
4683
|
function startPendingRequest(params, entry, pending) {
|
|
4519
4684
|
const { requestId, request: req, timestamp } = params;
|
|
4520
4685
|
if (!req || typeof timestamp !== "number") {
|
|
4521
4686
|
return;
|
|
4522
4687
|
}
|
|
4688
|
+
const startTime = eventTimeMs(params, entry);
|
|
4523
4689
|
pending.set(requestId, {
|
|
4524
4690
|
id: `${entry.timestamp}-${requestId}`,
|
|
4525
4691
|
url: req.url,
|
|
4526
4692
|
method: req.method,
|
|
4527
4693
|
requestHeaders: req.headers ?? {},
|
|
4528
|
-
timestamp:
|
|
4529
|
-
startTime
|
|
4694
|
+
timestamp: startTime,
|
|
4695
|
+
startTime
|
|
4530
4696
|
});
|
|
4531
4697
|
}
|
|
4532
4698
|
function handlePerfLogEvent(method, params, entry, pending, completed) {
|
|
@@ -4545,8 +4711,8 @@ function handlePerfLogEvent(method, params, entry, pending, completed) {
|
|
|
4545
4711
|
const p = pending.get(params.requestId);
|
|
4546
4712
|
if (p && p.status !== void 0) {
|
|
4547
4713
|
p.size = params.encodedDataLength;
|
|
4548
|
-
p.endTime = entry
|
|
4549
|
-
p.time =
|
|
4714
|
+
p.endTime = eventTimeMs(params, entry);
|
|
4715
|
+
p.time = Math.max(0, p.endTime - p.startTime);
|
|
4550
4716
|
completed.push({ ...p });
|
|
4551
4717
|
pending.delete(params.requestId);
|
|
4552
4718
|
}
|
|
@@ -4556,8 +4722,8 @@ function handlePerfLogEvent(method, params, entry, pending, completed) {
|
|
|
4556
4722
|
const p = pending.get(params.requestId);
|
|
4557
4723
|
if (p) {
|
|
4558
4724
|
p.error = params.errorText;
|
|
4559
|
-
p.endTime = entry
|
|
4560
|
-
p.time =
|
|
4725
|
+
p.endTime = eventTimeMs(params, entry);
|
|
4726
|
+
p.time = Math.max(0, p.endTime - p.startTime);
|
|
4561
4727
|
completed.push({ ...p });
|
|
4562
4728
|
pending.delete(params.requestId);
|
|
4563
4729
|
}
|
|
@@ -5754,7 +5920,7 @@ async function tryAttachBidi(ctx, browser) {
|
|
|
5754
5920
|
log10.warn("bidi:true set but browser.driver unavailable \u2014 skipping");
|
|
5755
5921
|
return;
|
|
5756
5922
|
}
|
|
5757
|
-
const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-
|
|
5923
|
+
const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-FIHBBGT4.js");
|
|
5758
5924
|
const ok = await attachBidiHandlers(
|
|
5759
5925
|
driver,
|
|
5760
5926
|
buildBidiSinks(ctx.sessionCapturer)
|
|
@@ -5790,7 +5956,13 @@ async function ensureSessionInitialized(ctx, browser, finalizeCurrentScreencast2
|
|
|
5790
5956
|
(resolve2) => setTimeout(resolve2, TIMING.INITIAL_CONNECTION_WAIT)
|
|
5791
5957
|
);
|
|
5792
5958
|
ctx.sessionCapturer = new SessionCapturer(
|
|
5793
|
-
ctx.mode === "trace" ? {} : {
|
|
5959
|
+
ctx.mode === "trace" ? {} : {
|
|
5960
|
+
port: ctx.port,
|
|
5961
|
+
hostname: ctx.hostname,
|
|
5962
|
+
// A session change reopens the WS mid-run — tell the backend to keep
|
|
5963
|
+
// the accumulated run state so earlier tests' commands survive.
|
|
5964
|
+
reconnect: Boolean(isSessionChange)
|
|
5965
|
+
},
|
|
5794
5966
|
browser
|
|
5795
5967
|
);
|
|
5796
5968
|
ctx.sessionCapturer.traceMode = ctx.mode;
|
|
@@ -5851,6 +6023,10 @@ var NightwatchDevToolsPlugin = class {
|
|
|
5851
6023
|
#skipCount = 0;
|
|
5852
6024
|
#configPath;
|
|
5853
6025
|
#srcFolders = [];
|
|
6026
|
+
/** Index ranges into the session capturer's flat arrays, one per spec file. */
|
|
6027
|
+
#specRanges = [];
|
|
6028
|
+
/** Set of spec files already flushed to disk. */
|
|
6029
|
+
#flushedSpecs = /* @__PURE__ */ new Set();
|
|
5854
6030
|
#getRerunLabel() {
|
|
5855
6031
|
return process.env[REUSE_ENV.RERUN_ENTRY_TYPE] === "test" ? process.env[REUSE_ENV.RERUN_LABEL]?.trim() : void 0;
|
|
5856
6032
|
}
|
|
@@ -5872,7 +6048,8 @@ var NightwatchDevToolsPlugin = class {
|
|
|
5872
6048
|
screencast,
|
|
5873
6049
|
bidi: options.bidi ?? false,
|
|
5874
6050
|
mode,
|
|
5875
|
-
traceFormat: options.traceFormat ?? "zip"
|
|
6051
|
+
traceFormat: options.traceFormat ?? "zip",
|
|
6052
|
+
traceGranularity: options.traceGranularity ?? "session"
|
|
5876
6053
|
};
|
|
5877
6054
|
this.#screencastOptions = { ...SCREENCAST_DEFAULTS, ...screencast };
|
|
5878
6055
|
this.#bidiEnabled = options.bidi === true;
|
|
@@ -6130,6 +6307,25 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6130
6307
|
this.sessionCapturer.captureSource(fullPath).catch(() => {
|
|
6131
6308
|
});
|
|
6132
6309
|
}
|
|
6310
|
+
if (fullPath) {
|
|
6311
|
+
const prevRange = recordSpecBoundary(
|
|
6312
|
+
{
|
|
6313
|
+
specRanges: this.#specRanges,
|
|
6314
|
+
flushedSpecs: this.#flushedSpecs,
|
|
6315
|
+
capturer: this.sessionCapturer,
|
|
6316
|
+
actionSnapshots: this.sessionCapturer.actionSnapshots
|
|
6317
|
+
},
|
|
6318
|
+
fullPath,
|
|
6319
|
+
this.options.traceGranularity
|
|
6320
|
+
);
|
|
6321
|
+
if (prevRange) {
|
|
6322
|
+
void this.#flushSpecTrace(prevRange).catch(
|
|
6323
|
+
(err) => log11.warn(
|
|
6324
|
+
`Failed to flush trace for spec "${prevRange.specFile}": ${errorMessage(err)}`
|
|
6325
|
+
)
|
|
6326
|
+
);
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6133
6329
|
await this.#closePreviousRunningTest(currentSuite, testFile, currentTest);
|
|
6134
6330
|
const processedTests = this.testManager.getProcessedTests(testFile);
|
|
6135
6331
|
const currentTestName = this.#pickCurrentTestName(
|
|
@@ -6187,6 +6383,42 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6187
6383
|
#logRunSummary() {
|
|
6188
6384
|
logRunSummary(this.#getInternals());
|
|
6189
6385
|
}
|
|
6386
|
+
async #flushSpecTrace(range, nextRange) {
|
|
6387
|
+
if (this.#flushedSpecs.has(range.specFile)) {
|
|
6388
|
+
return void 0;
|
|
6389
|
+
}
|
|
6390
|
+
this.#flushedSpecs.add(range.specFile);
|
|
6391
|
+
const sessionId = this.sessionCapturer.metadata?.sessionId;
|
|
6392
|
+
if (!sessionId) {
|
|
6393
|
+
return void 0;
|
|
6394
|
+
}
|
|
6395
|
+
const allMetadata = /* @__PURE__ */ new Map();
|
|
6396
|
+
if (this.suiteManager) {
|
|
6397
|
+
for (const suite of this.suiteManager.getAllSuites().values()) {
|
|
6398
|
+
for (const entry of suite.tests) {
|
|
6399
|
+
if (typeof entry === "string") {
|
|
6400
|
+
continue;
|
|
6401
|
+
}
|
|
6402
|
+
allMetadata.set(entry.uid, {
|
|
6403
|
+
title: entry.fullTitle,
|
|
6404
|
+
specFile: entry.file
|
|
6405
|
+
});
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6409
|
+
const tracePath = await writeSpecTrace({
|
|
6410
|
+
range,
|
|
6411
|
+
nextRange,
|
|
6412
|
+
capturer: this.sessionCapturer,
|
|
6413
|
+
actionSnapshots: this.sessionCapturer.actionSnapshots,
|
|
6414
|
+
sessionId,
|
|
6415
|
+
outputDir: resolveAdapterOutputDir({ configPath: this.#configPath }),
|
|
6416
|
+
format: this.options.traceFormat,
|
|
6417
|
+
testMetadata: allMetadata
|
|
6418
|
+
});
|
|
6419
|
+
log11.info(`Trace for spec "${range.specFile}" saved to ${tracePath}`);
|
|
6420
|
+
return tracePath;
|
|
6421
|
+
}
|
|
6190
6422
|
async #writeTraceZipIfNeeded() {
|
|
6191
6423
|
if (this.options.mode !== "trace" || !this.sessionCapturer) {
|
|
6192
6424
|
return;
|
|
@@ -6199,6 +6431,28 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6199
6431
|
if (this.sessionCapturer.snapshotCaptures.length) {
|
|
6200
6432
|
await Promise.allSettled(this.sessionCapturer.snapshotCaptures);
|
|
6201
6433
|
}
|
|
6434
|
+
if (this.options.traceGranularity === "spec") {
|
|
6435
|
+
for (const range of this.#specRanges) {
|
|
6436
|
+
if (!this.#flushedSpecs.has(range.specFile)) {
|
|
6437
|
+
await this.#flushSpecTrace(range);
|
|
6438
|
+
}
|
|
6439
|
+
}
|
|
6440
|
+
return;
|
|
6441
|
+
}
|
|
6442
|
+
const testMetadata = /* @__PURE__ */ new Map();
|
|
6443
|
+
if (this.suiteManager) {
|
|
6444
|
+
for (const suite of this.suiteManager.getAllSuites().values()) {
|
|
6445
|
+
for (const entry of suite.tests) {
|
|
6446
|
+
if (typeof entry === "string") {
|
|
6447
|
+
continue;
|
|
6448
|
+
}
|
|
6449
|
+
testMetadata.set(entry.uid, {
|
|
6450
|
+
title: entry.fullTitle,
|
|
6451
|
+
specFile: entry.file
|
|
6452
|
+
});
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6202
6456
|
const snapshots = this.sessionCapturer.actionSnapshots;
|
|
6203
6457
|
const tracePath = await writeTraceZip(this.sessionCapturer, {
|
|
6204
6458
|
outputDir: resolveAdapterOutputDir({
|
|
@@ -6206,7 +6460,8 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6206
6460
|
}),
|
|
6207
6461
|
sessionId,
|
|
6208
6462
|
actionSnapshots: snapshots.length ? snapshots : void 0,
|
|
6209
|
-
format: this.options.traceFormat
|
|
6463
|
+
format: this.options.traceFormat,
|
|
6464
|
+
testMetadata
|
|
6210
6465
|
});
|
|
6211
6466
|
log11.info(`Trace saved to ${tracePath}`);
|
|
6212
6467
|
} catch (err) {
|