@wdio/nightwatch-devtools 1.5.1 → 1.6.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 +129 -35
- package/dist/{bidi-FIHBBGT4.js → bidi-5ALZJJNC.js} +2 -2
- package/dist/{chunk-DKSBYMPM.js → chunk-DNCL4MP7.js} +1 -1
- package/dist/chunk-DNCL4MP7.js.map +1 -0
- package/dist/index.d.ts +42 -44
- package/dist/index.js +3013 -739
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/dist/chunk-DKSBYMPM.js.map +0 -1
- /package/dist/{bidi-FIHBBGT4.js.map → bidi-5ALZJJNC.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
serializeError,
|
|
13
13
|
stripAnsi,
|
|
14
14
|
toError
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-DNCL4MP7.js";
|
|
16
16
|
|
|
17
17
|
// src/index.ts
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
@@ -58,6 +58,12 @@ var REUSE_ENV = {
|
|
|
58
58
|
RERUN_ENTRY_TYPE: "DEVTOOLS_RERUN_ENTRY_TYPE"
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
// ../shared/src/snapshot-format.ts
|
|
62
|
+
var SNAPSHOT_INDENT_UNIT = " ";
|
|
63
|
+
var SNAPSHOT_PAGE_HEADER = "[Page";
|
|
64
|
+
var SNAPSHOT_LOCATOR_DELIM = "\u2192";
|
|
65
|
+
var SNAPSHOT_PURPOSE_TOKEN = "\u2208";
|
|
66
|
+
|
|
61
67
|
// ../shared/src/timing.ts
|
|
62
68
|
var TIMING_BASE = {
|
|
63
69
|
/** ms to let the dashboard render between rapid state updates. */
|
|
@@ -83,6 +89,30 @@ var DEFAULTS_BASE = {
|
|
|
83
89
|
};
|
|
84
90
|
|
|
85
91
|
// ../shared/src/trace-actions.ts
|
|
92
|
+
var ASSERT_ACTION_CLASS = "Assert";
|
|
93
|
+
var TRACKED_ASSERT_METHODS = [
|
|
94
|
+
"equal",
|
|
95
|
+
"strictEqual",
|
|
96
|
+
"deepEqual",
|
|
97
|
+
"deepStrictEqual",
|
|
98
|
+
"notEqual",
|
|
99
|
+
"notStrictEqual",
|
|
100
|
+
"notDeepEqual",
|
|
101
|
+
"notDeepStrictEqual",
|
|
102
|
+
"ok",
|
|
103
|
+
"fail",
|
|
104
|
+
"throws",
|
|
105
|
+
"doesNotThrow",
|
|
106
|
+
"rejects",
|
|
107
|
+
"doesNotReject",
|
|
108
|
+
"match",
|
|
109
|
+
"doesNotMatch"
|
|
110
|
+
];
|
|
111
|
+
var ASSERT_COMMAND_RE = /^(?:assert|verify|expect)\.(\w+)$/;
|
|
112
|
+
function mapAssertCommand(command) {
|
|
113
|
+
const match = ASSERT_COMMAND_RE.exec(command);
|
|
114
|
+
return match ? { class: ASSERT_ACTION_CLASS, method: match[1] } : null;
|
|
115
|
+
}
|
|
86
116
|
var ACTION_MAP = {
|
|
87
117
|
// WDIO browser-level
|
|
88
118
|
url: { class: "Page", method: "navigate" },
|
|
@@ -111,8 +141,51 @@ var ACTION_MAP = {
|
|
|
111
141
|
execute: { class: "Page", method: "evaluate" },
|
|
112
142
|
executeAsync: { class: "Page", method: "evaluate" },
|
|
113
143
|
switchToFrame: { class: "Frame", method: "goto" },
|
|
114
|
-
touchAction: { class: "Element", method: "tap" }
|
|
144
|
+
touchAction: { class: "Element", method: "tap" },
|
|
145
|
+
// WDIO element reads — surfaced so query steps appear in the timeline the way
|
|
146
|
+
// locator queries do in standard trace viewers. Adapters already capture
|
|
147
|
+
// these; only the export allow-list kept them out.
|
|
148
|
+
getText: { class: "Element", method: "getText" },
|
|
149
|
+
getValue: { class: "Element", method: "getValue" },
|
|
150
|
+
getAttribute: { class: "Element", method: "getAttribute" },
|
|
151
|
+
getProperty: { class: "Element", method: "getProperty" },
|
|
152
|
+
getCSSProperty: { class: "Element", method: "getCSSProperty" },
|
|
153
|
+
getTagName: { class: "Element", method: "getTagName" },
|
|
154
|
+
getLocation: { class: "Element", method: "getLocation" },
|
|
155
|
+
getSize: { class: "Element", method: "getSize" },
|
|
156
|
+
isDisplayed: { class: "Element", method: "isDisplayed" },
|
|
157
|
+
isExisting: { class: "Element", method: "isExisting" },
|
|
158
|
+
isEnabled: { class: "Element", method: "isEnabled" },
|
|
159
|
+
isSelected: { class: "Element", method: "isSelected" },
|
|
160
|
+
isClickable: { class: "Element", method: "isClickable" },
|
|
161
|
+
isFocused: { class: "Element", method: "isFocused" },
|
|
162
|
+
// Explicit user-facing waits (not the internal polling loops behind them).
|
|
163
|
+
waitForDisplayed: { class: "Element", method: "waitForDisplayed" },
|
|
164
|
+
waitForExist: { class: "Element", method: "waitForExist" },
|
|
165
|
+
waitForEnabled: { class: "Element", method: "waitForEnabled" },
|
|
166
|
+
waitForClickable: { class: "Element", method: "waitForClickable" },
|
|
167
|
+
waitUntil: { class: "Browser", method: "waitForFunction" },
|
|
168
|
+
// WDIO page/browser reads
|
|
169
|
+
getTitle: { class: "Page", method: "getTitle" },
|
|
170
|
+
getUrl: { class: "Page", method: "getUrl" },
|
|
171
|
+
getPageSource: { class: "Page", method: "getPageSource" },
|
|
172
|
+
// Selenium read aliases — normalized onto the WDIO names above so both runners
|
|
173
|
+
// read identically. getText/getAttribute/getTagName/isDisplayed/isEnabled/
|
|
174
|
+
// isSelected share the command name across runners and need no alias.
|
|
175
|
+
getCssValue: { class: "Element", method: "getCSSProperty" },
|
|
176
|
+
getRect: { class: "Element", method: "getRect" },
|
|
177
|
+
getCurrentUrl: { class: "Page", method: "getUrl" }
|
|
115
178
|
};
|
|
179
|
+
var POINTABLE_METHODS = /* @__PURE__ */ new Set([
|
|
180
|
+
"click",
|
|
181
|
+
"dblclick",
|
|
182
|
+
"hover",
|
|
183
|
+
"tap",
|
|
184
|
+
"dragTo",
|
|
185
|
+
"scrollIntoViewIfNeeded",
|
|
186
|
+
"selectOption",
|
|
187
|
+
"fill"
|
|
188
|
+
]);
|
|
116
189
|
|
|
117
190
|
// ../shared/src/types.ts
|
|
118
191
|
var TEST_STATE = {
|
|
@@ -128,14 +201,37 @@ var SCREENCAST_DEFAULTS = {
|
|
|
128
201
|
quality: 70,
|
|
129
202
|
maxWidth: 1280,
|
|
130
203
|
maxHeight: 720,
|
|
131
|
-
pollIntervalMs: 200
|
|
204
|
+
pollIntervalMs: 200,
|
|
205
|
+
maxBufferFrames: 2e3
|
|
132
206
|
};
|
|
133
207
|
|
|
134
208
|
// ../core/src/action-mapping.ts
|
|
135
209
|
function mapCommandToAction(command) {
|
|
136
|
-
return ACTION_MAP[command] ??
|
|
210
|
+
return ACTION_MAP[command] ?? mapAssertCommand(command);
|
|
211
|
+
}
|
|
212
|
+
var ASSERT_TITLE_VALUE_MAX = 40;
|
|
213
|
+
function formatAssertValue(value) {
|
|
214
|
+
let text;
|
|
215
|
+
if (typeof value === "object" && value !== null) {
|
|
216
|
+
try {
|
|
217
|
+
text = JSON.stringify(value);
|
|
218
|
+
} catch {
|
|
219
|
+
text = String(value);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
text = typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
223
|
+
}
|
|
224
|
+
return text.length > ASSERT_TITLE_VALUE_MAX ? `${text.slice(0, ASSERT_TITLE_VALUE_MAX - 1)}\u2026` : text;
|
|
137
225
|
}
|
|
138
|
-
function
|
|
226
|
+
function formatAssertTitle(action, args, params, command) {
|
|
227
|
+
const values = args.length > 0 ? args.slice(0, 2) : [params?.actual, params?.expected];
|
|
228
|
+
const label = values.filter((value) => value !== void 0).map(formatAssertValue).join(", ");
|
|
229
|
+
return `${command ?? `assert.${action.method}`}(${label})`;
|
|
230
|
+
}
|
|
231
|
+
function formatActionTitle(action, args, params, command) {
|
|
232
|
+
if (action.class === ASSERT_ACTION_CLASS) {
|
|
233
|
+
return formatAssertTitle(action, args, params, command);
|
|
234
|
+
}
|
|
139
235
|
const firstArg = args[0] ?? params?.selector;
|
|
140
236
|
if (firstArg === void 0) {
|
|
141
237
|
return `${action.class}.${action.method}()`;
|
|
@@ -521,14 +617,14 @@ function translateRecursively(domNode, parentPath = "", index = null) {
|
|
|
521
617
|
}
|
|
522
618
|
}
|
|
523
619
|
}
|
|
524
|
-
const
|
|
620
|
+
const path14 = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
|
|
525
621
|
return {
|
|
526
622
|
children: childNodesOf(domNode).map(
|
|
527
|
-
(childNode, childIndex) => translateRecursively(childNode,
|
|
623
|
+
(childNode, childIndex) => translateRecursively(childNode, path14, childIndex)
|
|
528
624
|
),
|
|
529
625
|
tagName: domNode.nodeName,
|
|
530
626
|
attributes,
|
|
531
|
-
path:
|
|
627
|
+
path: path14
|
|
532
628
|
};
|
|
533
629
|
}
|
|
534
630
|
function isSameElement(node1, node2) {
|
|
@@ -665,11 +761,11 @@ function checkXPathUniqueness(doc, xpathExpr, targetNode) {
|
|
|
665
761
|
return { isUnique: false };
|
|
666
762
|
}
|
|
667
763
|
}
|
|
668
|
-
function findDOMNodeByPath(doc,
|
|
669
|
-
if (!
|
|
764
|
+
function findDOMNodeByPath(doc, path14) {
|
|
765
|
+
if (!path14) {
|
|
670
766
|
return doc.documentElement;
|
|
671
767
|
}
|
|
672
|
-
const indices =
|
|
768
|
+
const indices = path14.split(".").map(Number);
|
|
673
769
|
let current = doc.documentElement;
|
|
674
770
|
for (const index of indices) {
|
|
675
771
|
if (!current) {
|
|
@@ -1310,7 +1406,7 @@ function inferPurpose(nodes, index) {
|
|
|
1310
1406
|
}
|
|
1311
1407
|
function serializeWebSnapshot(nodes, context, options = {}) {
|
|
1312
1408
|
const { inViewportOnly = true } = options;
|
|
1313
|
-
let header =
|
|
1409
|
+
let header = SNAPSHOT_PAGE_HEADER;
|
|
1314
1410
|
if (context?.title) {
|
|
1315
1411
|
header += `: ${context.title}`;
|
|
1316
1412
|
}
|
|
@@ -1324,7 +1420,7 @@ function serializeWebSnapshot(nodes, context, options = {}) {
|
|
|
1324
1420
|
if (inViewportOnly && node.isInViewport === false) {
|
|
1325
1421
|
continue;
|
|
1326
1422
|
}
|
|
1327
|
-
const indent =
|
|
1423
|
+
const indent = SNAPSHOT_INDENT_UNIT.repeat(node.depth + 1);
|
|
1328
1424
|
const isInteractive = INTERACTIVE_ROLES.has(node.role);
|
|
1329
1425
|
if (isStatictextEchoedByParent(nodes, i)) {
|
|
1330
1426
|
continue;
|
|
@@ -1337,12 +1433,16 @@ function serializeWebSnapshot(nodes, context, options = {}) {
|
|
|
1337
1433
|
const purpose = inferPurpose(nodes, i);
|
|
1338
1434
|
if (node.name) {
|
|
1339
1435
|
lines.push(
|
|
1340
|
-
purpose ? `${indent}${roleLabel} "${node.name}"
|
|
1436
|
+
purpose ? `${indent}${roleLabel} "${node.name}" ${SNAPSHOT_PURPOSE_TOKEN} "${purpose}" ${SNAPSHOT_LOCATOR_DELIM} ${node.selector}` : `${indent}${roleLabel} "${node.name}" ${SNAPSHOT_LOCATOR_DELIM} ${node.selector}`
|
|
1341
1437
|
);
|
|
1342
1438
|
} else if (purpose) {
|
|
1343
|
-
lines.push(
|
|
1439
|
+
lines.push(
|
|
1440
|
+
`${indent}${roleLabel} ${SNAPSHOT_PURPOSE_TOKEN} "${purpose}" ${SNAPSHOT_LOCATOR_DELIM} ${node.selector}`
|
|
1441
|
+
);
|
|
1344
1442
|
} else {
|
|
1345
|
-
lines.push(
|
|
1443
|
+
lines.push(
|
|
1444
|
+
`${indent}${roleLabel} ${SNAPSHOT_LOCATOR_DELIM} ${node.selector}`
|
|
1445
|
+
);
|
|
1346
1446
|
}
|
|
1347
1447
|
} else {
|
|
1348
1448
|
lines.push(
|
|
@@ -1660,7 +1760,7 @@ function renderMobileNodes(nodes) {
|
|
|
1660
1760
|
const selectorIndex = /* @__PURE__ */ new Map();
|
|
1661
1761
|
for (let i = 0; i < nodes.length; i++) {
|
|
1662
1762
|
const node = nodes[i];
|
|
1663
|
-
const indent =
|
|
1763
|
+
const indent = SNAPSHOT_INDENT_UNIT.repeat(node.depth + 1);
|
|
1664
1764
|
if (NOISY_ROLES.has(node.role) && !node.name && node.depth > 1 && !node.isInteractive) {
|
|
1665
1765
|
continue;
|
|
1666
1766
|
}
|
|
@@ -1693,12 +1793,16 @@ function renderMobileNodes(nodes) {
|
|
|
1693
1793
|
const purpose = mobileInferPurpose(nodes, i);
|
|
1694
1794
|
if (node.name) {
|
|
1695
1795
|
lines.push(
|
|
1696
|
-
purpose ? `${indent}${node.role} "${node.name}"
|
|
1796
|
+
purpose ? `${indent}${node.role} "${node.name}" ${SNAPSHOT_PURPOSE_TOKEN} "${purpose}" ${SNAPSHOT_LOCATOR_DELIM} ${selector}` : `${indent}${node.role} "${node.name}" ${SNAPSHOT_LOCATOR_DELIM} ${selector}`
|
|
1697
1797
|
);
|
|
1698
1798
|
} else if (purpose) {
|
|
1699
|
-
lines.push(
|
|
1799
|
+
lines.push(
|
|
1800
|
+
`${indent}${node.role} ${SNAPSHOT_PURPOSE_TOKEN} "${purpose}" ${SNAPSHOT_LOCATOR_DELIM} ${selector}`
|
|
1801
|
+
);
|
|
1700
1802
|
} else {
|
|
1701
|
-
lines.push(
|
|
1803
|
+
lines.push(
|
|
1804
|
+
`${indent}${node.role} ${SNAPSHOT_LOCATOR_DELIM} ${selector}`
|
|
1805
|
+
);
|
|
1702
1806
|
}
|
|
1703
1807
|
} else {
|
|
1704
1808
|
lines.push(
|
|
@@ -2003,7 +2107,8 @@ async function captureActionSnapshot(input) {
|
|
|
2003
2107
|
),
|
|
2004
2108
|
runWith(
|
|
2005
2109
|
input.runScript,
|
|
2006
|
-
|
|
2110
|
+
// includeBounds: the per-action element rects drive A8 input points.
|
|
2111
|
+
elementsScript(true, true),
|
|
2007
2112
|
[]
|
|
2008
2113
|
)
|
|
2009
2114
|
]);
|
|
@@ -2050,6 +2155,443 @@ async function captureActionSnapshot(input) {
|
|
|
2050
2155
|
}
|
|
2051
2156
|
}
|
|
2052
2157
|
|
|
2158
|
+
// ../core/src/artifact-naming.ts
|
|
2159
|
+
function trimChar(value, char) {
|
|
2160
|
+
let start2 = 0;
|
|
2161
|
+
let end = value.length;
|
|
2162
|
+
while (start2 < end && value[start2] === char) {
|
|
2163
|
+
start2++;
|
|
2164
|
+
}
|
|
2165
|
+
while (end > start2 && value[end - 1] === char) {
|
|
2166
|
+
end--;
|
|
2167
|
+
}
|
|
2168
|
+
return value.slice(start2, end);
|
|
2169
|
+
}
|
|
2170
|
+
function fileSlug(value) {
|
|
2171
|
+
return trimChar(value.replace(/[^a-zA-Z0-9_-]+/g, "-"), "-");
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// ../core/src/artifacts-manifest.ts
|
|
2175
|
+
import fs from "fs/promises";
|
|
2176
|
+
import path from "path";
|
|
2177
|
+
function artifactsManifestFilename(sessionId) {
|
|
2178
|
+
return `devtools-artifacts-${sessionId}.json`;
|
|
2179
|
+
}
|
|
2180
|
+
function buildArtifactsManifest(input) {
|
|
2181
|
+
const tests = [];
|
|
2182
|
+
for (const [uid, meta] of input.testMetadata) {
|
|
2183
|
+
tests.push({
|
|
2184
|
+
uid,
|
|
2185
|
+
title: meta.title,
|
|
2186
|
+
specFile: meta.specFile,
|
|
2187
|
+
state: meta.state,
|
|
2188
|
+
attempt: meta.attempt
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
return {
|
|
2192
|
+
sessionId: input.sessionId,
|
|
2193
|
+
format: input.format,
|
|
2194
|
+
artifacts: [...input.artifacts],
|
|
2195
|
+
tests
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
async function writeArtifactsManifest(outputDir, manifest) {
|
|
2199
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
2200
|
+
const filePath = path.join(
|
|
2201
|
+
outputDir,
|
|
2202
|
+
artifactsManifestFilename(manifest.sessionId)
|
|
2203
|
+
);
|
|
2204
|
+
await fs.writeFile(filePath, JSON.stringify(manifest, null, 2), "utf8");
|
|
2205
|
+
return filePath;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// ../core/src/allure-artifacts.ts
|
|
2209
|
+
import fs5 from "fs/promises";
|
|
2210
|
+
import { basename } from "path";
|
|
2211
|
+
|
|
2212
|
+
// ../core/src/screenshot-artifact.ts
|
|
2213
|
+
import fs2 from "fs/promises";
|
|
2214
|
+
import path2 from "path";
|
|
2215
|
+
function shouldCaptureScreenshot(policy, failed) {
|
|
2216
|
+
if (policy === "on") {
|
|
2217
|
+
return true;
|
|
2218
|
+
}
|
|
2219
|
+
if (policy === "only-on-failure") {
|
|
2220
|
+
return failed;
|
|
2221
|
+
}
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
async function writeScreenshotArtifact(input) {
|
|
2225
|
+
await fs2.mkdir(input.outputDir, { recursive: true });
|
|
2226
|
+
const filename = `screenshot-${fileSlug(input.testUid)}-${input.sessionId.slice(0, 8)}.png`;
|
|
2227
|
+
const filePath = path2.join(input.outputDir, filename);
|
|
2228
|
+
await fs2.writeFile(filePath, Buffer.from(input.base64, "base64"));
|
|
2229
|
+
return {
|
|
2230
|
+
kind: "screenshot",
|
|
2231
|
+
path: filePath,
|
|
2232
|
+
scope: "test",
|
|
2233
|
+
key: input.testUid,
|
|
2234
|
+
testUids: [input.testUid],
|
|
2235
|
+
retained: true
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// ../core/src/video-slice.ts
|
|
2240
|
+
import fs4 from "fs/promises";
|
|
2241
|
+
import path4 from "path";
|
|
2242
|
+
|
|
2243
|
+
// ../core/src/video-encoder.ts
|
|
2244
|
+
import fs3 from "fs/promises";
|
|
2245
|
+
import path3 from "path";
|
|
2246
|
+
import os from "os";
|
|
2247
|
+
import { createRequire } from "module";
|
|
2248
|
+
var require2 = createRequire(import.meta.url);
|
|
2249
|
+
function loadFfmpeg() {
|
|
2250
|
+
try {
|
|
2251
|
+
return require2("fluent-ffmpeg");
|
|
2252
|
+
} catch {
|
|
2253
|
+
throw new Error(
|
|
2254
|
+
"VideoEncoder: fluent-ffmpeg is required for screencast encoding. Install it with: npm install fluent-ffmpeg"
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
async function writeFramesAndManifest(frames, tmpDir, ext) {
|
|
2259
|
+
const manifestLines = ["ffconcat version 1.0"];
|
|
2260
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2261
|
+
const frameName = `frame-${String(i).padStart(6, "0")}.${ext}`;
|
|
2262
|
+
const framePath = path3.join(tmpDir, frameName);
|
|
2263
|
+
await fs3.writeFile(framePath, Buffer.from(frames[i].data, "base64"));
|
|
2264
|
+
const nextTs = frames[i + 1]?.timestamp ?? frames[i].timestamp + 100;
|
|
2265
|
+
const durationSecs = Math.max((nextTs - frames[i].timestamp) / 1e3, 0.01);
|
|
2266
|
+
manifestLines.push(`file '${framePath}'`);
|
|
2267
|
+
manifestLines.push(`duration ${durationSecs.toFixed(6)}`);
|
|
2268
|
+
}
|
|
2269
|
+
const lastFramePath = path3.join(
|
|
2270
|
+
tmpDir,
|
|
2271
|
+
`frame-${String(frames.length - 1).padStart(6, "0")}.${ext}`
|
|
2272
|
+
);
|
|
2273
|
+
manifestLines.push(`file '${lastFramePath}'`);
|
|
2274
|
+
const manifestPath = path3.join(tmpDir, "manifest.txt");
|
|
2275
|
+
await fs3.writeFile(manifestPath, manifestLines.join("\n"));
|
|
2276
|
+
return manifestPath;
|
|
2277
|
+
}
|
|
2278
|
+
function classifyFfmpegError(err) {
|
|
2279
|
+
const msg = err.message || "";
|
|
2280
|
+
if (msg.includes("Cannot find ffmpeg") || msg.includes("ENOENT") || msg.includes("spawn") || msg.includes("not found")) {
|
|
2281
|
+
return new Error(
|
|
2282
|
+
"VideoEncoder: ffmpeg binary not found on PATH. Install ffmpeg: https://ffmpeg.org/download.html"
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
return new Error(`VideoEncoder: ffmpeg error \u2014 ${msg}`);
|
|
2286
|
+
}
|
|
2287
|
+
function runFfmpeg(ffmpeg, manifestPath, outputPath) {
|
|
2288
|
+
return new Promise((resolve2, reject) => {
|
|
2289
|
+
ffmpeg().input(manifestPath).inputOptions(["-f", "concat", "-safe", "0"]).videoCodec("libvpx").outputOptions([
|
|
2290
|
+
"-b:v",
|
|
2291
|
+
"1M",
|
|
2292
|
+
"-pix_fmt",
|
|
2293
|
+
"yuv420p",
|
|
2294
|
+
"-vsync",
|
|
2295
|
+
"cfr",
|
|
2296
|
+
"-r",
|
|
2297
|
+
"10",
|
|
2298
|
+
// Force a keyframe every frame. VP8 can only seek to keyframes, and
|
|
2299
|
+
// libvpx's default GOP places a single keyframe on near-static
|
|
2300
|
+
// screencast content — so scrubber/marker seeks snap back to the
|
|
2301
|
+
// start. At 10fps over short recordings, all-intra keeps seeking
|
|
2302
|
+
// frame-accurate at an acceptable size.
|
|
2303
|
+
"-g",
|
|
2304
|
+
"1",
|
|
2305
|
+
"-keyint_min",
|
|
2306
|
+
"1",
|
|
2307
|
+
"-auto-alt-ref",
|
|
2308
|
+
"0",
|
|
2309
|
+
"-disposition:v",
|
|
2310
|
+
"default"
|
|
2311
|
+
]).output(outputPath).on("end", () => resolve2()).on("error", (err) => reject(classifyFfmpegError(err))).run();
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
async function encodeToVideo(frames, outputPath, options = {}) {
|
|
2315
|
+
if (frames.length === 0) {
|
|
2316
|
+
throw new Error("VideoEncoder: no frames to encode");
|
|
2317
|
+
}
|
|
2318
|
+
const ffmpeg = loadFfmpeg();
|
|
2319
|
+
const ext = options.captureFormat === "png" ? "png" : "jpg";
|
|
2320
|
+
const tmpDir = await fs3.mkdtemp(
|
|
2321
|
+
path3.join(os.tmpdir(), "devtools-screencast-")
|
|
2322
|
+
);
|
|
2323
|
+
try {
|
|
2324
|
+
const manifestPath = await writeFramesAndManifest(frames, tmpDir, ext);
|
|
2325
|
+
await runFfmpeg(ffmpeg, manifestPath, outputPath);
|
|
2326
|
+
} finally {
|
|
2327
|
+
await fs3.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// ../core/src/video-slice.ts
|
|
2333
|
+
function sliceFramesFrom(frames, startWallTime) {
|
|
2334
|
+
return frames.filter((f) => f.timestamp >= startWallTime);
|
|
2335
|
+
}
|
|
2336
|
+
async function encodePerTestVideo(input) {
|
|
2337
|
+
const minFrames = input.minFrames ?? 2;
|
|
2338
|
+
if (input.frames.length < minFrames) {
|
|
2339
|
+
return void 0;
|
|
2340
|
+
}
|
|
2341
|
+
await fs4.mkdir(input.outputDir, { recursive: true });
|
|
2342
|
+
const retrySuffix = input.attempt ? `-retry${input.attempt}` : "";
|
|
2343
|
+
const filename = `video-${fileSlug(input.testUid)}-${input.sessionId.slice(0, 8)}${retrySuffix}.webm`;
|
|
2344
|
+
const filePath = path4.join(input.outputDir, filename);
|
|
2345
|
+
try {
|
|
2346
|
+
await encodeToVideo([...input.frames], filePath, {
|
|
2347
|
+
captureFormat: input.captureFormat
|
|
2348
|
+
});
|
|
2349
|
+
} catch (err) {
|
|
2350
|
+
input.onLog?.("warn", `Per-test video encode failed: ${errorMessage(err)}`);
|
|
2351
|
+
return void 0;
|
|
2352
|
+
}
|
|
2353
|
+
return {
|
|
2354
|
+
kind: "video",
|
|
2355
|
+
path: filePath,
|
|
2356
|
+
scope: "test",
|
|
2357
|
+
key: input.testUid,
|
|
2358
|
+
testUids: [input.testUid],
|
|
2359
|
+
retained: true
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// ../core/src/trace-retention.ts
|
|
2364
|
+
var KNOWN_POLICIES = /* @__PURE__ */ new Set([
|
|
2365
|
+
"on",
|
|
2366
|
+
"retain-on-failure",
|
|
2367
|
+
"retain-on-first-failure",
|
|
2368
|
+
"on-first-retry",
|
|
2369
|
+
"on-all-retries",
|
|
2370
|
+
"retain-on-failure-and-retries"
|
|
2371
|
+
]);
|
|
2372
|
+
function shouldRetainTrace(policy, input) {
|
|
2373
|
+
if (policy === void 0 || policy === "on" || !KNOWN_POLICIES.has(policy)) {
|
|
2374
|
+
return { retain: true };
|
|
2375
|
+
}
|
|
2376
|
+
const outcomes = Array.from(input.outcomes);
|
|
2377
|
+
if (outcomes.length === 0) {
|
|
2378
|
+
return { retain: true, failOpen: true };
|
|
2379
|
+
}
|
|
2380
|
+
const anyFailed = outcomes.some((o) => o.state === "failed");
|
|
2381
|
+
if (!input.attemptInfoAvailable && policy !== "retain-on-failure") {
|
|
2382
|
+
return { retain: anyFailed, degradedToFailure: true };
|
|
2383
|
+
}
|
|
2384
|
+
const groups = groupByTest(outcomes);
|
|
2385
|
+
switch (policy) {
|
|
2386
|
+
case "retain-on-failure":
|
|
2387
|
+
return { retain: groups.some((g) => finalAttempt(g).state === "failed") };
|
|
2388
|
+
case "retain-on-first-failure":
|
|
2389
|
+
return {
|
|
2390
|
+
retain: groups.some((g) => firstAttempt(g)?.state === "failed")
|
|
2391
|
+
};
|
|
2392
|
+
case "on-first-retry":
|
|
2393
|
+
return { retain: groups.some((g) => g.some((o) => o.attempt === 1)) };
|
|
2394
|
+
case "on-all-retries":
|
|
2395
|
+
return { retain: groups.some((g) => g.some((o) => attemptOf(o) >= 1)) };
|
|
2396
|
+
case "retain-on-failure-and-retries":
|
|
2397
|
+
return {
|
|
2398
|
+
retain: groups.some(
|
|
2399
|
+
(g) => finalAttempt(g).state === "failed" || g.some((o) => attemptOf(o) >= 1)
|
|
2400
|
+
)
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
var attemptOf = (o) => o.attempt ?? 0;
|
|
2405
|
+
function groupByTest(outcomes) {
|
|
2406
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
2407
|
+
const groups = [];
|
|
2408
|
+
for (const outcome of outcomes) {
|
|
2409
|
+
if (outcome.uid === void 0) {
|
|
2410
|
+
groups.push([outcome]);
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
const existing = byUid.get(outcome.uid);
|
|
2414
|
+
if (existing) {
|
|
2415
|
+
existing.push(outcome);
|
|
2416
|
+
} else {
|
|
2417
|
+
const group = [outcome];
|
|
2418
|
+
byUid.set(outcome.uid, group);
|
|
2419
|
+
groups.push(group);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return groups;
|
|
2423
|
+
}
|
|
2424
|
+
function finalAttempt(group) {
|
|
2425
|
+
return group.reduce((best, o) => attemptOf(o) >= attemptOf(best) ? o : best);
|
|
2426
|
+
}
|
|
2427
|
+
function firstAttempt(group) {
|
|
2428
|
+
return group.find((o) => attemptOf(o) === 0);
|
|
2429
|
+
}
|
|
2430
|
+
function tracePolicyModeWarning(policy, mode) {
|
|
2431
|
+
if (policy === void 0 || mode === "trace") {
|
|
2432
|
+
return void 0;
|
|
2433
|
+
}
|
|
2434
|
+
return 'tracePolicy only applies in trace mode; ignoring it because mode is not "trace".';
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// ../core/src/allure-artifacts.ts
|
|
2438
|
+
var CONTENT_TYPE_BY_KIND = {
|
|
2439
|
+
trace: "application/zip",
|
|
2440
|
+
video: "video/webm",
|
|
2441
|
+
screenshot: "image/png"
|
|
2442
|
+
};
|
|
2443
|
+
async function attachTraceArtifact(artifact, sink, onLog) {
|
|
2444
|
+
if (!sink || !artifact.retained || !artifact.path) {
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
try {
|
|
2448
|
+
const stat = await fs5.stat(artifact.path);
|
|
2449
|
+
if (!stat.isFile()) {
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
const content = await fs5.readFile(artifact.path);
|
|
2453
|
+
await sink(
|
|
2454
|
+
basename(artifact.path),
|
|
2455
|
+
content,
|
|
2456
|
+
CONTENT_TYPE_BY_KIND[artifact.kind]
|
|
2457
|
+
);
|
|
2458
|
+
} catch (err) {
|
|
2459
|
+
onLog?.(
|
|
2460
|
+
"warn",
|
|
2461
|
+
`Allure attach skipped for ${artifact.path}: ${String(err)}`
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
function lastRenderedScreenshot(snapshots, startWallTime) {
|
|
2466
|
+
for (let i = snapshots.length - 1; i >= 0; i--) {
|
|
2467
|
+
const snap = snapshots[i];
|
|
2468
|
+
if (snap.timestamp < startWallTime) {
|
|
2469
|
+
return void 0;
|
|
2470
|
+
}
|
|
2471
|
+
if (snap.command !== "__final__" && snap.screenshot) {
|
|
2472
|
+
return snap.screenshot;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return void 0;
|
|
2476
|
+
}
|
|
2477
|
+
async function captureAndAttachScreenshot(input) {
|
|
2478
|
+
const { mode, granularity, policy, failed, screenshotBase64, sessionId } = input;
|
|
2479
|
+
if (mode !== "trace" || granularity !== "test" || !screenshotBase64 || !sessionId || !input.testUid || !shouldCaptureScreenshot(policy, failed)) {
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const artifact = await writeScreenshotArtifact({
|
|
2483
|
+
outputDir: input.outputDir,
|
|
2484
|
+
testUid: input.testUid,
|
|
2485
|
+
sessionId,
|
|
2486
|
+
base64: screenshotBase64
|
|
2487
|
+
});
|
|
2488
|
+
input.onArtifact(artifact);
|
|
2489
|
+
await attachTraceArtifact(artifact, input.attach, input.onLog);
|
|
2490
|
+
}
|
|
2491
|
+
async function captureAndAttachVideo(input) {
|
|
2492
|
+
const { mode, granularity, policy, frames, testUid, sessionId } = input;
|
|
2493
|
+
if (mode !== "trace" || granularity !== "test" || !policy || policy === "off" || !frames || !testUid || !sessionId || input.outcomes.length === 0) {
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
if (!shouldRetainTrace(policy, {
|
|
2497
|
+
outcomes: input.outcomes,
|
|
2498
|
+
attemptInfoAvailable: true
|
|
2499
|
+
}).retain) {
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
const artifact = await encodePerTestVideo({
|
|
2503
|
+
frames: sliceFramesFrom(frames, input.startWallTime),
|
|
2504
|
+
outputDir: input.outputDir,
|
|
2505
|
+
testUid,
|
|
2506
|
+
sessionId,
|
|
2507
|
+
attempt: input.attempt,
|
|
2508
|
+
captureFormat: input.captureFormat,
|
|
2509
|
+
onLog: input.onLog
|
|
2510
|
+
});
|
|
2511
|
+
if (!artifact) {
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
input.onArtifact(artifact);
|
|
2515
|
+
await attachTraceArtifact(artifact, input.attach, input.onLog);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// ../core/src/attempt-tracker.ts
|
|
2519
|
+
var TestAttemptTracker = class {
|
|
2520
|
+
#ledger = /* @__PURE__ */ new Map();
|
|
2521
|
+
#sawRetry = false;
|
|
2522
|
+
/** Record a starting test; returns its attempt number (0 first, +1 per rerun).
|
|
2523
|
+
* `specFile` (optional) enables spec-scoped retention lookups. */
|
|
2524
|
+
recordStart(uid, specFile) {
|
|
2525
|
+
const entry = this.#ledger.get(uid);
|
|
2526
|
+
if (entry && entry.attempts.length > 0) {
|
|
2527
|
+
this.#sawRetry = true;
|
|
2528
|
+
entry.attempts[entry.attempts.length - 1].state = "failed";
|
|
2529
|
+
}
|
|
2530
|
+
const attempt = entry ? entry.attempts.length : 0;
|
|
2531
|
+
const slot = { uid, attempt };
|
|
2532
|
+
if (entry) {
|
|
2533
|
+
entry.attempts.push(slot);
|
|
2534
|
+
if (specFile !== void 0) {
|
|
2535
|
+
entry.specFile = specFile;
|
|
2536
|
+
}
|
|
2537
|
+
} else {
|
|
2538
|
+
this.#ledger.set(uid, { specFile, attempts: [slot] });
|
|
2539
|
+
}
|
|
2540
|
+
return attempt;
|
|
2541
|
+
}
|
|
2542
|
+
/** Stamp the resolved state onto uid's most recent attempt slot once the
|
|
2543
|
+
* outcome is known. `attempt` overrides the slot's number when the adapter
|
|
2544
|
+
* resolved a more authoritative value (e.g. WDIO's result.retries). */
|
|
2545
|
+
recordOutcome(uid, state, attempt) {
|
|
2546
|
+
const attempts = this.#ledger.get(uid)?.attempts;
|
|
2547
|
+
const slot = attempts?.[attempts.length - 1];
|
|
2548
|
+
if (!slot) {
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
slot.state = state;
|
|
2552
|
+
if (attempt !== void 0) {
|
|
2553
|
+
slot.attempt = attempt;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
/** Latest attempt recorded for `uid`, or undefined if it never started. */
|
|
2557
|
+
attemptFor(uid) {
|
|
2558
|
+
const entry = this.#ledger.get(uid);
|
|
2559
|
+
return entry ? entry.attempts.length - 1 : void 0;
|
|
2560
|
+
}
|
|
2561
|
+
/** True once any test has started more than once (a retry occurred). */
|
|
2562
|
+
get sawRetry() {
|
|
2563
|
+
return this.#sawRetry;
|
|
2564
|
+
}
|
|
2565
|
+
all() {
|
|
2566
|
+
const out = [];
|
|
2567
|
+
for (const entry of this.#ledger.values()) {
|
|
2568
|
+
out.push(...entry.attempts);
|
|
2569
|
+
}
|
|
2570
|
+
return out;
|
|
2571
|
+
}
|
|
2572
|
+
forSpec(specFile) {
|
|
2573
|
+
const out = [];
|
|
2574
|
+
for (const entry of this.#ledger.values()) {
|
|
2575
|
+
if (entry.specFile === specFile) {
|
|
2576
|
+
out.push(...entry.attempts);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
return out;
|
|
2580
|
+
}
|
|
2581
|
+
forTest(uid, attempt) {
|
|
2582
|
+
const attempts = this.#ledger.get(uid)?.attempts ?? [];
|
|
2583
|
+
return attempt === void 0 ? attempts : attempts.filter((a) => a.attempt === attempt);
|
|
2584
|
+
}
|
|
2585
|
+
/** Clear all state — used at session boundaries and reuse-mode reconnects. */
|
|
2586
|
+
reset() {
|
|
2587
|
+
this.#ledger.clear();
|
|
2588
|
+
this.#sawRetry = false;
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2592
|
+
// ../core/src/assert-patcher.ts
|
|
2593
|
+
import { createRequire as createRequire2 } from "module";
|
|
2594
|
+
|
|
2053
2595
|
// ../core/src/stack.ts
|
|
2054
2596
|
import { parse as parseStackTrace } from "stacktrace-parser";
|
|
2055
2597
|
function isUserCodeFrame(frame) {
|
|
@@ -2064,8 +2606,14 @@ function normalizeFilePath(filePath) {
|
|
|
2064
2606
|
return stripped;
|
|
2065
2607
|
}
|
|
2066
2608
|
}
|
|
2067
|
-
function
|
|
2068
|
-
|
|
2609
|
+
function isAssertFromUserCode(stack) {
|
|
2610
|
+
if (!stack) {
|
|
2611
|
+
return true;
|
|
2612
|
+
}
|
|
2613
|
+
const caller = parseStackTrace(stack)[1];
|
|
2614
|
+
return !!caller && isUserCodeFrame(caller);
|
|
2615
|
+
}
|
|
2616
|
+
function getCallSourceFromStack(stack = new Error().stack) {
|
|
2069
2617
|
if (!stack) {
|
|
2070
2618
|
return { filePath: void 0, callSource: "unknown:0" };
|
|
2071
2619
|
}
|
|
@@ -2077,31 +2625,839 @@ function getCallSourceFromStack() {
|
|
|
2077
2625
|
return { filePath, callSource: `${filePath}:${frame.lineNumber ?? 0}` };
|
|
2078
2626
|
}
|
|
2079
2627
|
|
|
2080
|
-
// ../core/src/
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
function
|
|
2086
|
-
if (
|
|
2087
|
-
return
|
|
2628
|
+
// ../core/src/assert-patcher.ts
|
|
2629
|
+
var require3 = createRequire2(import.meta.url);
|
|
2630
|
+
var ASSERT_PATCHED_SYMBOL = /* @__PURE__ */ Symbol.for(
|
|
2631
|
+
"@wdio/devtools-core/assert-patched"
|
|
2632
|
+
);
|
|
2633
|
+
function safeSerializeAssertArg(value) {
|
|
2634
|
+
if (value === null || value === void 0) {
|
|
2635
|
+
return value;
|
|
2088
2636
|
}
|
|
2089
|
-
|
|
2637
|
+
if (value instanceof RegExp) {
|
|
2638
|
+
return value.toString();
|
|
2639
|
+
}
|
|
2640
|
+
if (typeof value === "function") {
|
|
2641
|
+
return "[Function]";
|
|
2642
|
+
}
|
|
2643
|
+
if (typeof value === "object") {
|
|
2644
|
+
try {
|
|
2645
|
+
return JSON.parse(JSON.stringify(value));
|
|
2646
|
+
} catch {
|
|
2647
|
+
return String(value);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return value;
|
|
2090
2651
|
}
|
|
2091
|
-
function
|
|
2652
|
+
function displayAssertValue(value) {
|
|
2653
|
+
if (typeof value === "string") {
|
|
2654
|
+
return `'${value}'`;
|
|
2655
|
+
}
|
|
2092
2656
|
try {
|
|
2093
|
-
|
|
2094
|
-
const out = [];
|
|
2095
|
-
u.searchParams.forEach((value, name) => out.push({ name, value }));
|
|
2096
|
-
return out;
|
|
2657
|
+
return JSON.stringify(value) ?? String(value);
|
|
2097
2658
|
} catch {
|
|
2098
|
-
return
|
|
2659
|
+
return String(value);
|
|
2099
2660
|
}
|
|
2100
2661
|
}
|
|
2101
|
-
function
|
|
2102
|
-
const
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2662
|
+
function describeAssertFailure(err) {
|
|
2663
|
+
const error = toError(err);
|
|
2664
|
+
const e = typeof err === "object" && err ? err : {};
|
|
2665
|
+
if (!("actual" in e) && !("expected" in e)) {
|
|
2666
|
+
return { result: void 0, error };
|
|
2667
|
+
}
|
|
2668
|
+
const rawMessage = stripAnsi(error.message);
|
|
2669
|
+
const cleanMessage = e.generatedMessage ? `${rawMessage.split("\n")[0] ?? "Assertion failed"}
|
|
2670
|
+
|
|
2671
|
+
Expected: ${displayAssertValue(e.expected)}
|
|
2672
|
+
Received: ${displayAssertValue(e.actual)}` : rawMessage;
|
|
2673
|
+
error.message = cleanMessage;
|
|
2674
|
+
if (error.stack) {
|
|
2675
|
+
error.stack = stripAnsi(error.stack).replace(rawMessage, cleanMessage);
|
|
2676
|
+
}
|
|
2677
|
+
return {
|
|
2678
|
+
result: {
|
|
2679
|
+
passed: false,
|
|
2680
|
+
actual: safeSerializeAssertArg(e.actual),
|
|
2681
|
+
expected: safeSerializeAssertArg(e.expected)
|
|
2682
|
+
},
|
|
2683
|
+
error
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
function makeAssertEmitters(methodName, args, onCommand, callerStack) {
|
|
2687
|
+
const callInfo = getCallSourceFromStack(callerStack);
|
|
2688
|
+
if (callInfo.filePath === void 0 || !isAssertFromUserCode(callerStack)) {
|
|
2689
|
+
return { passed: () => {
|
|
2690
|
+
}, failed: () => {
|
|
2691
|
+
} };
|
|
2692
|
+
}
|
|
2693
|
+
const startedAt = Date.now();
|
|
2694
|
+
const sanitizedArgs = args.map(safeSerializeAssertArg);
|
|
2695
|
+
const emit = (result, error) => onCommand({
|
|
2696
|
+
command: `assert.${methodName}`,
|
|
2697
|
+
args: sanitizedArgs,
|
|
2698
|
+
result,
|
|
2699
|
+
error,
|
|
2700
|
+
callSource: callInfo.callSource,
|
|
2701
|
+
timestamp: startedAt
|
|
2702
|
+
});
|
|
2703
|
+
return {
|
|
2704
|
+
passed: () => emit("passed", void 0),
|
|
2705
|
+
failed: (err) => {
|
|
2706
|
+
const { result, error } = describeAssertFailure(err);
|
|
2707
|
+
emit(result, error);
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
function makePatchedAssertMethod(methodName, assertObj, original, onCommand) {
|
|
2712
|
+
return function patchedAssert(...args) {
|
|
2713
|
+
const callerStack = new Error().stack;
|
|
2714
|
+
const { passed, failed } = makeAssertEmitters(
|
|
2715
|
+
methodName,
|
|
2716
|
+
args,
|
|
2717
|
+
onCommand,
|
|
2718
|
+
callerStack
|
|
2719
|
+
);
|
|
2720
|
+
let result;
|
|
2721
|
+
assertObj[methodName] = original;
|
|
2722
|
+
try {
|
|
2723
|
+
result = original.apply(this, args);
|
|
2724
|
+
} catch (err) {
|
|
2725
|
+
failed(err);
|
|
2726
|
+
throw err;
|
|
2727
|
+
} finally {
|
|
2728
|
+
assertObj[methodName] = patchedAssert;
|
|
2729
|
+
}
|
|
2730
|
+
const maybe = result;
|
|
2731
|
+
if (maybe && typeof maybe.then === "function") {
|
|
2732
|
+
return result.then(
|
|
2733
|
+
(v) => {
|
|
2734
|
+
passed();
|
|
2735
|
+
return v;
|
|
2736
|
+
},
|
|
2737
|
+
(err) => {
|
|
2738
|
+
failed(err);
|
|
2739
|
+
throw err;
|
|
2740
|
+
}
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
passed();
|
|
2744
|
+
return result;
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function capturedAssertToCommandLog(cmd, testUid) {
|
|
2748
|
+
const entry = {
|
|
2749
|
+
command: cmd.command,
|
|
2750
|
+
args: cmd.args,
|
|
2751
|
+
result: cmd.result,
|
|
2752
|
+
timestamp: cmd.timestamp,
|
|
2753
|
+
startTime: cmd.timestamp
|
|
2754
|
+
};
|
|
2755
|
+
if (cmd.error) {
|
|
2756
|
+
entry.error = {
|
|
2757
|
+
name: cmd.error.name,
|
|
2758
|
+
message: cmd.error.message,
|
|
2759
|
+
stack: cmd.error.stack
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
if (cmd.callSource) {
|
|
2763
|
+
entry.callSource = cmd.callSource;
|
|
2764
|
+
}
|
|
2765
|
+
if (testUid) {
|
|
2766
|
+
entry.testUid = testUid;
|
|
2767
|
+
}
|
|
2768
|
+
return entry;
|
|
2769
|
+
}
|
|
2770
|
+
function matcherAssertionToCommandLog(input, testUid) {
|
|
2771
|
+
const command = `${input.prefix ?? "expect"}.${input.method}`;
|
|
2772
|
+
const message = typeof input.message === "function" ? input.message() : input.message;
|
|
2773
|
+
const entry = capturedAssertToCommandLog(
|
|
2774
|
+
{
|
|
2775
|
+
command,
|
|
2776
|
+
args: (input.args ?? []).map(safeSerializeAssertArg),
|
|
2777
|
+
result: input.passed ? "passed" : void 0,
|
|
2778
|
+
error: input.passed ? void 0 : new Error(stripAnsi(message ?? `${command} failed`)),
|
|
2779
|
+
callSource: input.callSource,
|
|
2780
|
+
timestamp: Date.now()
|
|
2781
|
+
},
|
|
2782
|
+
testUid
|
|
2783
|
+
);
|
|
2784
|
+
if (input.title) {
|
|
2785
|
+
entry.title = input.title;
|
|
2786
|
+
}
|
|
2787
|
+
return entry;
|
|
2788
|
+
}
|
|
2789
|
+
function patchAssertNamespace(nsObj, onCommand) {
|
|
2790
|
+
let count = 0;
|
|
2791
|
+
for (const methodName of TRACKED_ASSERT_METHODS) {
|
|
2792
|
+
const original = nsObj[methodName];
|
|
2793
|
+
if (typeof original !== "function") {
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
nsObj[methodName] = makePatchedAssertMethod(
|
|
2797
|
+
methodName,
|
|
2798
|
+
nsObj,
|
|
2799
|
+
original,
|
|
2800
|
+
onCommand
|
|
2801
|
+
);
|
|
2802
|
+
count++;
|
|
2803
|
+
}
|
|
2804
|
+
return count;
|
|
2805
|
+
}
|
|
2806
|
+
function patchNodeAssert(onCommand, onLog) {
|
|
2807
|
+
const log14 = (level, message) => onLog?.(level, message);
|
|
2808
|
+
let assertModule;
|
|
2809
|
+
try {
|
|
2810
|
+
assertModule = require3("node:assert");
|
|
2811
|
+
} catch {
|
|
2812
|
+
log14("warn", "node:assert not available \u2014 skipping assertion capture");
|
|
2813
|
+
return false;
|
|
2814
|
+
}
|
|
2815
|
+
const assertObj = assertModule;
|
|
2816
|
+
if (assertObj[ASSERT_PATCHED_SYMBOL]) {
|
|
2817
|
+
return true;
|
|
2818
|
+
}
|
|
2819
|
+
assertObj[ASSERT_PATCHED_SYMBOL] = true;
|
|
2820
|
+
let patched = patchAssertNamespace(assertObj, onCommand);
|
|
2821
|
+
const strict = assertObj.strict;
|
|
2822
|
+
if (strict && (typeof strict === "object" || typeof strict === "function")) {
|
|
2823
|
+
patched += patchAssertNamespace(
|
|
2824
|
+
strict,
|
|
2825
|
+
onCommand
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
log14("info", `Patched ${patched} node:assert method(s)`);
|
|
2829
|
+
return true;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// ../core/src/sha1.ts
|
|
2833
|
+
import { createHash } from "crypto";
|
|
2834
|
+
function sha1Hex(data) {
|
|
2835
|
+
return createHash("sha1").update(data).digest("hex");
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// ../core/src/trace-sources.ts
|
|
2839
|
+
var MAX_SOURCE_BYTES = 2 * 1024 * 1024;
|
|
2840
|
+
function sourceResourceName(filePath) {
|
|
2841
|
+
return `src@${sha1Hex(filePath)}.txt`;
|
|
2842
|
+
}
|
|
2843
|
+
function splitNumericSuffix(value) {
|
|
2844
|
+
const sep = value.lastIndexOf(":");
|
|
2845
|
+
if (sep <= 0) {
|
|
2846
|
+
return { rest: value };
|
|
2847
|
+
}
|
|
2848
|
+
const digits = value.slice(sep + 1);
|
|
2849
|
+
if (!/^\d+$/.test(digits)) {
|
|
2850
|
+
return { rest: value };
|
|
2851
|
+
}
|
|
2852
|
+
return { rest: value.slice(0, sep), num: Number(digits) };
|
|
2853
|
+
}
|
|
2854
|
+
function callSourceToStack(callSource) {
|
|
2855
|
+
if (!callSource || callSource === "unknown:0") {
|
|
2856
|
+
return void 0;
|
|
2857
|
+
}
|
|
2858
|
+
const last = splitNumericSuffix(callSource);
|
|
2859
|
+
if (last.num === void 0) {
|
|
2860
|
+
return [{ file: callSource, line: 0, column: 0 }];
|
|
2861
|
+
}
|
|
2862
|
+
const prev = splitNumericSuffix(last.rest);
|
|
2863
|
+
if (prev.num === void 0) {
|
|
2864
|
+
return [{ file: last.rest, line: last.num, column: 0 }];
|
|
2865
|
+
}
|
|
2866
|
+
return [{ file: prev.rest, line: prev.num, column: last.num }];
|
|
2867
|
+
}
|
|
2868
|
+
function buildSourceResources(sources) {
|
|
2869
|
+
const out = [];
|
|
2870
|
+
for (const [filePath, text] of Object.entries(sources)) {
|
|
2871
|
+
const data = Buffer.from(text, "utf8");
|
|
2872
|
+
if (data.byteLength > MAX_SOURCE_BYTES) {
|
|
2873
|
+
continue;
|
|
2874
|
+
}
|
|
2875
|
+
out.push({ resourceName: sourceResourceName(filePath), data });
|
|
2876
|
+
}
|
|
2877
|
+
return out;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
// ../core/src/trace-hierarchy.ts
|
|
2881
|
+
function buildGroupPath(cmd, testMetadata) {
|
|
2882
|
+
const path14 = [];
|
|
2883
|
+
if (cmd.testUid) {
|
|
2884
|
+
const entry = testMetadata?.get(cmd.testUid);
|
|
2885
|
+
for (const ancestor of entry?.ancestry ?? []) {
|
|
2886
|
+
path14.push({ uid: ancestor.uid, title: ancestor.title });
|
|
2887
|
+
}
|
|
2888
|
+
path14.push({ uid: cmd.testUid, title: entry?.title ?? cmd.testUid });
|
|
2889
|
+
}
|
|
2890
|
+
if (cmd.stepUid) {
|
|
2891
|
+
const stepEntry = testMetadata?.get(cmd.stepUid);
|
|
2892
|
+
path14.push({ uid: cmd.stepUid, title: stepEntry?.title ?? cmd.stepUid });
|
|
2893
|
+
}
|
|
2894
|
+
return path14;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
// ../core/src/trace-action-events.ts
|
|
2898
|
+
var MAX_RESULT_BYTES = 64 * 1024;
|
|
2899
|
+
function serializableResult(result) {
|
|
2900
|
+
if (result === void 0) {
|
|
2901
|
+
return void 0;
|
|
2902
|
+
}
|
|
2903
|
+
try {
|
|
2904
|
+
const json = JSON.stringify(result);
|
|
2905
|
+
if (json === void 0 || json.length > MAX_RESULT_BYTES) {
|
|
2906
|
+
return void 0;
|
|
2907
|
+
}
|
|
2908
|
+
return JSON.parse(json);
|
|
2909
|
+
} catch {
|
|
2910
|
+
return void 0;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
function collapsedAssertResult(result) {
|
|
2914
|
+
if (typeof result === "object" && result !== null && "passed" in result) {
|
|
2915
|
+
return result;
|
|
2916
|
+
}
|
|
2917
|
+
return void 0;
|
|
2918
|
+
}
|
|
2919
|
+
function buildAssertParams(cmd) {
|
|
2920
|
+
const params = Object.fromEntries(
|
|
2921
|
+
cmd.args.map((arg, index) => [String(index), arg])
|
|
2922
|
+
);
|
|
2923
|
+
const [actual, expected, message] = cmd.args;
|
|
2924
|
+
const collapsed = collapsedAssertResult(cmd.result);
|
|
2925
|
+
const semantic = {
|
|
2926
|
+
actual: collapsed?.actual ?? actual,
|
|
2927
|
+
expected: collapsed?.expected ?? expected,
|
|
2928
|
+
message: collapsed?.message ?? message
|
|
2929
|
+
};
|
|
2930
|
+
for (const [key, value] of Object.entries(semantic)) {
|
|
2931
|
+
if (value !== void 0) {
|
|
2932
|
+
params[key] = value;
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
return params;
|
|
2936
|
+
}
|
|
2937
|
+
function buildActionParams(action, rawArgs) {
|
|
2938
|
+
const isValueMethod = FILL_METHODS.has(action.method);
|
|
2939
|
+
if (action.class === "Element" && isValueMethod && rawArgs.length >= 2) {
|
|
2940
|
+
return { selector: rawArgs[0], value: rawArgs[1] };
|
|
2941
|
+
}
|
|
2942
|
+
if (action.class === "Element" && isValueMethod && rawArgs.length === 1) {
|
|
2943
|
+
return { value: rawArgs[0] };
|
|
2944
|
+
}
|
|
2945
|
+
if (action.class === "Element" && rawArgs.length === 1 && typeof rawArgs[0] === "string") {
|
|
2946
|
+
return { selector: rawArgs[0] };
|
|
2947
|
+
}
|
|
2948
|
+
if (rawArgs.length === 1 && typeof rawArgs[0] === "string") {
|
|
2949
|
+
return { url: rawArgs[0] };
|
|
2950
|
+
}
|
|
2951
|
+
return Object.fromEntries(rawArgs.map((a, i) => [String(i), a]));
|
|
2952
|
+
}
|
|
2953
|
+
function closeGroupsFrom(stream, from) {
|
|
2954
|
+
for (let i = stream.openGroups.length - 1; i >= from; i--) {
|
|
2955
|
+
stream.events.push({
|
|
2956
|
+
type: "after",
|
|
2957
|
+
callId: stream.openGroups[i].callId,
|
|
2958
|
+
endTime: stream.prevEndMs
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
stream.openGroups.length = Math.max(0, from);
|
|
2962
|
+
}
|
|
2963
|
+
function syncGroups(stream, cmd, pageId, wallTime, testMetadata) {
|
|
2964
|
+
const desired = buildGroupPath(cmd, testMetadata);
|
|
2965
|
+
let common = 0;
|
|
2966
|
+
while (common < stream.openGroups.length && common < desired.length && stream.openGroups[common].uid === desired[common].uid) {
|
|
2967
|
+
common++;
|
|
2968
|
+
}
|
|
2969
|
+
closeGroupsFrom(stream, common);
|
|
2970
|
+
for (let i = common; i < desired.length; i++) {
|
|
2971
|
+
stream.callCounter++;
|
|
2972
|
+
const callId = `call@${stream.callCounter}`;
|
|
2973
|
+
const parentId = stream.openGroups[i - 1]?.callId;
|
|
2974
|
+
const groupBefore = {
|
|
2975
|
+
type: "before",
|
|
2976
|
+
callId,
|
|
2977
|
+
startTime: Math.max(
|
|
2978
|
+
stream.prevEndMs,
|
|
2979
|
+
(cmd.startTime ?? cmd.timestamp) - wallTime
|
|
2980
|
+
),
|
|
2981
|
+
class: "Tracing",
|
|
2982
|
+
method: "tracingGroup",
|
|
2983
|
+
pageId,
|
|
2984
|
+
params: { name: desired[i].title },
|
|
2985
|
+
title: desired[i].title,
|
|
2986
|
+
apiName: "tracing.tracingGroup"
|
|
2987
|
+
};
|
|
2988
|
+
if (parentId) {
|
|
2989
|
+
groupBefore.parentId = parentId;
|
|
2990
|
+
}
|
|
2991
|
+
stream.events.push(groupBefore);
|
|
2992
|
+
stream.openGroups.push({ uid: desired[i].uid, callId });
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
function buildParamsAndTitle(action, cmd) {
|
|
2996
|
+
const isAssert = action.class === ASSERT_ACTION_CLASS;
|
|
2997
|
+
const params = isAssert ? buildAssertParams(cmd) : buildActionParams(action, cmd.args);
|
|
2998
|
+
if (cmd.selector && params.locator === void 0) {
|
|
2999
|
+
params.locator = cmd.selector;
|
|
3000
|
+
}
|
|
3001
|
+
return {
|
|
3002
|
+
params,
|
|
3003
|
+
title: formatActionTitle(
|
|
3004
|
+
action,
|
|
3005
|
+
cmd.args,
|
|
3006
|
+
params,
|
|
3007
|
+
isAssert ? cmd.command : void 0
|
|
3008
|
+
)
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
function actionError(cmd, isAssert) {
|
|
3012
|
+
if (cmd.error) {
|
|
3013
|
+
const err = cmd.error;
|
|
3014
|
+
return { message: err.message ?? String(cmd.error) };
|
|
3015
|
+
}
|
|
3016
|
+
if (isAssert) {
|
|
3017
|
+
const collapsed = collapsedAssertResult(cmd.result);
|
|
3018
|
+
if (collapsed && collapsed.passed === false) {
|
|
3019
|
+
return { message: String(collapsed.message ?? "Assertion failed") };
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
return void 0;
|
|
3023
|
+
}
|
|
3024
|
+
function resolveActionPoint(action, cmd, snapshotIndex) {
|
|
3025
|
+
if (action.class !== "Element" || !POINTABLE_METHODS.has(action.method)) {
|
|
3026
|
+
return void 0;
|
|
3027
|
+
}
|
|
3028
|
+
const selector = cmd.args?.[0];
|
|
3029
|
+
if (typeof selector !== "string") {
|
|
3030
|
+
return void 0;
|
|
3031
|
+
}
|
|
3032
|
+
const elements = snapshotIndex?.elementsAt(cmd.timestamp);
|
|
3033
|
+
for (const el of elements ?? []) {
|
|
3034
|
+
const e = el;
|
|
3035
|
+
if (e.selector === selector && e.boundingBox) {
|
|
3036
|
+
const bb = e.boundingBox;
|
|
3037
|
+
return { x: bb.x + bb.width / 2, y: bb.y + bb.height / 2 };
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
return void 0;
|
|
3041
|
+
}
|
|
3042
|
+
function buildAfterEvent(cmd, action, callId, endMs, snapshotIndex) {
|
|
3043
|
+
const afterEvent = { type: "after", callId, endTime: endMs };
|
|
3044
|
+
const error = actionError(cmd, action.class === ASSERT_ACTION_CLASS);
|
|
3045
|
+
if (error) {
|
|
3046
|
+
afterEvent.error = error;
|
|
3047
|
+
}
|
|
3048
|
+
const result = serializableResult(cmd.result);
|
|
3049
|
+
if (result !== void 0) {
|
|
3050
|
+
afterEvent.result = result;
|
|
3051
|
+
}
|
|
3052
|
+
const afterName = snapshotIndex?.claimAfter(cmd.timestamp, callId);
|
|
3053
|
+
if (afterName) {
|
|
3054
|
+
afterEvent.afterSnapshot = afterName;
|
|
3055
|
+
}
|
|
3056
|
+
const point = resolveActionPoint(action, cmd, snapshotIndex);
|
|
3057
|
+
if (point) {
|
|
3058
|
+
afterEvent.point = point;
|
|
3059
|
+
}
|
|
3060
|
+
return afterEvent;
|
|
3061
|
+
}
|
|
3062
|
+
function pushActionPair(stream, cmd, action, pageId, wallTime, snapshotIndex) {
|
|
3063
|
+
stream.callCounter++;
|
|
3064
|
+
const callId = `call@${stream.callCounter}`;
|
|
3065
|
+
const rawStartMs = (cmd.startTime ?? cmd.timestamp) - wallTime;
|
|
3066
|
+
const rawEndMs = cmd.timestamp - wallTime;
|
|
3067
|
+
const startMs = Math.max(stream.prevEndMs, rawStartMs);
|
|
3068
|
+
const endMs = Math.max(startMs + 1, rawEndMs);
|
|
3069
|
+
const { params, title } = buildParamsAndTitle(action, cmd);
|
|
3070
|
+
const beforeEvent = {
|
|
3071
|
+
type: "before",
|
|
3072
|
+
callId,
|
|
3073
|
+
startTime: startMs,
|
|
3074
|
+
class: action.class,
|
|
3075
|
+
method: action.method,
|
|
3076
|
+
pageId,
|
|
3077
|
+
params,
|
|
3078
|
+
title,
|
|
3079
|
+
apiName: `${action.class.toLowerCase()}.${action.method}`,
|
|
3080
|
+
parentId: stream.openGroups[stream.openGroups.length - 1]?.callId
|
|
3081
|
+
};
|
|
3082
|
+
const stack = callSourceToStack(cmd.callSource);
|
|
3083
|
+
if (stack) {
|
|
3084
|
+
beforeEvent.stack = stack;
|
|
3085
|
+
}
|
|
3086
|
+
const beforeName = snapshotIndex?.beforeName();
|
|
3087
|
+
if (beforeName) {
|
|
3088
|
+
beforeEvent.beforeSnapshot = beforeName;
|
|
3089
|
+
}
|
|
3090
|
+
stream.events.push(beforeEvent);
|
|
3091
|
+
stream.events.push(buildAfterEvent(cmd, action, callId, endMs, snapshotIndex));
|
|
3092
|
+
stream.prevEndMs = endMs;
|
|
3093
|
+
}
|
|
3094
|
+
function buildActionEvents(commands, pageId, wallTime, testMetadata, snapshotIndex) {
|
|
3095
|
+
const stream = {
|
|
3096
|
+
events: [],
|
|
3097
|
+
prevEndMs: 0,
|
|
3098
|
+
callCounter: 0,
|
|
3099
|
+
openGroups: []
|
|
3100
|
+
};
|
|
3101
|
+
const ordered = [...commands].sort(
|
|
3102
|
+
(a, b) => (a.startTime ?? a.timestamp) - (b.startTime ?? b.timestamp)
|
|
3103
|
+
);
|
|
3104
|
+
for (const cmd of ordered) {
|
|
3105
|
+
const action = mapCommandToAction(cmd.command);
|
|
3106
|
+
if (!action) {
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
3109
|
+
syncGroups(stream, cmd, pageId, wallTime, testMetadata);
|
|
3110
|
+
pushActionPair(stream, cmd, action, pageId, wallTime, snapshotIndex);
|
|
3111
|
+
}
|
|
3112
|
+
closeGroupsFrom(stream, 0);
|
|
3113
|
+
return stream.events;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
// ../core/src/trace-console.ts
|
|
3117
|
+
var MAX_CONSOLE_EVENTS = 1e4;
|
|
3118
|
+
function toTraceLevel(level) {
|
|
3119
|
+
if (level === "warn") {
|
|
3120
|
+
return "warning";
|
|
3121
|
+
}
|
|
3122
|
+
if (level === "trace") {
|
|
3123
|
+
return "debug";
|
|
3124
|
+
}
|
|
3125
|
+
return level;
|
|
3126
|
+
}
|
|
3127
|
+
function previewArg(arg) {
|
|
3128
|
+
if (typeof arg === "string") {
|
|
3129
|
+
return arg;
|
|
3130
|
+
}
|
|
3131
|
+
try {
|
|
3132
|
+
return JSON.stringify(arg) ?? String(arg);
|
|
3133
|
+
} catch {
|
|
3134
|
+
return String(arg);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
function buildConsoleEvents(logs, pageId, wallTime) {
|
|
3138
|
+
const capped = logs.slice(0, MAX_CONSOLE_EVENTS);
|
|
3139
|
+
const events = capped.map((log14) => {
|
|
3140
|
+
const time = Math.max(0, log14.timestamp - wallTime);
|
|
3141
|
+
const text = log14.args.map(previewArg).join(" ");
|
|
3142
|
+
if (log14.source === "browser" || log14.source === void 0) {
|
|
3143
|
+
return {
|
|
3144
|
+
type: "console",
|
|
3145
|
+
time,
|
|
3146
|
+
pageId,
|
|
3147
|
+
messageType: toTraceLevel(log14.type),
|
|
3148
|
+
text,
|
|
3149
|
+
args: log14.args.map((arg) => ({ preview: previewArg(arg), value: arg })),
|
|
3150
|
+
// Location isn't captured at the patch site; the required field ships zeroed.
|
|
3151
|
+
location: { url: "", lineNumber: 0, columnNumber: 0 }
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
return {
|
|
3155
|
+
type: log14.type === "error" || log14.type === "warn" ? "stderr" : "stdout",
|
|
3156
|
+
timestamp: time,
|
|
3157
|
+
text,
|
|
3158
|
+
source: log14.source
|
|
3159
|
+
};
|
|
3160
|
+
});
|
|
3161
|
+
if (logs.length > MAX_CONSOLE_EVENTS) {
|
|
3162
|
+
const last = capped[capped.length - 1];
|
|
3163
|
+
events.push({
|
|
3164
|
+
type: "stderr",
|
|
3165
|
+
timestamp: last ? Math.max(0, last.timestamp - wallTime) : 0,
|
|
3166
|
+
text: `[devtools] console truncated: dropped ${logs.length - MAX_CONSOLE_EVENTS} entries`
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
return events;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// ../core/src/trace-exporter.ts
|
|
3173
|
+
import fs6 from "fs/promises";
|
|
3174
|
+
import path5 from "path";
|
|
3175
|
+
|
|
3176
|
+
// ../core/src/trace-snapshots.ts
|
|
3177
|
+
function collapseByTimestamp(snapshots) {
|
|
3178
|
+
const byTs = /* @__PURE__ */ new Map();
|
|
3179
|
+
const order = [];
|
|
3180
|
+
for (const snap of snapshots) {
|
|
3181
|
+
const existing = byTs.get(snap.timestamp);
|
|
3182
|
+
if (!existing) {
|
|
3183
|
+
byTs.set(snap.timestamp, { ...snap });
|
|
3184
|
+
order.push(snap.timestamp);
|
|
3185
|
+
continue;
|
|
3186
|
+
}
|
|
3187
|
+
if ((snap.screenshot?.length ?? 0) > (existing.screenshot?.length ?? 0)) {
|
|
3188
|
+
existing.screenshot = snap.screenshot;
|
|
3189
|
+
}
|
|
3190
|
+
if (!existing.elements?.length && snap.elements?.length) {
|
|
3191
|
+
existing.elements = snap.elements;
|
|
3192
|
+
}
|
|
3193
|
+
if (!existing.snapshotText && snap.snapshotText) {
|
|
3194
|
+
existing.snapshotText = snap.snapshotText;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
return order.map((ts) => byTs.get(ts));
|
|
3198
|
+
}
|
|
3199
|
+
function buildSnapshotResources(rawSnapshots, pageId) {
|
|
3200
|
+
const snapshots = collapseByTimestamp(rawSnapshots);
|
|
3201
|
+
const out = [];
|
|
3202
|
+
for (const snap of snapshots) {
|
|
3203
|
+
const base = `${pageId}-${snap.timestamp}`;
|
|
3204
|
+
if (snap.screenshot) {
|
|
3205
|
+
out.push({
|
|
3206
|
+
resourceName: `${base}.jpeg`,
|
|
3207
|
+
data: Buffer.from(snap.screenshot, "base64")
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
if (snap.elements && snap.elements.length) {
|
|
3211
|
+
out.push({
|
|
3212
|
+
resourceName: `${base}-elements.json`,
|
|
3213
|
+
data: Buffer.from(JSON.stringify(snap.elements), "utf8")
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
if (snap.snapshotText) {
|
|
3217
|
+
out.push({
|
|
3218
|
+
resourceName: `${base}-snapshot.txt`,
|
|
3219
|
+
data: Buffer.from(snap.snapshotText, "utf8")
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
return out;
|
|
3224
|
+
}
|
|
3225
|
+
function frameForSnapshot(snap, pageId, timestamp, viewport) {
|
|
3226
|
+
const base = `${pageId}-${snap.timestamp}`;
|
|
3227
|
+
const frame = {
|
|
3228
|
+
type: "screencast-frame",
|
|
3229
|
+
pageId,
|
|
3230
|
+
sha1: `${base}.jpeg`,
|
|
3231
|
+
width: viewport.width,
|
|
3232
|
+
height: viewport.height,
|
|
3233
|
+
timestamp
|
|
3234
|
+
};
|
|
3235
|
+
if (snap.elements && snap.elements.length) {
|
|
3236
|
+
frame.elements = `${base}-elements.json`;
|
|
3237
|
+
}
|
|
3238
|
+
if (snap.snapshotText) {
|
|
3239
|
+
frame.snapshot = `${base}-snapshot.txt`;
|
|
3240
|
+
}
|
|
3241
|
+
return frame;
|
|
3242
|
+
}
|
|
3243
|
+
function buildFilmstripEvents(rawSnapshots, pageId, wallTime, viewport) {
|
|
3244
|
+
const snapshots = collapseByTimestamp(rawSnapshots);
|
|
3245
|
+
const firstSnap = snapshots.find((s) => s.screenshot);
|
|
3246
|
+
const events = [];
|
|
3247
|
+
if (firstSnap) {
|
|
3248
|
+
events.push(frameForSnapshot(firstSnap, pageId, 0, viewport));
|
|
3249
|
+
}
|
|
3250
|
+
for (const snap of snapshots) {
|
|
3251
|
+
if (snap === firstSnap || !snap.screenshot) {
|
|
3252
|
+
continue;
|
|
3253
|
+
}
|
|
3254
|
+
events.push(
|
|
3255
|
+
frameForSnapshot(
|
|
3256
|
+
snap,
|
|
3257
|
+
pageId,
|
|
3258
|
+
Math.max(0, snap.timestamp - wallTime),
|
|
3259
|
+
viewport
|
|
3260
|
+
)
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
return events;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
// ../core/src/screencast-trace.ts
|
|
3267
|
+
import { createHash as createHash2 } from "crypto";
|
|
3268
|
+
var DEFAULT_MAX_FRAMES = 600;
|
|
3269
|
+
var DEFAULT_MIN_INTERVAL_MS = 100;
|
|
3270
|
+
function thinScreencastFrames(frames, options = {}) {
|
|
3271
|
+
const minInterval = options.minFrameIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
3272
|
+
const maxFrames = options.maxFrames ?? DEFAULT_MAX_FRAMES;
|
|
3273
|
+
const kept = [];
|
|
3274
|
+
let lastKeptTs = -Infinity;
|
|
3275
|
+
let lastData;
|
|
3276
|
+
for (const frame of frames) {
|
|
3277
|
+
if (frame.timestamp - lastKeptTs < minInterval) {
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
if (frame.data === lastData) {
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3283
|
+
kept.push(frame);
|
|
3284
|
+
lastKeptTs = frame.timestamp;
|
|
3285
|
+
lastData = frame.data;
|
|
3286
|
+
}
|
|
3287
|
+
if (kept.length <= maxFrames) {
|
|
3288
|
+
return kept;
|
|
3289
|
+
}
|
|
3290
|
+
if (maxFrames <= 1) {
|
|
3291
|
+
return kept.length ? [kept[0]] : [];
|
|
3292
|
+
}
|
|
3293
|
+
const out = [];
|
|
3294
|
+
const step = (kept.length - 1) / (maxFrames - 1);
|
|
3295
|
+
for (let i = 0; i < maxFrames; i++) {
|
|
3296
|
+
out.push(kept[Math.round(i * step)]);
|
|
3297
|
+
}
|
|
3298
|
+
return out;
|
|
3299
|
+
}
|
|
3300
|
+
function buildDenseScreencast(frames, pageId, wallTime, viewport, options = {}) {
|
|
3301
|
+
const events = [];
|
|
3302
|
+
const resources = [];
|
|
3303
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3304
|
+
for (const frame of thinScreencastFrames(frames, options)) {
|
|
3305
|
+
const data = Buffer.from(frame.data, "base64");
|
|
3306
|
+
const sha1 = createHash2("sha1").update(data).digest("hex");
|
|
3307
|
+
const resourceName = `${sha1}.jpeg`;
|
|
3308
|
+
if (!seen.has(sha1)) {
|
|
3309
|
+
seen.add(sha1);
|
|
3310
|
+
resources.push({ resourceName, data });
|
|
3311
|
+
}
|
|
3312
|
+
events.push({
|
|
3313
|
+
type: "screencast-frame",
|
|
3314
|
+
pageId,
|
|
3315
|
+
sha1: resourceName,
|
|
3316
|
+
width: viewport.width,
|
|
3317
|
+
height: viewport.height,
|
|
3318
|
+
timestamp: Math.max(0, frame.timestamp - wallTime)
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
return { events, resources };
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// ../core/src/trace-frame-snapshots.ts
|
|
3325
|
+
var SNAPSHOT_DOCTYPE = "html";
|
|
3326
|
+
var FALLBACK_FRAME_URL = "about:blank";
|
|
3327
|
+
var BODY_STYLE = "margin:0";
|
|
3328
|
+
var IMAGE_STYLE = "display:block;width:100vw;height:100vh;object-fit:contain";
|
|
3329
|
+
var FrameSnapshotIndex = class {
|
|
3330
|
+
#byTimestamp = /* @__PURE__ */ new Map();
|
|
3331
|
+
// Element rects by timestamp, indexed independently of the screenshot gate so
|
|
3332
|
+
// A8 input points resolve even when the frame carries no screenshot.
|
|
3333
|
+
#elementsByTimestamp = /* @__PURE__ */ new Map();
|
|
3334
|
+
#refs = [];
|
|
3335
|
+
#lastName;
|
|
3336
|
+
constructor(snapshots) {
|
|
3337
|
+
for (const snap of snapshots) {
|
|
3338
|
+
if (snap.elements) {
|
|
3339
|
+
const currentEls = this.#elementsByTimestamp.get(snap.timestamp);
|
|
3340
|
+
if (!currentEls || snap.elements.length > currentEls.length) {
|
|
3341
|
+
this.#elementsByTimestamp.set(snap.timestamp, snap.elements);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
if (!snap.screenshot) {
|
|
3345
|
+
continue;
|
|
3346
|
+
}
|
|
3347
|
+
const current = this.#byTimestamp.get(snap.timestamp);
|
|
3348
|
+
if (!current || snap.screenshot.length > (current.screenshot?.length ?? 0)) {
|
|
3349
|
+
this.#byTimestamp.set(snap.timestamp, snap);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
/** Captured element rects at a command's completion timestamp, if any. */
|
|
3354
|
+
elementsAt(timestamp) {
|
|
3355
|
+
return this.#elementsByTimestamp.get(timestamp);
|
|
3356
|
+
}
|
|
3357
|
+
/** Snapshot name representing the page state before the next action. */
|
|
3358
|
+
beforeName() {
|
|
3359
|
+
return this.#lastName;
|
|
3360
|
+
}
|
|
3361
|
+
/** Claims the screenshot captured at the command's completion, if any. */
|
|
3362
|
+
claimAfter(timestamp, callId) {
|
|
3363
|
+
const snap = this.#byTimestamp.get(timestamp);
|
|
3364
|
+
if (!snap) {
|
|
3365
|
+
return void 0;
|
|
3366
|
+
}
|
|
3367
|
+
this.#byTimestamp.delete(timestamp);
|
|
3368
|
+
const snapshotName = `after@${callId}`;
|
|
3369
|
+
this.#refs.push({ callId, snapshotName, snapshot: snap });
|
|
3370
|
+
this.#lastName = snapshotName;
|
|
3371
|
+
return snapshotName;
|
|
3372
|
+
}
|
|
3373
|
+
refs() {
|
|
3374
|
+
return this.#refs;
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3377
|
+
function frameIdForPage(pageId) {
|
|
3378
|
+
const suffix = pageId.startsWith("page@") ? pageId.slice("page@".length) : pageId;
|
|
3379
|
+
return `frame@${suffix}`;
|
|
3380
|
+
}
|
|
3381
|
+
function imageMimeType(base64) {
|
|
3382
|
+
return base64.startsWith("iVBOR") ? "image/png" : "image/jpeg";
|
|
3383
|
+
}
|
|
3384
|
+
function imageDocument(snap) {
|
|
3385
|
+
const screenshot = snap.screenshot ?? "";
|
|
3386
|
+
return [
|
|
3387
|
+
"HTML",
|
|
3388
|
+
{},
|
|
3389
|
+
["HEAD", {}, ["BASE", { href: snap.url ?? FALLBACK_FRAME_URL }]],
|
|
3390
|
+
[
|
|
3391
|
+
"BODY",
|
|
3392
|
+
{ style: BODY_STYLE },
|
|
3393
|
+
[
|
|
3394
|
+
"IMG",
|
|
3395
|
+
{
|
|
3396
|
+
src: `data:${imageMimeType(screenshot)};base64,${screenshot}`,
|
|
3397
|
+
style: IMAGE_STYLE
|
|
3398
|
+
}
|
|
3399
|
+
]
|
|
3400
|
+
]
|
|
3401
|
+
];
|
|
3402
|
+
}
|
|
3403
|
+
function buildImageFrameSnapshots(refs, pageId, wallTime, viewport) {
|
|
3404
|
+
const frameId = frameIdForPage(pageId);
|
|
3405
|
+
return refs.map((ref) => ({
|
|
3406
|
+
type: "frame-snapshot",
|
|
3407
|
+
snapshot: {
|
|
3408
|
+
callId: ref.callId,
|
|
3409
|
+
snapshotName: ref.snapshotName,
|
|
3410
|
+
pageId,
|
|
3411
|
+
frameId,
|
|
3412
|
+
frameUrl: ref.snapshot.url ?? FALLBACK_FRAME_URL,
|
|
3413
|
+
doctype: SNAPSHOT_DOCTYPE,
|
|
3414
|
+
html: imageDocument(ref.snapshot),
|
|
3415
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
3416
|
+
timestamp: Math.max(0, ref.snapshot.timestamp - wallTime),
|
|
3417
|
+
wallTime: ref.snapshot.timestamp,
|
|
3418
|
+
collectionTime: 0,
|
|
3419
|
+
resourceOverrides: [],
|
|
3420
|
+
isMainFrame: true
|
|
3421
|
+
}
|
|
3422
|
+
}));
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
// ../core/src/trace-har.ts
|
|
3426
|
+
var INLINE_BODY_TEXT_MAX_BYTES = 8 * 1024;
|
|
3427
|
+
function toHeaderArray(h) {
|
|
3428
|
+
if (!h) {
|
|
3429
|
+
return [];
|
|
3430
|
+
}
|
|
3431
|
+
return Object.entries(h).map(([name, value]) => ({ name, value }));
|
|
3432
|
+
}
|
|
3433
|
+
function toQueryString(url) {
|
|
3434
|
+
try {
|
|
3435
|
+
const u = new URL(url);
|
|
3436
|
+
const out = [];
|
|
3437
|
+
u.searchParams.forEach((value, name) => out.push({ name, value }));
|
|
3438
|
+
return out;
|
|
3439
|
+
} catch {
|
|
3440
|
+
return [];
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
function toHarContent(entry, mimeType, bodySha1) {
|
|
3444
|
+
const content = { size: entry.size ?? 0, mimeType };
|
|
3445
|
+
const body = entry.responseBody;
|
|
3446
|
+
if (body === void 0) {
|
|
3447
|
+
return content;
|
|
3448
|
+
}
|
|
3449
|
+
if (Buffer.byteLength(body, "utf8") < INLINE_BODY_TEXT_MAX_BYTES) {
|
|
3450
|
+
content.text = body;
|
|
3451
|
+
}
|
|
3452
|
+
if (bodySha1) {
|
|
3453
|
+
content._sha1 = bodySha1;
|
|
3454
|
+
}
|
|
3455
|
+
return content;
|
|
3456
|
+
}
|
|
3457
|
+
function networkRequestToHar(entry, opts = {}) {
|
|
3458
|
+
const startedDateTime = new Date(entry.timestamp).toISOString();
|
|
3459
|
+
const duration = entry.time ?? (entry.endTime ?? entry.startTime) - entry.startTime;
|
|
3460
|
+
const status = entry.response?.status ?? entry.status ?? 0;
|
|
2105
3461
|
const mimeType = entry.response?.mimeType ?? "";
|
|
2106
3462
|
const responseHeaders = entry.response?.headers ?? entry.responseHeaders;
|
|
2107
3463
|
return {
|
|
@@ -2125,7 +3481,7 @@ function networkRequestToHar(entry) {
|
|
|
2125
3481
|
httpVersion: "HTTP/1.1",
|
|
2126
3482
|
cookies: [],
|
|
2127
3483
|
headers: toHeaderArray(responseHeaders),
|
|
2128
|
-
content:
|
|
3484
|
+
content: toHarContent(entry, mimeType, opts.bodySha1),
|
|
2129
3485
|
redirectURL: "",
|
|
2130
3486
|
headersSize: -1,
|
|
2131
3487
|
bodySize: entry.size ?? -1
|
|
@@ -2153,6 +3509,9 @@ function buildTraceZip(inputs) {
|
|
|
2153
3509
|
"transcript.md"
|
|
2154
3510
|
);
|
|
2155
3511
|
}
|
|
3512
|
+
if (inputs.mutationsNdjson?.length) {
|
|
3513
|
+
zipFile.addBuffer(inputs.mutationsNdjson, "trace.mutations");
|
|
3514
|
+
}
|
|
2156
3515
|
for (const resource of inputs.resources) {
|
|
2157
3516
|
zipFile.addBuffer(resource.data, `resources/${resource.resourceName}`);
|
|
2158
3517
|
}
|
|
@@ -2164,13 +3523,80 @@ function buildTraceZip(inputs) {
|
|
|
2164
3523
|
});
|
|
2165
3524
|
}
|
|
2166
3525
|
|
|
3526
|
+
// ../core/src/trace-mutations.ts
|
|
3527
|
+
var MAX_MUTATIONS_NDJSON_BYTES = 50 * 1024 * 1024;
|
|
3528
|
+
function buildMutationsNdjson(mutations, cap = MAX_MUTATIONS_NDJSON_BYTES) {
|
|
3529
|
+
if (!mutations.length) {
|
|
3530
|
+
return { ndjson: Buffer.alloc(0), truncated: false, written: 0 };
|
|
3531
|
+
}
|
|
3532
|
+
const lines = [];
|
|
3533
|
+
let bytes = 0;
|
|
3534
|
+
for (const mutation of mutations) {
|
|
3535
|
+
const line = JSON.stringify(mutation);
|
|
3536
|
+
const add = Buffer.byteLength(line, "utf8") + (lines.length ? 1 : 0);
|
|
3537
|
+
if (lines.length > 0 && bytes + add > cap) {
|
|
3538
|
+
break;
|
|
3539
|
+
}
|
|
3540
|
+
lines.push(line);
|
|
3541
|
+
bytes += add;
|
|
3542
|
+
}
|
|
3543
|
+
const written = lines.length;
|
|
3544
|
+
const dropped = mutations.length - written;
|
|
3545
|
+
if (dropped > 0) {
|
|
3546
|
+
const marker = { __truncated__: true, dropped };
|
|
3547
|
+
lines.push(JSON.stringify(marker));
|
|
3548
|
+
}
|
|
3549
|
+
return {
|
|
3550
|
+
ndjson: Buffer.from(lines.join("\n"), "utf8"),
|
|
3551
|
+
truncated: dropped > 0,
|
|
3552
|
+
written
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
|
|
2167
3556
|
// ../core/src/trace-exporter.ts
|
|
2168
3557
|
var TRACE_VERSION = 8;
|
|
2169
3558
|
var LIBRARY_NAME = "@wdio/devtools-core";
|
|
2170
3559
|
var LIBRARY_VERSION = "1.0.0";
|
|
3560
|
+
var MAX_BODY_RESOURCE_BYTES = 1024 * 1024;
|
|
3561
|
+
var MAX_TOTAL_BODY_RESOURCE_BYTES = 20 * 1024 * 1024;
|
|
3562
|
+
function buildNetworkBodyResources(requests, caps = {
|
|
3563
|
+
maxBodyBytes: MAX_BODY_RESOURCE_BYTES,
|
|
3564
|
+
maxTotalBytes: MAX_TOTAL_BODY_RESOURCE_BYTES
|
|
3565
|
+
}) {
|
|
3566
|
+
const resources = [];
|
|
3567
|
+
const sha1ByRequestId = /* @__PURE__ */ new Map();
|
|
3568
|
+
const stored = /* @__PURE__ */ new Set();
|
|
3569
|
+
let totalBytes = 0;
|
|
3570
|
+
for (const request of requests) {
|
|
3571
|
+
if (request.responseBody === void 0) {
|
|
3572
|
+
continue;
|
|
3573
|
+
}
|
|
3574
|
+
const data = Buffer.from(request.responseBody, "utf8");
|
|
3575
|
+
if (data.byteLength > caps.maxBodyBytes) {
|
|
3576
|
+
continue;
|
|
3577
|
+
}
|
|
3578
|
+
const sha1 = sha1Hex(data);
|
|
3579
|
+
if (stored.has(sha1)) {
|
|
3580
|
+
sha1ByRequestId.set(request.id, sha1);
|
|
3581
|
+
continue;
|
|
3582
|
+
}
|
|
3583
|
+
if (totalBytes + data.byteLength > caps.maxTotalBytes) {
|
|
3584
|
+
continue;
|
|
3585
|
+
}
|
|
3586
|
+
stored.add(sha1);
|
|
3587
|
+
totalBytes += data.byteLength;
|
|
3588
|
+
resources.push({ resourceName: sha1, data });
|
|
3589
|
+
sha1ByRequestId.set(request.id, sha1);
|
|
3590
|
+
}
|
|
3591
|
+
return { resources, sha1ByRequestId };
|
|
3592
|
+
}
|
|
2171
3593
|
function shortId(sessionId) {
|
|
2172
3594
|
return (sessionId ?? Math.random().toString(36).slice(2, 10)).slice(0, 8);
|
|
2173
3595
|
}
|
|
3596
|
+
function allocateTraceIds(sessionId) {
|
|
3597
|
+
const idPrefix = shortId(sessionId);
|
|
3598
|
+
return { contextId: `context@${idPrefix}`, pageId: `page@${idPrefix}` };
|
|
3599
|
+
}
|
|
2174
3600
|
function resolveContextNaming(caps) {
|
|
2175
3601
|
const platformName = typeof caps?.platformName === "string" ? caps.platformName.toLowerCase() : void 0;
|
|
2176
3602
|
const deviceName = typeof caps?.["appium:deviceName"] === "string" ? caps["appium:deviceName"] : void 0;
|
|
@@ -2205,106 +3631,14 @@ function buildContextOptions(trace, contextId, wallTime) {
|
|
|
2205
3631
|
}
|
|
2206
3632
|
};
|
|
2207
3633
|
}
|
|
2208
|
-
function
|
|
2209
|
-
const events = [];
|
|
2210
|
-
let prevEndMs = 0;
|
|
2211
|
-
let callCounter = 0;
|
|
2212
|
-
let lastTestUid;
|
|
2213
|
-
let groupCallId;
|
|
2214
|
-
for (const cmd of commands) {
|
|
2215
|
-
const action = mapCommandToAction(cmd.command);
|
|
2216
|
-
if (!action) {
|
|
2217
|
-
continue;
|
|
2218
|
-
}
|
|
2219
|
-
if (cmd.testUid && cmd.testUid !== lastTestUid) {
|
|
2220
|
-
if (lastTestUid && groupCallId) {
|
|
2221
|
-
callCounter++;
|
|
2222
|
-
events.push({
|
|
2223
|
-
type: "after",
|
|
2224
|
-
callId: groupCallId,
|
|
2225
|
-
endTime: prevEndMs
|
|
2226
|
-
});
|
|
2227
|
-
}
|
|
2228
|
-
callCounter++;
|
|
2229
|
-
groupCallId = `call@${callCounter}`;
|
|
2230
|
-
const meta = testMetadata?.get(cmd.testUid);
|
|
2231
|
-
const groupName = meta?.title ?? cmd.testUid;
|
|
2232
|
-
events.push({
|
|
2233
|
-
type: "before",
|
|
2234
|
-
callId: groupCallId,
|
|
2235
|
-
startTime: Math.max(
|
|
2236
|
-
prevEndMs,
|
|
2237
|
-
(cmd.startTime ?? cmd.timestamp) - wallTime
|
|
2238
|
-
),
|
|
2239
|
-
class: "Tracing",
|
|
2240
|
-
method: "tracingGroup",
|
|
2241
|
-
pageId,
|
|
2242
|
-
params: { name: groupName },
|
|
2243
|
-
title: groupName,
|
|
2244
|
-
apiName: "tracing.tracingGroup"
|
|
2245
|
-
});
|
|
2246
|
-
lastTestUid = cmd.testUid;
|
|
2247
|
-
}
|
|
2248
|
-
callCounter++;
|
|
2249
|
-
const callId = `call@${callCounter}`;
|
|
2250
|
-
const rawStartMs = (cmd.startTime ?? cmd.timestamp) - wallTime;
|
|
2251
|
-
const rawEndMs = cmd.timestamp - wallTime;
|
|
2252
|
-
const startMs = Math.max(prevEndMs, rawStartMs);
|
|
2253
|
-
const endMs = Math.max(startMs + 1, rawEndMs);
|
|
2254
|
-
const rawArgs = cmd.args;
|
|
2255
|
-
let params;
|
|
2256
|
-
const isValueMethod = FILL_METHODS.has(action.method);
|
|
2257
|
-
if (action.class === "Element" && isValueMethod && rawArgs.length >= 2) {
|
|
2258
|
-
params = { selector: rawArgs[0], value: rawArgs[1] };
|
|
2259
|
-
} else if (action.class === "Element" && isValueMethod && rawArgs.length === 1) {
|
|
2260
|
-
params = { value: rawArgs[0] };
|
|
2261
|
-
} else if (action.class === "Element" && rawArgs.length === 1 && typeof rawArgs[0] === "string") {
|
|
2262
|
-
params = { selector: rawArgs[0] };
|
|
2263
|
-
} else if (rawArgs.length === 1 && typeof rawArgs[0] === "string") {
|
|
2264
|
-
params = { url: rawArgs[0] };
|
|
2265
|
-
} else {
|
|
2266
|
-
params = Object.fromEntries(rawArgs.map((a, i) => [String(i), a]));
|
|
2267
|
-
}
|
|
2268
|
-
events.push({
|
|
2269
|
-
type: "before",
|
|
2270
|
-
callId,
|
|
2271
|
-
startTime: startMs,
|
|
2272
|
-
class: action.class,
|
|
2273
|
-
method: action.method,
|
|
2274
|
-
pageId,
|
|
2275
|
-
params,
|
|
2276
|
-
title: formatActionTitle(action, cmd.args, params),
|
|
2277
|
-
apiName: `${action.class.toLowerCase()}.${action.method}`,
|
|
2278
|
-
parentId: groupCallId
|
|
2279
|
-
});
|
|
2280
|
-
const afterEvent = {
|
|
2281
|
-
type: "after",
|
|
2282
|
-
callId,
|
|
2283
|
-
endTime: endMs
|
|
2284
|
-
};
|
|
2285
|
-
if (cmd.error) {
|
|
2286
|
-
const err = cmd.error;
|
|
2287
|
-
afterEvent.error = { message: err.message ?? String(cmd.error) };
|
|
2288
|
-
}
|
|
2289
|
-
events.push(afterEvent);
|
|
2290
|
-
prevEndMs = endMs;
|
|
2291
|
-
}
|
|
2292
|
-
if (lastTestUid && groupCallId) {
|
|
2293
|
-
callCounter++;
|
|
2294
|
-
events.push({
|
|
2295
|
-
type: "after",
|
|
2296
|
-
callId: groupCallId,
|
|
2297
|
-
endTime: prevEndMs
|
|
2298
|
-
});
|
|
2299
|
-
}
|
|
2300
|
-
return events;
|
|
2301
|
-
}
|
|
2302
|
-
function buildNetworkNdjson(requests, wallTime, pageId) {
|
|
3634
|
+
function buildNetworkNdjson(requests, wallTime, pageId, sha1ByRequestId) {
|
|
2303
3635
|
if (!requests.length) {
|
|
2304
3636
|
return Buffer.alloc(0);
|
|
2305
3637
|
}
|
|
2306
3638
|
const lines = requests.map((r) => {
|
|
2307
|
-
const entry = networkRequestToHar(r
|
|
3639
|
+
const entry = networkRequestToHar(r, {
|
|
3640
|
+
bodySha1: sha1ByRequestId.get(r.id)
|
|
3641
|
+
});
|
|
2308
3642
|
entry.snapshot = {
|
|
2309
3643
|
...entry.snapshot,
|
|
2310
3644
|
// Monotonic offset so the viewer positions bars on the timeline.
|
|
@@ -2316,51 +3650,6 @@ function buildNetworkNdjson(requests, wallTime, pageId) {
|
|
|
2316
3650
|
});
|
|
2317
3651
|
return Buffer.from(lines.join("\n"), "utf8");
|
|
2318
3652
|
}
|
|
2319
|
-
function buildSnapshotResources(snapshots, pageId) {
|
|
2320
|
-
const out = [];
|
|
2321
|
-
for (const snap of snapshots) {
|
|
2322
|
-
const base = `${pageId}-${snap.timestamp}`;
|
|
2323
|
-
if (snap.screenshot) {
|
|
2324
|
-
out.push({
|
|
2325
|
-
resourceName: `${base}.jpeg`,
|
|
2326
|
-
data: Buffer.from(snap.screenshot, "base64")
|
|
2327
|
-
});
|
|
2328
|
-
}
|
|
2329
|
-
if (snap.elements && snap.elements.length) {
|
|
2330
|
-
out.push({
|
|
2331
|
-
resourceName: `${base}-elements.json`,
|
|
2332
|
-
data: Buffer.from(JSON.stringify(snap.elements), "utf8")
|
|
2333
|
-
});
|
|
2334
|
-
}
|
|
2335
|
-
if (snap.snapshotText) {
|
|
2336
|
-
out.push({
|
|
2337
|
-
resourceName: `${base}-snapshot.txt`,
|
|
2338
|
-
data: Buffer.from(snap.snapshotText, "utf8")
|
|
2339
|
-
});
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
return out;
|
|
2343
|
-
}
|
|
2344
|
-
function buildScreencastFrames(snapshots, pageId, wallTime, viewport) {
|
|
2345
|
-
return snapshots.filter((s) => s.screenshot).map((s) => {
|
|
2346
|
-
const base = `${pageId}-${s.timestamp}`;
|
|
2347
|
-
const frame = {
|
|
2348
|
-
type: "screencast-frame",
|
|
2349
|
-
pageId,
|
|
2350
|
-
sha1: `${base}.jpeg`,
|
|
2351
|
-
width: viewport.width,
|
|
2352
|
-
height: viewport.height,
|
|
2353
|
-
timestamp: Math.max(0, s.timestamp - wallTime)
|
|
2354
|
-
};
|
|
2355
|
-
if (s.elements && s.elements.length) {
|
|
2356
|
-
frame.elements = `${base}-elements.json`;
|
|
2357
|
-
}
|
|
2358
|
-
if (s.snapshotText) {
|
|
2359
|
-
frame.snapshot = `${base}-snapshot.txt`;
|
|
2360
|
-
}
|
|
2361
|
-
return frame;
|
|
2362
|
-
});
|
|
2363
|
-
}
|
|
2364
3653
|
function eventTime(e) {
|
|
2365
3654
|
switch (e.type) {
|
|
2366
3655
|
case "context-options":
|
|
@@ -2371,6 +3660,13 @@ function eventTime(e) {
|
|
|
2371
3660
|
return e.endTime;
|
|
2372
3661
|
case "screencast-frame":
|
|
2373
3662
|
return e.timestamp;
|
|
3663
|
+
case "frame-snapshot":
|
|
3664
|
+
return e.snapshot.timestamp;
|
|
3665
|
+
case "console":
|
|
3666
|
+
return e.time;
|
|
3667
|
+
case "stdout":
|
|
3668
|
+
case "stderr":
|
|
3669
|
+
return e.timestamp;
|
|
2374
3670
|
}
|
|
2375
3671
|
}
|
|
2376
3672
|
function eventOrder(e) {
|
|
@@ -2380,9 +3676,14 @@ function eventOrder(e) {
|
|
|
2380
3676
|
case "after":
|
|
2381
3677
|
return 1;
|
|
2382
3678
|
case "screencast-frame":
|
|
3679
|
+
case "frame-snapshot":
|
|
2383
3680
|
return 2;
|
|
2384
|
-
case "
|
|
3681
|
+
case "console":
|
|
3682
|
+
case "stdout":
|
|
3683
|
+
case "stderr":
|
|
2385
3684
|
return 3;
|
|
3685
|
+
case "before":
|
|
3686
|
+
return 4;
|
|
2386
3687
|
}
|
|
2387
3688
|
}
|
|
2388
3689
|
function compareEvents(a, b) {
|
|
@@ -2392,8 +3693,11 @@ function compareEvents(a, b) {
|
|
|
2392
3693
|
function generateTranscript(commands, startWallTime, title) {
|
|
2393
3694
|
const wallTimeISO = new Date(startWallTime).toISOString();
|
|
2394
3695
|
const lines = [`# ${title ?? "Session"} \u2014 ${wallTimeISO}`, ""];
|
|
3696
|
+
const ordered = [...commands].sort(
|
|
3697
|
+
(a, b) => (a.startTime ?? a.timestamp) - (b.startTime ?? b.timestamp)
|
|
3698
|
+
);
|
|
2395
3699
|
const captured = [];
|
|
2396
|
-
for (const c of
|
|
3700
|
+
for (const c of ordered) {
|
|
2397
3701
|
const action = mapCommandToAction(String(c.command));
|
|
2398
3702
|
if (action) {
|
|
2399
3703
|
captured.push({ entry: c, action });
|
|
@@ -2417,51 +3721,72 @@ function generateTranscript(commands, startWallTime, title) {
|
|
|
2417
3721
|
});
|
|
2418
3722
|
return lines.join("\n");
|
|
2419
3723
|
}
|
|
2420
|
-
function
|
|
2421
|
-
const firstCommandTs = trace.commands[0]?.timestamp;
|
|
2422
|
-
const wallTime = opts.wallTimeOverride ?? firstCommandTs ?? Date.now();
|
|
2423
|
-
const idPrefix = shortId(opts.sessionId);
|
|
2424
|
-
const contextId = `context@${idPrefix}`;
|
|
2425
|
-
const pageId = `page@${idPrefix}`;
|
|
3724
|
+
function buildEventStream(trace, ctxOptions, pageId, wallTime, testMetadata, denseFrameEvents = []) {
|
|
2426
3725
|
const viewport = trace.metadata.viewport ?? { width: 1280, height: 720 };
|
|
2427
3726
|
const snapshots = trace.actionSnapshots ?? [];
|
|
2428
|
-
const
|
|
2429
|
-
const
|
|
2430
|
-
const
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
3727
|
+
const snapshotIndex = new FrameSnapshotIndex(snapshots);
|
|
3728
|
+
const filmstrip = denseFrameEvents.length > 0 ? denseFrameEvents : buildFilmstripEvents(snapshots, pageId, wallTime, viewport);
|
|
3729
|
+
const events = [
|
|
3730
|
+
ctxOptions,
|
|
3731
|
+
...filmstrip,
|
|
3732
|
+
...buildActionEvents(
|
|
3733
|
+
trace.commands,
|
|
2435
3734
|
pageId,
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
initFrame.elements = `${base}-elements.json`;
|
|
2443
|
-
}
|
|
2444
|
-
if (firstSnap.snapshotText) {
|
|
2445
|
-
initFrame.snapshot = `${base}-snapshot.txt`;
|
|
2446
|
-
}
|
|
2447
|
-
events.push(initFrame);
|
|
2448
|
-
}
|
|
2449
|
-
events.push(
|
|
2450
|
-
...buildScreencastFrames(
|
|
2451
|
-
firstSnap ? snapshots.filter((s) => s !== firstSnap) : snapshots,
|
|
3735
|
+
wallTime,
|
|
3736
|
+
testMetadata,
|
|
3737
|
+
snapshotIndex
|
|
3738
|
+
),
|
|
3739
|
+
...buildImageFrameSnapshots(
|
|
3740
|
+
snapshotIndex.refs(),
|
|
2452
3741
|
pageId,
|
|
2453
3742
|
wallTime,
|
|
2454
3743
|
viewport
|
|
2455
3744
|
),
|
|
2456
|
-
...
|
|
2457
|
-
|
|
3745
|
+
...buildConsoleEvents(trace.consoleLogs, pageId, wallTime)
|
|
3746
|
+
];
|
|
2458
3747
|
events.sort(compareEvents);
|
|
2459
|
-
|
|
3748
|
+
return events;
|
|
3749
|
+
}
|
|
3750
|
+
function buildTraceBundle(trace, opts = {}) {
|
|
3751
|
+
const firstCommandTs = trace.commands[0]?.timestamp;
|
|
3752
|
+
const wallTime = opts.wallTimeOverride ?? firstCommandTs ?? Date.now();
|
|
3753
|
+
const { contextId, pageId } = allocateTraceIds(opts.sessionId);
|
|
3754
|
+
const ctxOptions = buildContextOptions(trace, contextId, wallTime);
|
|
3755
|
+
const dense = buildDenseScreencast(
|
|
3756
|
+
trace.screencastFrames ?? [],
|
|
3757
|
+
pageId,
|
|
3758
|
+
wallTime,
|
|
3759
|
+
trace.metadata.viewport ?? { width: 1280, height: 720 }
|
|
3760
|
+
);
|
|
3761
|
+
const events = buildEventStream(
|
|
3762
|
+
trace,
|
|
3763
|
+
ctxOptions,
|
|
3764
|
+
pageId,
|
|
3765
|
+
wallTime,
|
|
3766
|
+
opts.testMetadata,
|
|
3767
|
+
dense.events
|
|
3768
|
+
);
|
|
3769
|
+
const networkBodies = buildNetworkBodyResources(trace.networkRequests);
|
|
2460
3770
|
return {
|
|
2461
3771
|
traceNdjson: events.map((e) => JSON.stringify(e)).join("\n") + "\n",
|
|
2462
|
-
networkNdjson: buildNetworkNdjson(
|
|
2463
|
-
|
|
2464
|
-
|
|
3772
|
+
networkNdjson: buildNetworkNdjson(
|
|
3773
|
+
trace.networkRequests,
|
|
3774
|
+
wallTime,
|
|
3775
|
+
pageId,
|
|
3776
|
+
networkBodies.sha1ByRequestId
|
|
3777
|
+
),
|
|
3778
|
+
transcriptMd: generateTranscript(
|
|
3779
|
+
trace.commands,
|
|
3780
|
+
wallTime,
|
|
3781
|
+
ctxOptions.title
|
|
3782
|
+
),
|
|
3783
|
+
mutationsNdjson: buildMutationsNdjson(trace.mutations ?? []).ndjson,
|
|
3784
|
+
resources: [
|
|
3785
|
+
...buildSnapshotResources(trace.actionSnapshots ?? [], pageId),
|
|
3786
|
+
...dense.resources,
|
|
3787
|
+
...buildSourceResources(trace.sources),
|
|
3788
|
+
...networkBodies.resources
|
|
3789
|
+
]
|
|
2465
3790
|
};
|
|
2466
3791
|
}
|
|
2467
3792
|
async function exportTraceZip(trace, opts = {}) {
|
|
@@ -2470,25 +3795,30 @@ async function exportTraceZip(trace, opts = {}) {
|
|
|
2470
3795
|
traceNdjson: bundle.traceNdjson,
|
|
2471
3796
|
networkNdjson: bundle.networkNdjson,
|
|
2472
3797
|
resources: bundle.resources,
|
|
2473
|
-
transcriptMd: bundle.transcriptMd
|
|
3798
|
+
transcriptMd: bundle.transcriptMd,
|
|
3799
|
+
mutationsNdjson: bundle.mutationsNdjson
|
|
2474
3800
|
});
|
|
2475
3801
|
}
|
|
2476
3802
|
async function exportTraceDirectory(trace, targetDir, opts = {}) {
|
|
2477
3803
|
const bundle = buildTraceBundle(trace, opts);
|
|
2478
|
-
await
|
|
3804
|
+
await fs6.mkdir(path5.join(targetDir, "resources"), { recursive: true });
|
|
2479
3805
|
await Promise.all([
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
3806
|
+
fs6.writeFile(path5.join(targetDir, "trace.trace"), bundle.traceNdjson),
|
|
3807
|
+
fs6.writeFile(
|
|
3808
|
+
path5.join(targetDir, "transcript.md"),
|
|
2483
3809
|
bundle.transcriptMd,
|
|
2484
3810
|
"utf8"
|
|
2485
3811
|
),
|
|
2486
|
-
bundle.networkNdjson.length ?
|
|
2487
|
-
|
|
3812
|
+
bundle.networkNdjson.length ? fs6.writeFile(
|
|
3813
|
+
path5.join(targetDir, "trace.network"),
|
|
2488
3814
|
bundle.networkNdjson
|
|
2489
3815
|
) : Promise.resolve(),
|
|
3816
|
+
bundle.mutationsNdjson.length ? fs6.writeFile(
|
|
3817
|
+
path5.join(targetDir, "trace.mutations"),
|
|
3818
|
+
bundle.mutationsNdjson
|
|
3819
|
+
) : Promise.resolve(),
|
|
2490
3820
|
...bundle.resources.map(
|
|
2491
|
-
(r) =>
|
|
3821
|
+
(r) => fs6.writeFile(path5.join(targetDir, "resources", r.resourceName), r.data)
|
|
2492
3822
|
)
|
|
2493
3823
|
]);
|
|
2494
3824
|
}
|
|
@@ -2506,145 +3836,508 @@ async function writeTraceZip(capturer, opts) {
|
|
|
2506
3836
|
},
|
|
2507
3837
|
commands: capturer.commandsLog,
|
|
2508
3838
|
sources: Object.fromEntries(capturer.sources),
|
|
2509
|
-
...actionSnapshots.length ? { actionSnapshots } : {}
|
|
3839
|
+
...actionSnapshots.length ? { actionSnapshots } : {},
|
|
3840
|
+
...opts.screencastFrames?.length ? { screencastFrames: [...opts.screencastFrames] } : {}
|
|
2510
3841
|
};
|
|
2511
|
-
await
|
|
3842
|
+
await fs6.mkdir(opts.outputDir, { recursive: true });
|
|
2512
3843
|
const exportOpts = {
|
|
2513
3844
|
sessionId: opts.sessionId,
|
|
2514
3845
|
wallTimeOverride: capturer.startWallTime,
|
|
2515
3846
|
testMetadata: opts.testMetadata
|
|
2516
3847
|
};
|
|
3848
|
+
const stem = opts.fileStem ?? `trace-${opts.sessionId}`;
|
|
2517
3849
|
if (opts.format === "ndjson-directory") {
|
|
2518
|
-
const dir =
|
|
2519
|
-
await
|
|
3850
|
+
const dir = path5.join(opts.outputDir, stem);
|
|
3851
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
2520
3852
|
await exportTraceDirectory(traceLog, dir, exportOpts);
|
|
2521
3853
|
return dir;
|
|
2522
3854
|
}
|
|
2523
3855
|
const zip = await exportTraceZip(traceLog, exportOpts);
|
|
2524
|
-
const zipPath =
|
|
2525
|
-
await
|
|
3856
|
+
const zipPath = path5.join(opts.outputDir, `${stem}.zip`);
|
|
3857
|
+
await fs6.writeFile(zipPath, zip);
|
|
2526
3858
|
return zipPath;
|
|
2527
3859
|
}
|
|
2528
|
-
function synthesizeSnapshotsFromCommands(commands) {
|
|
2529
|
-
return commands.filter((c) => c.screenshot && mapCommandToAction(c.command)).map((c) => ({
|
|
2530
|
-
timestamp: c.timestamp,
|
|
2531
|
-
command: c.command,
|
|
2532
|
-
screenshot: c.screenshot
|
|
3860
|
+
function synthesizeSnapshotsFromCommands(commands) {
|
|
3861
|
+
return commands.filter((c) => c.screenshot && mapCommandToAction(c.command)).map((c) => ({
|
|
3862
|
+
timestamp: c.timestamp,
|
|
3863
|
+
command: c.command,
|
|
3864
|
+
screenshot: c.screenshot
|
|
3865
|
+
}));
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// ../core/src/spec-trace-helpers.ts
|
|
3869
|
+
import path6 from "path";
|
|
3870
|
+
|
|
3871
|
+
// ../core/src/uid.ts
|
|
3872
|
+
function deterministicUid(...parts) {
|
|
3873
|
+
const hash = parts.join("::").split("").reduce((acc, char) => (acc << 5) - acc + char.charCodeAt(0) | 0, 0);
|
|
3874
|
+
return `stable-${Math.abs(hash).toString(36)}`;
|
|
3875
|
+
}
|
|
3876
|
+
var signatureCounters = /* @__PURE__ */ new Map();
|
|
3877
|
+
function generateStableUid(file, name) {
|
|
3878
|
+
const signature = `${file}::${name}`;
|
|
3879
|
+
const count = signatureCounters.get(signature) ?? 0;
|
|
3880
|
+
signatureCounters.set(signature, count + 1);
|
|
3881
|
+
const input = count > 0 ? `${signature}::${count}` : signature;
|
|
3882
|
+
return deterministicUid(input);
|
|
3883
|
+
}
|
|
3884
|
+
function resetSignatureCounters() {
|
|
3885
|
+
signatureCounters.clear();
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// ../core/src/spec-trace-helpers.ts
|
|
3889
|
+
function findFlushableRange(ranges, testUid) {
|
|
3890
|
+
if (testUid !== void 0) {
|
|
3891
|
+
for (let i = ranges.length - 1; i >= 0; i--) {
|
|
3892
|
+
if (ranges[i].testUid === testUid) {
|
|
3893
|
+
return ranges[i];
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
return void 0;
|
|
3897
|
+
}
|
|
3898
|
+
return ranges[ranges.length - 1];
|
|
3899
|
+
}
|
|
3900
|
+
function sanitizeSpecName(specFile) {
|
|
3901
|
+
const cleaned = trimChar(
|
|
3902
|
+
specFile.replace(/^.*[/\\]/, "").replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_"),
|
|
3903
|
+
"_"
|
|
3904
|
+
);
|
|
3905
|
+
return cleaned || "unknown-spec";
|
|
3906
|
+
}
|
|
3907
|
+
function buildSpecSessionId(specFile, sessionId) {
|
|
3908
|
+
const base = sanitizeSpecName(specFile);
|
|
3909
|
+
const hash = deterministicUid(specFile).split("-").pop().slice(0, 8);
|
|
3910
|
+
return `${base}-${hash}-${sessionId.slice(0, 8)}`;
|
|
3911
|
+
}
|
|
3912
|
+
function buildTestSliceSessionId(specFile, key, sessionId) {
|
|
3913
|
+
const base = sanitizeSpecName(specFile);
|
|
3914
|
+
const hash = deterministicUid(key).split("-").pop().slice(0, 8);
|
|
3915
|
+
return `${base}-${hash}-${sessionId.slice(0, 8)}`;
|
|
3916
|
+
}
|
|
3917
|
+
var MAX_SLUG_LENGTH = 60;
|
|
3918
|
+
function slugify(text) {
|
|
3919
|
+
const collapsed = trimChar(
|
|
3920
|
+
text.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
|
|
3921
|
+
"-"
|
|
3922
|
+
);
|
|
3923
|
+
return trimChar(collapsed.slice(0, MAX_SLUG_LENGTH), "-");
|
|
3924
|
+
}
|
|
3925
|
+
function buildTestSliceFolder(specFile, testTitle, browser, key) {
|
|
3926
|
+
const specBase = sanitizeSpecName(path6.basename(specFile));
|
|
3927
|
+
const keyHash = deterministicUid(key).split("-").pop().slice(0, 8);
|
|
3928
|
+
const titleSlug = slugify(testTitle ?? "") || keyHash;
|
|
3929
|
+
const browserSlug = slugify(browser ?? "") || "browser";
|
|
3930
|
+
const retryMatch = key.match(/-retry(\d+)$/);
|
|
3931
|
+
const retrySuffix = retryMatch ? `-retry${retryMatch[1]}` : "";
|
|
3932
|
+
return `${specBase}-${titleSlug}-${browserSlug}-${keyHash}${retrySuffix}`;
|
|
3933
|
+
}
|
|
3934
|
+
function buildSpecCapturer(capturer, range, nextRange) {
|
|
3935
|
+
const end = nextRange ? {
|
|
3936
|
+
commands: nextRange.commandStartIdx,
|
|
3937
|
+
console: nextRange.consoleStartIdx,
|
|
3938
|
+
network: nextRange.networkStartIdx,
|
|
3939
|
+
mutations: nextRange.mutationStartIdx,
|
|
3940
|
+
traceLogs: nextRange.traceLogStartIdx
|
|
3941
|
+
} : void 0;
|
|
3942
|
+
return {
|
|
3943
|
+
mutations: capturer.mutations.slice(range.mutationStartIdx, end?.mutations),
|
|
3944
|
+
traceLogs: capturer.traceLogs.slice(range.traceLogStartIdx, end?.traceLogs),
|
|
3945
|
+
consoleLogs: capturer.consoleLogs.slice(
|
|
3946
|
+
range.consoleStartIdx,
|
|
3947
|
+
end?.console
|
|
3948
|
+
),
|
|
3949
|
+
networkRequests: capturer.networkRequests.slice(
|
|
3950
|
+
range.networkStartIdx,
|
|
3951
|
+
end?.network
|
|
3952
|
+
),
|
|
3953
|
+
commandsLog: capturer.commandsLog.slice(
|
|
3954
|
+
range.commandStartIdx,
|
|
3955
|
+
end?.commands
|
|
3956
|
+
),
|
|
3957
|
+
sources: new Map(capturer.sources),
|
|
3958
|
+
metadata: capturer.metadata,
|
|
3959
|
+
startWallTime: capturer.startWallTime
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
function filterTestMetadataBySpec(allMetadata, specFile) {
|
|
3963
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
3964
|
+
for (const [uid, meta] of allMetadata) {
|
|
3965
|
+
if (meta.specFile === specFile) {
|
|
3966
|
+
filtered.set(uid, meta);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
return filtered;
|
|
3970
|
+
}
|
|
3971
|
+
function filterTestMetadataByUid(allMetadata, testUid) {
|
|
3972
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
3973
|
+
const entry = allMetadata.get(testUid);
|
|
3974
|
+
if (entry) {
|
|
3975
|
+
filtered.set(testUid, entry);
|
|
3976
|
+
}
|
|
3977
|
+
return filtered;
|
|
3978
|
+
}
|
|
3979
|
+
function pushSliceRange(ctx, specFile, key, testUid, suppressSameKey) {
|
|
3980
|
+
const lastRange = ctx.specRanges[ctx.specRanges.length - 1];
|
|
3981
|
+
if (suppressSameKey && lastRange && lastRange.key === key) {
|
|
3982
|
+
return null;
|
|
3983
|
+
}
|
|
3984
|
+
const prevRange = lastRange && !ctx.flushedSpecs.has(lastRange.key) ? lastRange : null;
|
|
3985
|
+
ctx.specRanges.push({
|
|
3986
|
+
specFile,
|
|
3987
|
+
key,
|
|
3988
|
+
testUid,
|
|
3989
|
+
commandStartIdx: ctx.capturer.commandsLog.length,
|
|
3990
|
+
consoleStartIdx: ctx.capturer.consoleLogs.length,
|
|
3991
|
+
networkStartIdx: ctx.capturer.networkRequests.length,
|
|
3992
|
+
mutationStartIdx: ctx.capturer.mutations.length,
|
|
3993
|
+
traceLogStartIdx: ctx.capturer.traceLogs.length
|
|
3994
|
+
});
|
|
3995
|
+
return prevRange;
|
|
3996
|
+
}
|
|
3997
|
+
function recordSliceBoundary(ctx, granularity, specFile, testUid) {
|
|
3998
|
+
if (granularity === "spec") {
|
|
3999
|
+
return pushSliceRange(ctx, specFile, specFile, void 0, true);
|
|
4000
|
+
}
|
|
4001
|
+
if (granularity === "test" && testUid !== void 0) {
|
|
4002
|
+
const priorAttempts = ctx.specRanges.filter(
|
|
4003
|
+
(r) => r.testUid === testUid
|
|
4004
|
+
).length;
|
|
4005
|
+
const key = priorAttempts === 0 ? testUid : `${testUid}-retry${priorAttempts}`;
|
|
4006
|
+
return pushSliceRange(ctx, specFile, key, testUid, false);
|
|
4007
|
+
}
|
|
4008
|
+
return null;
|
|
4009
|
+
}
|
|
4010
|
+
function recordSpecBoundary(ctx, specFile, traceGranularity) {
|
|
4011
|
+
return recordSliceBoundary(ctx, traceGranularity, specFile);
|
|
4012
|
+
}
|
|
4013
|
+
function sliceByWindow(items, start2, end) {
|
|
4014
|
+
if (start2 === void 0) {
|
|
4015
|
+
return [];
|
|
4016
|
+
}
|
|
4017
|
+
return items.filter(
|
|
4018
|
+
(i) => i.timestamp >= start2 && (end === void 0 || i.timestamp < end)
|
|
4019
|
+
);
|
|
4020
|
+
}
|
|
4021
|
+
async function writeSliceTrace(input, sliceSessionId, testMetadata, overrides = {}) {
|
|
4022
|
+
const sliceCapturer = buildSpecCapturer(
|
|
4023
|
+
input.capturer,
|
|
4024
|
+
input.range,
|
|
4025
|
+
input.nextRange
|
|
4026
|
+
);
|
|
4027
|
+
const commandStart = (c) => c ? c.startTime ?? c.timestamp : void 0;
|
|
4028
|
+
const fallbackStart = () => {
|
|
4029
|
+
const times = [
|
|
4030
|
+
...sliceCapturer.consoleLogs.map((c) => c.timestamp),
|
|
4031
|
+
...sliceCapturer.networkRequests.map((n) => n.timestamp)
|
|
4032
|
+
];
|
|
4033
|
+
return times.length ? Math.min(...times) : void 0;
|
|
4034
|
+
};
|
|
4035
|
+
const windowStart = commandStart(sliceCapturer.commandsLog[0]) ?? fallbackStart();
|
|
4036
|
+
const windowEnd = input.nextRange ? commandStart(input.capturer.commandsLog[input.nextRange.commandStartIdx]) : void 0;
|
|
4037
|
+
const sliceSnapshots = sliceByWindow(
|
|
4038
|
+
input.actionSnapshots,
|
|
4039
|
+
windowStart,
|
|
4040
|
+
windowEnd
|
|
4041
|
+
);
|
|
4042
|
+
const sliceFrames = input.screencastFrames ? sliceByWindow(input.screencastFrames, windowStart, windowEnd) : void 0;
|
|
4043
|
+
if (windowStart !== void 0) {
|
|
4044
|
+
sliceCapturer.startWallTime = windowStart;
|
|
4045
|
+
}
|
|
4046
|
+
return writeTraceZip(sliceCapturer, {
|
|
4047
|
+
outputDir: overrides.outputDir ?? input.outputDir,
|
|
4048
|
+
sessionId: sliceSessionId,
|
|
4049
|
+
fileStem: overrides.fileStem,
|
|
4050
|
+
capabilities: input.capabilities,
|
|
4051
|
+
actionSnapshots: sliceSnapshots.length > 0 ? sliceSnapshots : void 0,
|
|
4052
|
+
screencastFrames: sliceFrames?.length ? sliceFrames : void 0,
|
|
4053
|
+
format: input.format,
|
|
4054
|
+
testMetadata
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
async function writeSpecTrace(input) {
|
|
4058
|
+
return writeSliceTrace(
|
|
4059
|
+
input,
|
|
4060
|
+
buildSpecSessionId(input.range.specFile, input.sessionId),
|
|
4061
|
+
filterTestMetadataBySpec(input.testMetadata, input.range.specFile)
|
|
4062
|
+
);
|
|
4063
|
+
}
|
|
4064
|
+
async function writeTestSliceTrace(input) {
|
|
4065
|
+
const testUid = input.range.testUid ?? input.range.key;
|
|
4066
|
+
const title = input.testMetadata.get(testUid)?.title;
|
|
4067
|
+
const browserName = input.capabilities?.browserName;
|
|
4068
|
+
const folder = buildTestSliceFolder(
|
|
4069
|
+
input.range.specFile,
|
|
4070
|
+
title,
|
|
4071
|
+
browserName,
|
|
4072
|
+
input.range.key
|
|
4073
|
+
);
|
|
4074
|
+
return writeSliceTrace(
|
|
4075
|
+
input,
|
|
4076
|
+
buildTestSliceSessionId(
|
|
4077
|
+
input.range.specFile,
|
|
4078
|
+
input.range.key,
|
|
4079
|
+
input.sessionId
|
|
4080
|
+
),
|
|
4081
|
+
filterTestMetadataByUid(input.testMetadata, testUid),
|
|
4082
|
+
{ outputDir: path6.join(input.outputDir, folder), fileStem: "trace" }
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
// ../core/src/trace-finalizer.ts
|
|
4087
|
+
var SPEC_WITHOUT_BOUNDARIES_WARNING = 'traceGranularity is "spec" but no spec boundaries were detected (the runner may not expose per-test hooks). Falling back to session-level trace.';
|
|
4088
|
+
var TEST_WITHOUT_BOUNDARIES_WARNING = 'traceGranularity is "test" but no test boundaries were detected (the runner may not expose per-test hooks). Falling back to session-level trace.';
|
|
4089
|
+
var SLICE_COUNT_WARN_THRESHOLD = 200;
|
|
4090
|
+
var PENDING_SETTLE_TIMEOUT_MS = 5e3;
|
|
4091
|
+
async function settlePending(pending, timeoutMs) {
|
|
4092
|
+
let timer;
|
|
4093
|
+
const timedOut = new Promise((resolve2) => {
|
|
4094
|
+
timer = setTimeout(() => resolve2(true), timeoutMs);
|
|
4095
|
+
timer.unref?.();
|
|
4096
|
+
});
|
|
4097
|
+
const settled = Promise.allSettled(pending).then(() => false);
|
|
4098
|
+
const result = await Promise.race([settled, timedOut]);
|
|
4099
|
+
if (timer) {
|
|
4100
|
+
clearTimeout(timer);
|
|
4101
|
+
}
|
|
4102
|
+
return result;
|
|
4103
|
+
}
|
|
4104
|
+
async function awaitPendingCaptures(ctx) {
|
|
4105
|
+
if (!ctx.awaitPending?.length) {
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
const timedOut = await settlePending(
|
|
4109
|
+
ctx.awaitPending,
|
|
4110
|
+
PENDING_SETTLE_TIMEOUT_MS
|
|
4111
|
+
);
|
|
4112
|
+
if (timedOut) {
|
|
4113
|
+
ctx.log?.(
|
|
4114
|
+
"warn",
|
|
4115
|
+
`One or more of ${ctx.awaitPending.length} snapshot capture(s) did not settle within ${PENDING_SETTLE_TIMEOUT_MS}ms; writing trace with the snapshots captured so far.`
|
|
4116
|
+
);
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
function sliceCountWarning(count) {
|
|
4120
|
+
return `traceGranularity produced ${count} trace slices. Consider pairing it with a retention policy (e.g. tracePolicy: "retain-on-failure") to avoid writing hundreds of trace archives.`;
|
|
4121
|
+
}
|
|
4122
|
+
function toOutcomes(metadata) {
|
|
4123
|
+
return Array.from(metadata.values(), (m) => ({
|
|
4124
|
+
state: m.state,
|
|
4125
|
+
attempt: m.attempt
|
|
2533
4126
|
}));
|
|
2534
4127
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
4128
|
+
function shouldRetain(ctx, metadata, ledgerOutcomes) {
|
|
4129
|
+
return shouldRetainTrace(ctx.policy, {
|
|
4130
|
+
outcomes: ledgerOutcomes?.length ? ledgerOutcomes : toOutcomes(metadata),
|
|
4131
|
+
attemptInfoAvailable: ctx.attemptInfoAvailable ?? false
|
|
4132
|
+
}).retain;
|
|
2540
4133
|
}
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
const count = signatureCounters.get(signature) ?? 0;
|
|
2545
|
-
signatureCounters.set(signature, count + 1);
|
|
2546
|
-
const input = count > 0 ? `${signature}::${count}` : signature;
|
|
2547
|
-
return deterministicUid(input);
|
|
4134
|
+
function attemptFromKey(key) {
|
|
4135
|
+
const match = /-retry(\d+)$/.exec(key);
|
|
4136
|
+
return match ? Number(match[1]) : 0;
|
|
2548
4137
|
}
|
|
2549
|
-
function
|
|
2550
|
-
|
|
4138
|
+
function sliceOutcomeView(ctx, range, isTestSlice) {
|
|
4139
|
+
if (!ctx.outcomes) {
|
|
4140
|
+
return void 0;
|
|
4141
|
+
}
|
|
4142
|
+
return isTestSlice ? ctx.outcomes.forTest(range.testUid, attemptFromKey(range.key)) : ctx.outcomes.forSpec(range.specFile);
|
|
2551
4143
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
4144
|
+
async function flushRangeTrace(ctx, range, nextRange) {
|
|
4145
|
+
if (ctx.mode !== "trace") {
|
|
4146
|
+
return void 0;
|
|
4147
|
+
}
|
|
4148
|
+
if (ctx.flushed.has(range.key)) {
|
|
4149
|
+
return void 0;
|
|
4150
|
+
}
|
|
4151
|
+
ctx.flushed.add(range.key);
|
|
4152
|
+
const isTestSlice = range.testUid !== void 0;
|
|
4153
|
+
const sliceMetadata = isTestSlice ? filterTestMetadataByUid(ctx.testMetadata, range.testUid) : filterTestMetadataBySpec(ctx.testMetadata, range.specFile);
|
|
4154
|
+
const outcomes = sliceOutcomeView(ctx, range, isTestSlice);
|
|
4155
|
+
const artifact = {
|
|
4156
|
+
kind: "trace",
|
|
4157
|
+
path: "",
|
|
4158
|
+
scope: isTestSlice ? "test" : "spec",
|
|
4159
|
+
key: range.key,
|
|
4160
|
+
testUids: Array.from(sliceMetadata.keys()),
|
|
4161
|
+
retained: shouldRetain(ctx, sliceMetadata, outcomes)
|
|
4162
|
+
};
|
|
4163
|
+
if (!artifact.retained) {
|
|
4164
|
+
ctx.onArtifact?.(artifact);
|
|
4165
|
+
return artifact;
|
|
4166
|
+
}
|
|
4167
|
+
const writeSlice = isTestSlice ? writeTestSliceTrace : writeSpecTrace;
|
|
4168
|
+
artifact.path = await writeSlice({
|
|
4169
|
+
range,
|
|
4170
|
+
nextRange,
|
|
4171
|
+
capturer: ctx.capturer,
|
|
4172
|
+
actionSnapshots: ctx.actionSnapshots ?? [],
|
|
4173
|
+
screencastFrames: ctx.screencastFrames,
|
|
4174
|
+
sessionId: ctx.sessionId,
|
|
4175
|
+
outputDir: ctx.resolveOutputDir(range),
|
|
4176
|
+
format: ctx.format,
|
|
4177
|
+
testMetadata: ctx.testMetadata,
|
|
4178
|
+
capabilities: ctx.capabilities
|
|
4179
|
+
});
|
|
4180
|
+
ctx.log?.(
|
|
4181
|
+
"info",
|
|
4182
|
+
`Trace for ${isTestSlice ? "test" : "spec"} "${range.key}" saved to ${artifact.path}`
|
|
4183
|
+
);
|
|
4184
|
+
ctx.onArtifact?.(artifact);
|
|
4185
|
+
return artifact;
|
|
4186
|
+
}
|
|
4187
|
+
async function flushRangeLogged(ctx, range) {
|
|
2565
4188
|
try {
|
|
2566
|
-
return
|
|
2567
|
-
} catch {
|
|
2568
|
-
|
|
2569
|
-
|
|
4189
|
+
return await flushRangeTrace(ctx, range);
|
|
4190
|
+
} catch (err) {
|
|
4191
|
+
const label = range.testUid !== void 0 ? `test "${range.key}"` : `spec "${range.specFile}"`;
|
|
4192
|
+
ctx.log?.(
|
|
4193
|
+
"warn",
|
|
4194
|
+
`Failed to flush trace for ${label}: ${errorMessage(err)}`
|
|
2570
4195
|
);
|
|
4196
|
+
return void 0;
|
|
2571
4197
|
}
|
|
2572
4198
|
}
|
|
2573
|
-
async function
|
|
2574
|
-
const
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
4199
|
+
async function writeSessionTrace(ctx) {
|
|
4200
|
+
const prepare = ctx.prepareSnapshots ?? ((s) => s);
|
|
4201
|
+
const snapshots = prepare(ctx.actionSnapshots ?? []);
|
|
4202
|
+
const artifact = {
|
|
4203
|
+
kind: "trace",
|
|
4204
|
+
path: "",
|
|
4205
|
+
scope: "session",
|
|
4206
|
+
testUids: Array.from(ctx.testMetadata.keys()),
|
|
4207
|
+
retained: shouldRetain(ctx, ctx.testMetadata, ctx.outcomes?.all())
|
|
4208
|
+
};
|
|
4209
|
+
if (!artifact.retained) {
|
|
4210
|
+
ctx.onArtifact?.(artifact);
|
|
4211
|
+
return artifact;
|
|
4212
|
+
}
|
|
4213
|
+
artifact.path = await writeTraceZip(ctx.capturer, {
|
|
4214
|
+
outputDir: ctx.resolveOutputDir(),
|
|
4215
|
+
sessionId: ctx.sessionId,
|
|
4216
|
+
capabilities: ctx.capabilities,
|
|
4217
|
+
actionSnapshots: snapshots.length ? snapshots : void 0,
|
|
4218
|
+
screencastFrames: ctx.screencastFrames,
|
|
4219
|
+
format: ctx.format,
|
|
4220
|
+
testMetadata: ctx.testMetadata
|
|
4221
|
+
});
|
|
4222
|
+
ctx.log?.("info", `Trace saved to ${artifact.path}`);
|
|
4223
|
+
ctx.onArtifact?.(artifact);
|
|
4224
|
+
return artifact;
|
|
4225
|
+
}
|
|
4226
|
+
async function safely(ctx, write) {
|
|
4227
|
+
try {
|
|
4228
|
+
return await write();
|
|
4229
|
+
} catch (err) {
|
|
4230
|
+
ctx.log?.("warn", `trace write failed: ${errorMessage(err)}`);
|
|
4231
|
+
return void 0;
|
|
2583
4232
|
}
|
|
2584
|
-
const lastFramePath = path2.join(
|
|
2585
|
-
tmpDir,
|
|
2586
|
-
`frame-${String(frames.length - 1).padStart(6, "0")}.${ext}`
|
|
2587
|
-
);
|
|
2588
|
-
manifestLines.push(`file '${lastFramePath}'`);
|
|
2589
|
-
const manifestPath = path2.join(tmpDir, "manifest.txt");
|
|
2590
|
-
await fs2.writeFile(manifestPath, manifestLines.join("\n"));
|
|
2591
|
-
return manifestPath;
|
|
2592
4233
|
}
|
|
2593
|
-
function
|
|
2594
|
-
const
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
4234
|
+
async function flushAllRanges(ctx) {
|
|
4235
|
+
const artifacts = [];
|
|
4236
|
+
for (let i = 0; i < ctx.ranges.length; i++) {
|
|
4237
|
+
const range = ctx.ranges[i];
|
|
4238
|
+
const nextRange = ctx.ranges[i + 1];
|
|
4239
|
+
const artifact = await safely(
|
|
4240
|
+
ctx,
|
|
4241
|
+
() => flushRangeTrace(ctx, range, nextRange)
|
|
2598
4242
|
);
|
|
4243
|
+
if (artifact) {
|
|
4244
|
+
artifacts.push(artifact);
|
|
4245
|
+
}
|
|
2599
4246
|
}
|
|
2600
|
-
return
|
|
4247
|
+
return artifacts;
|
|
2601
4248
|
}
|
|
2602
|
-
function
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
"cfr",
|
|
2611
|
-
"-r",
|
|
2612
|
-
"10",
|
|
2613
|
-
// Force a keyframe every frame. VP8 can only seek to keyframes, and
|
|
2614
|
-
// libvpx's default GOP places a single keyframe on near-static
|
|
2615
|
-
// screencast content — so scrubber/marker seeks snap back to the
|
|
2616
|
-
// start. At 10fps over short recordings, all-intra keeps seeking
|
|
2617
|
-
// frame-accurate at an acceptable size.
|
|
2618
|
-
"-g",
|
|
2619
|
-
"1",
|
|
2620
|
-
"-keyint_min",
|
|
2621
|
-
"1",
|
|
2622
|
-
"-auto-alt-ref",
|
|
2623
|
-
"0",
|
|
2624
|
-
"-disposition:v",
|
|
2625
|
-
"default"
|
|
2626
|
-
]).output(outputPath).on("end", () => resolve2()).on("error", (err) => reject(classifyFfmpegError(err))).run();
|
|
2627
|
-
});
|
|
4249
|
+
async function finalizeTraceExport(ctx) {
|
|
4250
|
+
if (ctx.mode !== "trace") {
|
|
4251
|
+
return [];
|
|
4252
|
+
}
|
|
4253
|
+
await awaitPendingCaptures(ctx);
|
|
4254
|
+
const artifacts = await fanOutTraceWrites(ctx);
|
|
4255
|
+
await maybeWriteManifest(ctx, artifacts);
|
|
4256
|
+
return artifacts;
|
|
2628
4257
|
}
|
|
2629
|
-
async function
|
|
2630
|
-
|
|
2631
|
-
|
|
4258
|
+
async function fanOutTraceWrites(ctx) {
|
|
4259
|
+
const sliced = ctx.granularity === "spec" || ctx.granularity === "test";
|
|
4260
|
+
if (sliced && ctx.ranges.length > 0) {
|
|
4261
|
+
if (ctx.ranges.length > SLICE_COUNT_WARN_THRESHOLD) {
|
|
4262
|
+
ctx.log?.("warn", sliceCountWarning(ctx.ranges.length));
|
|
4263
|
+
}
|
|
4264
|
+
return flushAllRanges(ctx);
|
|
2632
4265
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
4266
|
+
if (ctx.granularity === "spec") {
|
|
4267
|
+
ctx.log?.("warn", SPEC_WITHOUT_BOUNDARIES_WARNING);
|
|
4268
|
+
} else if (ctx.granularity === "test") {
|
|
4269
|
+
ctx.log?.("warn", TEST_WITHOUT_BOUNDARIES_WARNING);
|
|
4270
|
+
}
|
|
4271
|
+
const artifact = await safely(ctx, () => writeSessionTrace(ctx));
|
|
4272
|
+
return artifact ? [artifact] : [];
|
|
4273
|
+
}
|
|
4274
|
+
async function maybeWriteManifest(ctx, fanOutArtifacts) {
|
|
4275
|
+
if (!ctx.emitManifest) {
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
const artifacts = ctx.collectedArtifacts ?? fanOutArtifacts;
|
|
2638
4279
|
try {
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
4280
|
+
const path14 = await writeArtifactsManifest(
|
|
4281
|
+
ctx.resolveOutputDir(),
|
|
4282
|
+
buildArtifactsManifest({
|
|
4283
|
+
sessionId: ctx.sessionId,
|
|
4284
|
+
format: ctx.format ?? "zip",
|
|
4285
|
+
artifacts,
|
|
4286
|
+
testMetadata: ctx.testMetadata
|
|
4287
|
+
})
|
|
4288
|
+
);
|
|
4289
|
+
ctx.log?.("info", `Artifacts manifest written to ${path14}`);
|
|
4290
|
+
} catch (err) {
|
|
4291
|
+
ctx.log?.(
|
|
4292
|
+
"warn",
|
|
4293
|
+
`Failed to write artifacts manifest: ${errorMessage(err)}`
|
|
4294
|
+
);
|
|
2644
4295
|
}
|
|
2645
4296
|
}
|
|
2646
4297
|
|
|
4298
|
+
// ../core/src/terminal-throttle.ts
|
|
4299
|
+
var TERMINAL_REPEAT_WINDOW_MS = 1e3;
|
|
4300
|
+
var LEADING_ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d\d\d)?Z {1,4}/;
|
|
4301
|
+
function terminalRepeatKey(line) {
|
|
4302
|
+
return line.replace(LEADING_ISO_TIMESTAMP_RE, "");
|
|
4303
|
+
}
|
|
4304
|
+
var TerminalLineThrottle = class {
|
|
4305
|
+
#lastEmitted = /* @__PURE__ */ new Map();
|
|
4306
|
+
#windowMs;
|
|
4307
|
+
constructor(windowMs = TERMINAL_REPEAT_WINDOW_MS) {
|
|
4308
|
+
this.#windowMs = windowMs;
|
|
4309
|
+
}
|
|
4310
|
+
/**
|
|
4311
|
+
* True if the line should be emitted, false if it's a within-window repeat of
|
|
4312
|
+
* a line already emitted. The window is anchored to the last *emit*, not the
|
|
4313
|
+
* last occurrence, so a sustained stream of one line still emits once per
|
|
4314
|
+
* window instead of going silent after the first. Expired keys are pruned on
|
|
4315
|
+
* each emit, so state stays bounded by the number of distinct lines seen
|
|
4316
|
+
* within the window.
|
|
4317
|
+
*/
|
|
4318
|
+
shouldEmit(line, now = Date.now()) {
|
|
4319
|
+
const key = terminalRepeatKey(line);
|
|
4320
|
+
const last = this.#lastEmitted.get(key);
|
|
4321
|
+
if (last !== void 0 && now - last < this.#windowMs) {
|
|
4322
|
+
return false;
|
|
4323
|
+
}
|
|
4324
|
+
this.#lastEmitted.set(key, now);
|
|
4325
|
+
this.#prune(now);
|
|
4326
|
+
return true;
|
|
4327
|
+
}
|
|
4328
|
+
#prune(now) {
|
|
4329
|
+
for (const [key, ts] of this.#lastEmitted) {
|
|
4330
|
+
if (now - ts >= this.#windowMs) {
|
|
4331
|
+
this.#lastEmitted.delete(key);
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
};
|
|
4336
|
+
|
|
2647
4337
|
// ../core/src/finalize-screencast.ts
|
|
4338
|
+
import fs7 from "fs";
|
|
4339
|
+
import os2 from "os";
|
|
4340
|
+
import path7 from "path";
|
|
2648
4341
|
async function finalizeScreencast({
|
|
2649
4342
|
recorder,
|
|
2650
4343
|
sessionId,
|
|
@@ -2655,11 +4348,11 @@ async function finalizeScreencast({
|
|
|
2655
4348
|
sendUpstream,
|
|
2656
4349
|
onLog
|
|
2657
4350
|
}) {
|
|
2658
|
-
const
|
|
4351
|
+
const log14 = (level, message) => onLog?.(level, message);
|
|
2659
4352
|
try {
|
|
2660
4353
|
await recorder.stop();
|
|
2661
4354
|
} catch (err) {
|
|
2662
|
-
|
|
4355
|
+
log14("warn", `Screencast stop failed: ${errorMessage(err)}`);
|
|
2663
4356
|
return;
|
|
2664
4357
|
}
|
|
2665
4358
|
const frames = recorder.frames;
|
|
@@ -2668,15 +4361,16 @@ async function finalizeScreencast({
|
|
|
2668
4361
|
}
|
|
2669
4362
|
const fileName = `${filenamePrefix}-${sessionId}.webm`;
|
|
2670
4363
|
const candidate = outputDir || process.cwd();
|
|
2671
|
-
let videoPath =
|
|
4364
|
+
let videoPath = path7.join(candidate, fileName);
|
|
2672
4365
|
try {
|
|
2673
|
-
|
|
4366
|
+
fs7.mkdirSync(candidate, { recursive: true });
|
|
4367
|
+
fs7.accessSync(candidate, fs7.constants.W_OK);
|
|
2674
4368
|
} catch {
|
|
2675
|
-
videoPath =
|
|
4369
|
+
videoPath = path7.join(os2.tmpdir(), fileName);
|
|
2676
4370
|
}
|
|
2677
4371
|
try {
|
|
2678
4372
|
await encodeToVideo(frames, videoPath, { captureFormat });
|
|
2679
|
-
|
|
4373
|
+
log14("info", `\u{1F4F9} Screencast video: ${videoPath}`);
|
|
2680
4374
|
sendUpstream("screencast", {
|
|
2681
4375
|
sessionId,
|
|
2682
4376
|
videoPath,
|
|
@@ -2686,33 +4380,37 @@ async function finalizeScreencast({
|
|
|
2686
4380
|
startTime: frames[0]?.timestamp
|
|
2687
4381
|
});
|
|
2688
4382
|
} catch (err) {
|
|
2689
|
-
|
|
4383
|
+
log14("warn", `Screencast encode failed: ${errorMessage(err)}`);
|
|
2690
4384
|
}
|
|
2691
4385
|
}
|
|
2692
4386
|
|
|
2693
4387
|
// ../core/src/output-dir.ts
|
|
2694
|
-
import
|
|
2695
|
-
import
|
|
2696
|
-
var NODE_MODULES_SEGMENT = `${
|
|
4388
|
+
import fs8 from "fs";
|
|
4389
|
+
import path8 from "path";
|
|
4390
|
+
var NODE_MODULES_SEGMENT = `${path8.sep}node_modules${path8.sep}`;
|
|
4391
|
+
var OUTPUT_SUBDIR = "test-results";
|
|
2697
4392
|
function isWritable(dir) {
|
|
2698
4393
|
try {
|
|
2699
|
-
|
|
4394
|
+
fs8.accessSync(dir, fs8.constants.W_OK);
|
|
2700
4395
|
return true;
|
|
2701
4396
|
} catch {
|
|
2702
4397
|
return false;
|
|
2703
4398
|
}
|
|
2704
4399
|
}
|
|
2705
4400
|
function resolveAdapterOutputDir(input = {}) {
|
|
4401
|
+
return path8.join(resolveBaseDir(input), OUTPUT_SUBDIR);
|
|
4402
|
+
}
|
|
4403
|
+
function resolveBaseDir(input) {
|
|
2706
4404
|
const fallback = input.fallbackDir ?? process.cwd();
|
|
2707
4405
|
if (input.userConfiguredDir) {
|
|
2708
4406
|
return input.userConfiguredDir;
|
|
2709
4407
|
}
|
|
2710
4408
|
const candidates = [];
|
|
2711
4409
|
if (input.testFilePath) {
|
|
2712
|
-
candidates.push(
|
|
4410
|
+
candidates.push(path8.dirname(input.testFilePath));
|
|
2713
4411
|
}
|
|
2714
4412
|
if (input.configPath) {
|
|
2715
|
-
candidates.push(
|
|
4413
|
+
candidates.push(path8.dirname(input.configPath));
|
|
2716
4414
|
}
|
|
2717
4415
|
candidates.push(fallback);
|
|
2718
4416
|
for (const dir of candidates) {
|
|
@@ -2932,7 +4630,38 @@ var ScreencastRecorderBase = class {
|
|
|
2932
4630
|
*/
|
|
2933
4631
|
pushCdpFrame(data, timestampSeconds) {
|
|
2934
4632
|
const timestamp = typeof timestampSeconds === "number" ? Math.round(timestampSeconds * 1e3) : Date.now();
|
|
2935
|
-
this
|
|
4633
|
+
this.#appendFrame({ data, timestamp });
|
|
4634
|
+
}
|
|
4635
|
+
/**
|
|
4636
|
+
* Append a frame, decimating the buffer in place once it exceeds
|
|
4637
|
+
* `maxBufferFrames` so a long session can't grow it without bound.
|
|
4638
|
+
*/
|
|
4639
|
+
#appendFrame(frame) {
|
|
4640
|
+
this.buffer.push(frame);
|
|
4641
|
+
if (this.buffer.length > Math.max(2, this.options.maxBufferFrames)) {
|
|
4642
|
+
this.#decimateBuffer();
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
/**
|
|
4646
|
+
* Halve the buffer by keeping every other frame plus the first and last, so
|
|
4647
|
+
* memory is bounded while the temporal spread survives (never tail-truncated).
|
|
4648
|
+
* `#startIndex` is remapped to the count of surviving pre-marker frames, which
|
|
4649
|
+
* keeps `frames`/`duration` excluding pre-marker frames across decimation.
|
|
4650
|
+
*/
|
|
4651
|
+
#decimateBuffer() {
|
|
4652
|
+
const lastIndex = this.buffer.length - 1;
|
|
4653
|
+
const kept = [];
|
|
4654
|
+
let preMarkerKept = 0;
|
|
4655
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
4656
|
+
if (i % 2 === 0 || i === lastIndex) {
|
|
4657
|
+
if (i < this.#startIndex) {
|
|
4658
|
+
preMarkerKept++;
|
|
4659
|
+
}
|
|
4660
|
+
kept.push(this.buffer[i]);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
this.buffer = kept;
|
|
4664
|
+
this.#startIndex = preMarkerKept;
|
|
2936
4665
|
}
|
|
2937
4666
|
/** Whether `setStartMarker` (or `markStartAtLatest`) has fired yet. */
|
|
2938
4667
|
get hasStartMarker() {
|
|
@@ -2971,13 +4700,13 @@ var ScreencastRecorderBase = class {
|
|
|
2971
4700
|
this.onUnavailable(new Error("first screenshot returned null"));
|
|
2972
4701
|
return;
|
|
2973
4702
|
}
|
|
2974
|
-
this
|
|
4703
|
+
this.#appendFrame({ data: first, timestamp: Date.now() });
|
|
2975
4704
|
const intervalMs = this.options.pollIntervalMs;
|
|
2976
4705
|
this.#pollTimer = setInterval(async () => {
|
|
2977
4706
|
try {
|
|
2978
4707
|
const data = await this.takeScreenshot();
|
|
2979
4708
|
if (data !== null) {
|
|
2980
|
-
this
|
|
4709
|
+
this.#appendFrame({ data, timestamp: Date.now() });
|
|
2981
4710
|
}
|
|
2982
4711
|
} catch {
|
|
2983
4712
|
this.#stopPolling();
|
|
@@ -2999,15 +4728,15 @@ var ScreencastRecorderBase = class {
|
|
|
2999
4728
|
};
|
|
3000
4729
|
|
|
3001
4730
|
// ../core/src/script-loader.ts
|
|
3002
|
-
import
|
|
3003
|
-
import
|
|
3004
|
-
import { createRequire as
|
|
3005
|
-
var
|
|
4731
|
+
import fs9 from "fs/promises";
|
|
4732
|
+
import path9 from "path";
|
|
4733
|
+
import { createRequire as createRequire3 } from "module";
|
|
4734
|
+
var require4 = createRequire3(import.meta.url);
|
|
3006
4735
|
async function loadInjectableScript() {
|
|
3007
|
-
const scriptPath =
|
|
3008
|
-
const scriptDir =
|
|
3009
|
-
const preloadScriptPath =
|
|
3010
|
-
const scriptContent = await
|
|
4736
|
+
const scriptPath = require4.resolve("@wdio/devtools-script");
|
|
4737
|
+
const scriptDir = path9.dirname(scriptPath);
|
|
4738
|
+
const preloadScriptPath = path9.join(scriptDir, "script.js");
|
|
4739
|
+
const scriptContent = await fs9.readFile(preloadScriptPath, "utf-8");
|
|
3011
4740
|
return `(async function() { ${scriptContent} })()`;
|
|
3012
4741
|
}
|
|
3013
4742
|
async function pollUntilReady(check, opts = {}) {
|
|
@@ -3023,7 +4752,7 @@ async function pollUntilReady(check, opts = {}) {
|
|
|
3023
4752
|
}
|
|
3024
4753
|
|
|
3025
4754
|
// ../core/src/session-capturer.ts
|
|
3026
|
-
import
|
|
4755
|
+
import fs10 from "fs/promises";
|
|
3027
4756
|
import { WebSocket } from "ws";
|
|
3028
4757
|
var SessionCapturerBase = class {
|
|
3029
4758
|
// ── State (mostly private; subclasses access shared ws via `this.ws`) ────
|
|
@@ -3041,6 +4770,11 @@ var SessionCapturerBase = class {
|
|
|
3041
4770
|
// stdout, OR when stream forwarding wants to log via console.
|
|
3042
4771
|
#isCapturingConsole = false;
|
|
3043
4772
|
#isCapturingStream = false;
|
|
4773
|
+
// Collapses high-frequency identical terminal lines (e.g. WDIO's per-command
|
|
4774
|
+
// COMMAND/RESULT logger frames reprinted every ~100ms during an expect poll)
|
|
4775
|
+
// so they don't flood the console lane. Terminal-source only — user
|
|
4776
|
+
// console.* is source='test' and never throttled.
|
|
4777
|
+
#terminalThrottle = new TerminalLineThrottle();
|
|
3044
4778
|
// Command bookkeeping — used by adapters that emit commands themselves
|
|
3045
4779
|
// (nightwatch, selenium). The WDIO service adapter doesn't call sendCommand
|
|
3046
4780
|
// (WDIO owns the command lifecycle), so this state is harmless overhead.
|
|
@@ -3165,6 +4899,32 @@ var SessionCapturerBase = class {
|
|
|
3165
4899
|
command: toSend
|
|
3166
4900
|
});
|
|
3167
4901
|
}
|
|
4902
|
+
/**
|
|
4903
|
+
* Mark the most recent user action of a test as failed — for framework
|
|
4904
|
+
* failures that aren't captured as their own command. Broadcasts the swap so
|
|
4905
|
+
* live mode highlights it too. Returns false when no eligible action is found,
|
|
4906
|
+
* OR when that most-recent action already carries an error: the failure is
|
|
4907
|
+
* then already represented (e.g. an expect matcher captured as its own row via
|
|
4908
|
+
* afterAssertion), so it must not bleed onto an earlier *passing* action.
|
|
4909
|
+
*/
|
|
4910
|
+
failLastAction(testUid, error) {
|
|
4911
|
+
for (let i = this.commandsLog.length - 1; i >= 0; i--) {
|
|
4912
|
+
const command = this.commandsLog[i];
|
|
4913
|
+
if (!mapCommandToAction(command.command)) {
|
|
4914
|
+
continue;
|
|
4915
|
+
}
|
|
4916
|
+
if (testUid && command.testUid !== testUid) {
|
|
4917
|
+
continue;
|
|
4918
|
+
}
|
|
4919
|
+
if (command.error) {
|
|
4920
|
+
return false;
|
|
4921
|
+
}
|
|
4922
|
+
command.error = error;
|
|
4923
|
+
this.sendReplaceCommand(command.timestamp, command);
|
|
4924
|
+
return true;
|
|
4925
|
+
}
|
|
4926
|
+
return false;
|
|
4927
|
+
}
|
|
3168
4928
|
/**
|
|
3169
4929
|
* Read a file from disk, store in `sources`, and broadcast to the UI via
|
|
3170
4930
|
* `sendUpstream('sources', { [path]: text })`. Idempotent — a cached path is
|
|
@@ -3176,7 +4936,7 @@ var SessionCapturerBase = class {
|
|
|
3176
4936
|
return;
|
|
3177
4937
|
}
|
|
3178
4938
|
try {
|
|
3179
|
-
const source = (await
|
|
4939
|
+
const source = (await fs10.readFile(filePath, "utf-8")).toString();
|
|
3180
4940
|
this.sources.set(filePath, source);
|
|
3181
4941
|
this.sendUpstream("sources", { [filePath]: source });
|
|
3182
4942
|
} catch (err) {
|
|
@@ -3324,7 +5084,7 @@ var SessionCapturerBase = class {
|
|
|
3324
5084
|
const segments = rawLine.split("\r").filter((s) => s.trim());
|
|
3325
5085
|
const lastSegment = segments[segments.length - 1] ?? rawLine;
|
|
3326
5086
|
const clean = stripAnsi(lastSegment).trim();
|
|
3327
|
-
if (!clean || this.isInternalStreamLine(clean) || SPINNER_RE.test(clean)) {
|
|
5087
|
+
if (!clean || this.isInternalStreamLine(clean) || SPINNER_RE.test(clean) || !this.#terminalThrottle.shouldEmit(clean)) {
|
|
3328
5088
|
continue;
|
|
3329
5089
|
}
|
|
3330
5090
|
this.onLine(detectLogLevel(clean), [clean], LOG_SOURCES.TERMINAL);
|
|
@@ -3402,100 +5162,6 @@ function safeStringify(value) {
|
|
|
3402
5162
|
}
|
|
3403
5163
|
}
|
|
3404
5164
|
|
|
3405
|
-
// ../core/src/spec-trace-helpers.ts
|
|
3406
|
-
function sanitizeSpecName(specFile) {
|
|
3407
|
-
return specFile.replace(/^.*[/\\]/, "").replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_").replace(/^_+|_+$/g, "") || "unknown-spec";
|
|
3408
|
-
}
|
|
3409
|
-
function buildSpecSessionId(specFile, sessionId) {
|
|
3410
|
-
const base = sanitizeSpecName(specFile);
|
|
3411
|
-
const hash = deterministicUid(specFile).split("-").pop().slice(0, 8);
|
|
3412
|
-
return `${base}-${hash}-${sessionId.slice(0, 8)}`;
|
|
3413
|
-
}
|
|
3414
|
-
function buildSpecCapturer(capturer, range, nextRange) {
|
|
3415
|
-
const end = nextRange ? {
|
|
3416
|
-
commands: nextRange.commandStartIdx,
|
|
3417
|
-
console: nextRange.consoleStartIdx,
|
|
3418
|
-
network: nextRange.networkStartIdx,
|
|
3419
|
-
mutations: nextRange.mutationStartIdx,
|
|
3420
|
-
traceLogs: nextRange.traceLogStartIdx
|
|
3421
|
-
} : void 0;
|
|
3422
|
-
return {
|
|
3423
|
-
mutations: capturer.mutations.slice(range.mutationStartIdx, end?.mutations),
|
|
3424
|
-
traceLogs: capturer.traceLogs.slice(range.traceLogStartIdx, end?.traceLogs),
|
|
3425
|
-
consoleLogs: capturer.consoleLogs.slice(
|
|
3426
|
-
range.consoleStartIdx,
|
|
3427
|
-
end?.console
|
|
3428
|
-
),
|
|
3429
|
-
networkRequests: capturer.networkRequests.slice(
|
|
3430
|
-
range.networkStartIdx,
|
|
3431
|
-
end?.network
|
|
3432
|
-
),
|
|
3433
|
-
commandsLog: capturer.commandsLog.slice(
|
|
3434
|
-
range.commandStartIdx,
|
|
3435
|
-
end?.commands
|
|
3436
|
-
),
|
|
3437
|
-
sources: new Map(capturer.sources),
|
|
3438
|
-
metadata: capturer.metadata,
|
|
3439
|
-
startWallTime: capturer.startWallTime
|
|
3440
|
-
};
|
|
3441
|
-
}
|
|
3442
|
-
function filterTestMetadataBySpec(allMetadata, specFile) {
|
|
3443
|
-
const filtered = /* @__PURE__ */ new Map();
|
|
3444
|
-
for (const [uid, meta] of allMetadata) {
|
|
3445
|
-
if (meta.specFile === specFile) {
|
|
3446
|
-
filtered.set(uid, meta);
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
return filtered;
|
|
3450
|
-
}
|
|
3451
|
-
function recordSpecBoundary(ctx, specFile, traceGranularity) {
|
|
3452
|
-
if (traceGranularity !== "spec") {
|
|
3453
|
-
return null;
|
|
3454
|
-
}
|
|
3455
|
-
const lastRange = ctx.specRanges[ctx.specRanges.length - 1];
|
|
3456
|
-
if (lastRange && lastRange.specFile === specFile) {
|
|
3457
|
-
return null;
|
|
3458
|
-
}
|
|
3459
|
-
const prevRange = lastRange && !ctx.flushedSpecs.has(lastRange.specFile) ? lastRange : null;
|
|
3460
|
-
ctx.specRanges.push({
|
|
3461
|
-
specFile,
|
|
3462
|
-
commandStartIdx: ctx.capturer.commandsLog.length,
|
|
3463
|
-
consoleStartIdx: ctx.capturer.consoleLogs.length,
|
|
3464
|
-
networkStartIdx: ctx.capturer.networkRequests.length,
|
|
3465
|
-
mutationStartIdx: ctx.capturer.mutations.length,
|
|
3466
|
-
traceLogStartIdx: ctx.capturer.traceLogs.length,
|
|
3467
|
-
snapshotCount: ctx.actionSnapshots.length
|
|
3468
|
-
});
|
|
3469
|
-
return prevRange;
|
|
3470
|
-
}
|
|
3471
|
-
async function writeSpecTrace(input) {
|
|
3472
|
-
const specCapturer = buildSpecCapturer(
|
|
3473
|
-
input.capturer,
|
|
3474
|
-
input.range,
|
|
3475
|
-
input.nextRange
|
|
3476
|
-
);
|
|
3477
|
-
const specSnapshots = input.actionSnapshots.slice(
|
|
3478
|
-
input.range.snapshotCount,
|
|
3479
|
-
input.nextRange?.snapshotCount ?? input.actionSnapshots.length
|
|
3480
|
-
);
|
|
3481
|
-
const specSessionId = buildSpecSessionId(
|
|
3482
|
-
input.range.specFile,
|
|
3483
|
-
input.sessionId
|
|
3484
|
-
);
|
|
3485
|
-
const testMetadata = filterTestMetadataBySpec(
|
|
3486
|
-
input.testMetadata,
|
|
3487
|
-
input.range.specFile
|
|
3488
|
-
);
|
|
3489
|
-
return writeTraceZip(specCapturer, {
|
|
3490
|
-
outputDir: input.outputDir,
|
|
3491
|
-
sessionId: specSessionId,
|
|
3492
|
-
capabilities: input.capabilities,
|
|
3493
|
-
actionSnapshots: specSnapshots.length > 0 ? specSnapshots : void 0,
|
|
3494
|
-
format: input.format,
|
|
3495
|
-
testMetadata
|
|
3496
|
-
});
|
|
3497
|
-
}
|
|
3498
|
-
|
|
3499
5165
|
// ../core/src/suite-helpers.ts
|
|
3500
5166
|
function createSuiteStats(init) {
|
|
3501
5167
|
return {
|
|
@@ -3575,16 +5241,61 @@ function stampTestEnd(test, end = /* @__PURE__ */ new Date()) {
|
|
|
3575
5241
|
test.end = end;
|
|
3576
5242
|
test._duration = end.getTime() - (test.start?.getTime() ?? end.getTime());
|
|
3577
5243
|
}
|
|
5244
|
+
function deriveAncestorKind(suite, parentKind) {
|
|
5245
|
+
if (parentKind === "feature") {
|
|
5246
|
+
return "scenario";
|
|
5247
|
+
}
|
|
5248
|
+
if (suite.file?.endsWith(".feature") && parentKind !== "scenario") {
|
|
5249
|
+
return "feature";
|
|
5250
|
+
}
|
|
5251
|
+
return "suite";
|
|
5252
|
+
}
|
|
5253
|
+
function collectSuiteTestMetadata(suites) {
|
|
5254
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
5255
|
+
const walk = (suite, ancestors) => {
|
|
5256
|
+
const parentKind = ancestors[ancestors.length - 1]?.kind;
|
|
5257
|
+
const chain = [
|
|
5258
|
+
...ancestors,
|
|
5259
|
+
{
|
|
5260
|
+
uid: suite.uid,
|
|
5261
|
+
title: suite.title,
|
|
5262
|
+
kind: deriveAncestorKind(suite, parentKind)
|
|
5263
|
+
}
|
|
5264
|
+
];
|
|
5265
|
+
for (const entry of suite.tests ?? []) {
|
|
5266
|
+
if (typeof entry !== "object" || entry === null) {
|
|
5267
|
+
continue;
|
|
5268
|
+
}
|
|
5269
|
+
metadata.set(entry.uid, {
|
|
5270
|
+
title: entry.fullTitle || entry.title,
|
|
5271
|
+
specFile: entry.file ?? suite.file,
|
|
5272
|
+
state: entry.state,
|
|
5273
|
+
attempt: entry.retries ?? 0,
|
|
5274
|
+
ancestry: chain
|
|
5275
|
+
});
|
|
5276
|
+
}
|
|
5277
|
+
for (const child of suite.suites ?? []) {
|
|
5278
|
+
walk(child, chain);
|
|
5279
|
+
}
|
|
5280
|
+
};
|
|
5281
|
+
for (const suite of suites) {
|
|
5282
|
+
if (typeof suite !== "object" || suite === null) {
|
|
5283
|
+
continue;
|
|
5284
|
+
}
|
|
5285
|
+
walk(suite, []);
|
|
5286
|
+
}
|
|
5287
|
+
return metadata;
|
|
5288
|
+
}
|
|
3578
5289
|
|
|
3579
5290
|
// ../core/src/test-discovery.ts
|
|
3580
|
-
import
|
|
5291
|
+
import fs11 from "fs";
|
|
3581
5292
|
function findTestDefinitions(filePath, opts = {}) {
|
|
3582
|
-
if (!
|
|
5293
|
+
if (!fs11.existsSync(filePath)) {
|
|
3583
5294
|
return [];
|
|
3584
5295
|
}
|
|
3585
5296
|
let source;
|
|
3586
5297
|
try {
|
|
3587
|
-
source =
|
|
5298
|
+
source = fs11.readFileSync(filePath, "utf-8");
|
|
3588
5299
|
} catch {
|
|
3589
5300
|
return [];
|
|
3590
5301
|
}
|
|
@@ -3678,15 +5389,113 @@ var TestReporterBase = class {
|
|
|
3678
5389
|
}
|
|
3679
5390
|
};
|
|
3680
5391
|
|
|
5392
|
+
// src/trace-context.ts
|
|
5393
|
+
function buildTraceContext(input, sessionId) {
|
|
5394
|
+
return {
|
|
5395
|
+
mode: input.mode,
|
|
5396
|
+
policy: input.policy,
|
|
5397
|
+
granularity: input.granularity,
|
|
5398
|
+
format: input.format,
|
|
5399
|
+
capturer: input.capturer,
|
|
5400
|
+
actionSnapshots: input.capturer.actionSnapshots,
|
|
5401
|
+
sessionId,
|
|
5402
|
+
testMetadata: collectSuiteTestMetadata(input.suites),
|
|
5403
|
+
outcomes: input.outcomes,
|
|
5404
|
+
ranges: input.ranges,
|
|
5405
|
+
flushed: input.flushed,
|
|
5406
|
+
screencastFrames: input.screencastFrames,
|
|
5407
|
+
resolveOutputDir: () => resolveAdapterOutputDir({
|
|
5408
|
+
testFilePath: input.testFilePath,
|
|
5409
|
+
configPath: input.configPath
|
|
5410
|
+
}),
|
|
5411
|
+
awaitPending: [...input.capturer.snapshotCaptures, ...input.traceFlushes],
|
|
5412
|
+
// Nightwatch feeds real per-test attempt numbers via TestAttemptTracker
|
|
5413
|
+
// (B4), so retry-aware policies use per-test attempts, not the fallback.
|
|
5414
|
+
attemptInfoAvailable: true,
|
|
5415
|
+
// Off by default; opt-in only — Nightwatch has no live Allure signal to
|
|
5416
|
+
// auto-detect against (produce-only Allure), unlike WDIO/Selenium.
|
|
5417
|
+
emitManifest: input.emitArtifactsManifest ?? false,
|
|
5418
|
+
collectedArtifacts: input.artifacts,
|
|
5419
|
+
onArtifact: (a) => input.artifacts.push(a),
|
|
5420
|
+
log: input.log
|
|
5421
|
+
};
|
|
5422
|
+
}
|
|
5423
|
+
|
|
5424
|
+
// src/test-artifacts.ts
|
|
5425
|
+
async function emitTestArtifacts(input) {
|
|
5426
|
+
await captureAndAttachScreenshot({
|
|
5427
|
+
mode: input.mode,
|
|
5428
|
+
granularity: input.granularity,
|
|
5429
|
+
policy: input.screenshotPolicy,
|
|
5430
|
+
failed: input.failed,
|
|
5431
|
+
screenshotBase64: lastRenderedScreenshot(
|
|
5432
|
+
input.actionSnapshots,
|
|
5433
|
+
input.startWallTime
|
|
5434
|
+
),
|
|
5435
|
+
sessionId: input.sessionId,
|
|
5436
|
+
outputDir: input.outputDir,
|
|
5437
|
+
testUid: input.uid,
|
|
5438
|
+
attach: void 0,
|
|
5439
|
+
onArtifact: input.onArtifact,
|
|
5440
|
+
onLog: input.onLog
|
|
5441
|
+
});
|
|
5442
|
+
await captureAndAttachVideo({
|
|
5443
|
+
mode: input.mode,
|
|
5444
|
+
granularity: input.granularity,
|
|
5445
|
+
policy: input.videoPolicy,
|
|
5446
|
+
frames: input.frames,
|
|
5447
|
+
startWallTime: input.startWallTime,
|
|
5448
|
+
outcomes: input.outcomes,
|
|
5449
|
+
attempt: input.attempt,
|
|
5450
|
+
outputDir: input.outputDir,
|
|
5451
|
+
testUid: input.uid,
|
|
5452
|
+
sessionId: input.sessionId,
|
|
5453
|
+
captureFormat: input.captureFormat,
|
|
5454
|
+
attach: void 0,
|
|
5455
|
+
onArtifact: input.onArtifact,
|
|
5456
|
+
onLog: input.onLog
|
|
5457
|
+
});
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
// src/helpers/assertCapture.ts
|
|
5461
|
+
import logger from "@wdio/logger";
|
|
5462
|
+
var log = logger("@wdio/nightwatch-devtools:assertCapture");
|
|
5463
|
+
function wireAssertCapture(getCapturer, getTestUid) {
|
|
5464
|
+
patchNodeAssert(
|
|
5465
|
+
(cmd) => captureAssert(getCapturer(), getTestUid(), cmd),
|
|
5466
|
+
(level, message) => log[level](message)
|
|
5467
|
+
);
|
|
5468
|
+
}
|
|
5469
|
+
function captureAssert(capturer, testUid, cmd) {
|
|
5470
|
+
if (!capturer) {
|
|
5471
|
+
return;
|
|
5472
|
+
}
|
|
5473
|
+
capturer.captureCommand(
|
|
5474
|
+
cmd.command,
|
|
5475
|
+
cmd.args,
|
|
5476
|
+
cmd.result,
|
|
5477
|
+
cmd.error,
|
|
5478
|
+
testUid,
|
|
5479
|
+
cmd.callSource,
|
|
5480
|
+
cmd.timestamp
|
|
5481
|
+
).catch(
|
|
5482
|
+
(err) => log.warn(`Failed to capture ${cmd.command}: ${errorMessage(err)}`)
|
|
5483
|
+
);
|
|
5484
|
+
const last = capturer.commandsLog[capturer.commandsLog.length - 1];
|
|
5485
|
+
if (last) {
|
|
5486
|
+
capturer.sendCommand(last);
|
|
5487
|
+
}
|
|
5488
|
+
}
|
|
5489
|
+
|
|
3681
5490
|
// src/index.ts
|
|
3682
5491
|
import { stop as stopBackend } from "@wdio/devtools-backend";
|
|
3683
|
-
import
|
|
5492
|
+
import logger13 from "@wdio/logger";
|
|
3684
5493
|
|
|
3685
5494
|
// src/run-lifecycle.ts
|
|
3686
|
-
import * as
|
|
3687
|
-
import * as
|
|
5495
|
+
import * as fs13 from "fs";
|
|
5496
|
+
import * as path11 from "path";
|
|
3688
5497
|
import * as os3 from "os";
|
|
3689
|
-
import
|
|
5498
|
+
import logger2 from "@wdio/logger";
|
|
3690
5499
|
import { remote } from "webdriverio";
|
|
3691
5500
|
import { start, stop } from "@wdio/devtools-backend";
|
|
3692
5501
|
|
|
@@ -3744,8 +5553,8 @@ var CONFIG_FILENAMES = [
|
|
|
3744
5553
|
var PLUGIN_GLOBAL_KEY = "__nightwatchDevtoolsPlugin";
|
|
3745
5554
|
|
|
3746
5555
|
// src/helpers/utils.ts
|
|
3747
|
-
import * as
|
|
3748
|
-
import * as
|
|
5556
|
+
import * as fs12 from "fs";
|
|
5557
|
+
import * as path10 from "path";
|
|
3749
5558
|
import { parse as parseStackTrace2 } from "stacktrace-parser";
|
|
3750
5559
|
function determineTestState(testcase) {
|
|
3751
5560
|
if (testcase.passed === 0 && testcase.failed === 0) {
|
|
@@ -3802,7 +5611,7 @@ function findTestFileFromStack() {
|
|
|
3802
5611
|
return void 0;
|
|
3803
5612
|
}
|
|
3804
5613
|
const filePath = normalizeFilePath(frame.file);
|
|
3805
|
-
return
|
|
5614
|
+
return fs12.existsSync(filePath) ? filePath : void 0;
|
|
3806
5615
|
}
|
|
3807
5616
|
function extractTestMetadata(filePath) {
|
|
3808
5617
|
const defs = findTestDefinitions(filePath, {
|
|
@@ -3930,21 +5739,21 @@ function resolveNightwatchConfig() {
|
|
|
3930
5739
|
);
|
|
3931
5740
|
if (configFlagIdx !== -1 && process.argv[configFlagIdx + 1]) {
|
|
3932
5741
|
const argvConfig = process.argv[configFlagIdx + 1];
|
|
3933
|
-
const resolved =
|
|
3934
|
-
if (
|
|
5742
|
+
const resolved = path10.isAbsolute(argvConfig) ? argvConfig : path10.resolve(process.cwd(), argvConfig);
|
|
5743
|
+
if (fs12.existsSync(resolved)) {
|
|
3935
5744
|
return resolved;
|
|
3936
5745
|
}
|
|
3937
5746
|
}
|
|
3938
5747
|
let dir = process.cwd();
|
|
3939
|
-
const root =
|
|
5748
|
+
const root = path10.parse(dir).root;
|
|
3940
5749
|
while (dir && dir !== root) {
|
|
3941
5750
|
for (const file of CONFIG_FILENAMES) {
|
|
3942
|
-
const candidate =
|
|
3943
|
-
if (
|
|
5751
|
+
const candidate = path10.join(dir, file);
|
|
5752
|
+
if (fs12.existsSync(candidate)) {
|
|
3944
5753
|
return candidate;
|
|
3945
5754
|
}
|
|
3946
5755
|
}
|
|
3947
|
-
const parent =
|
|
5756
|
+
const parent = path10.dirname(dir);
|
|
3948
5757
|
if (parent === dir) {
|
|
3949
5758
|
break;
|
|
3950
5759
|
}
|
|
@@ -3954,11 +5763,11 @@ function resolveNightwatchConfig() {
|
|
|
3954
5763
|
}
|
|
3955
5764
|
|
|
3956
5765
|
// src/run-lifecycle.ts
|
|
3957
|
-
var
|
|
5766
|
+
var log2 = logger2("@wdio/nightwatch-devtools:run-lifecycle");
|
|
3958
5767
|
function handleReuseMode(ctx) {
|
|
3959
5768
|
ctx.options.hostname = process.env[REUSE_ENV.HOST];
|
|
3960
5769
|
ctx.options.port = Number(process.env[REUSE_ENV.PORT]);
|
|
3961
|
-
|
|
5770
|
+
log2.info(
|
|
3962
5771
|
`\u267B Reusing DevTools backend at ${ctx.options.hostname}:${ctx.options.port}`
|
|
3963
5772
|
);
|
|
3964
5773
|
if (ctx.testReporter) {
|
|
@@ -3966,7 +5775,7 @@ function handleReuseMode(ctx) {
|
|
|
3966
5775
|
ctx.passCount = 0;
|
|
3967
5776
|
ctx.failCount = 0;
|
|
3968
5777
|
ctx.skipCount = 0;
|
|
3969
|
-
|
|
5778
|
+
log2.info("Cleared execution data for rerun");
|
|
3970
5779
|
}
|
|
3971
5780
|
}
|
|
3972
5781
|
async function runPluginBefore(ctx) {
|
|
@@ -3977,9 +5786,9 @@ async function runPluginBefore(ctx) {
|
|
|
3977
5786
|
const configPath = resolveNightwatchConfig();
|
|
3978
5787
|
ctx.setConfigPath(configPath);
|
|
3979
5788
|
if (configPath) {
|
|
3980
|
-
|
|
5789
|
+
log2.info(`\u2713 Config: ${configPath}`);
|
|
3981
5790
|
} else {
|
|
3982
|
-
|
|
5791
|
+
log2.warn(
|
|
3983
5792
|
"Could not find nightwatch config \u2014 test rerun will be unavailable"
|
|
3984
5793
|
);
|
|
3985
5794
|
}
|
|
@@ -3990,18 +5799,18 @@ async function runPluginBefore(ctx) {
|
|
|
3990
5799
|
}
|
|
3991
5800
|
try {
|
|
3992
5801
|
if (ctx.options.mode === "trace") {
|
|
3993
|
-
|
|
5802
|
+
log2.info("Trace mode \u2014 skipping backend port-bind and UI window");
|
|
3994
5803
|
} else {
|
|
3995
5804
|
ctx.options.port = await findFreePort(
|
|
3996
5805
|
ctx.options.port,
|
|
3997
5806
|
ctx.options.hostname
|
|
3998
5807
|
);
|
|
3999
|
-
|
|
5808
|
+
log2.info("\u{1F680} Starting DevTools backend...");
|
|
4000
5809
|
const { port } = await start(ctx.options);
|
|
4001
5810
|
ctx.options.port = port;
|
|
4002
5811
|
const url = `http://${ctx.options.hostname}:${ctx.options.port}`;
|
|
4003
|
-
|
|
4004
|
-
|
|
5812
|
+
log2.info(`\u2713 Backend started on port ${ctx.options.port}`);
|
|
5813
|
+
log2.info(` DevTools UI: ${url}`);
|
|
4005
5814
|
await ctx.openDevtoolsBrowserAt(url);
|
|
4006
5815
|
await new Promise(
|
|
4007
5816
|
(resolve2) => setTimeout(resolve2, TIMING.UI_CONNECTION_WAIT)
|
|
@@ -4010,18 +5819,18 @@ async function runPluginBefore(ctx) {
|
|
|
4010
5819
|
;
|
|
4011
5820
|
globalThis[PLUGIN_GLOBAL_KEY] = ctx.plugin;
|
|
4012
5821
|
} catch (err) {
|
|
4013
|
-
|
|
5822
|
+
log2.error(`Failed to start backend: ${errorMessage(err)}`);
|
|
4014
5823
|
throw err;
|
|
4015
5824
|
}
|
|
4016
5825
|
}
|
|
4017
5826
|
async function openDevtoolsBrowser(ctx, url) {
|
|
4018
5827
|
try {
|
|
4019
|
-
ctx.userDataDir =
|
|
5828
|
+
ctx.userDataDir = path11.join(
|
|
4020
5829
|
os3.tmpdir(),
|
|
4021
5830
|
`nightwatch-devtools-${ctx.options.port}-${Date.now()}`
|
|
4022
5831
|
);
|
|
4023
|
-
if (!
|
|
4024
|
-
|
|
5832
|
+
if (!fs13.existsSync(ctx.userDataDir)) {
|
|
5833
|
+
fs13.mkdirSync(ctx.userDataDir, { recursive: true });
|
|
4025
5834
|
}
|
|
4026
5835
|
ctx.devtoolsBrowser = await remote({
|
|
4027
5836
|
logLevel: "info",
|
|
@@ -4047,8 +5856,8 @@ async function openDevtoolsBrowser(ctx, url) {
|
|
|
4047
5856
|
});
|
|
4048
5857
|
await ctx.devtoolsBrowser.url(url);
|
|
4049
5858
|
} catch (err) {
|
|
4050
|
-
|
|
4051
|
-
|
|
5859
|
+
log2.error(`Failed to open DevTools UI: ${errorMessage(err)}`);
|
|
5860
|
+
log2.info(`Please manually open: ${url}`);
|
|
4052
5861
|
}
|
|
4053
5862
|
}
|
|
4054
5863
|
async function finalizeAllSuites(ctx, browser) {
|
|
@@ -4071,22 +5880,22 @@ function logRunSummary(ctx) {
|
|
|
4071
5880
|
ctx.failCount > 0 ? `${ctx.failCount} failed` : null,
|
|
4072
5881
|
ctx.skipCount > 0 ? `${ctx.skipCount} skipped` : null
|
|
4073
5882
|
].filter(Boolean).join(" ");
|
|
4074
|
-
|
|
4075
|
-
|
|
5883
|
+
log2.info(`${ctx.failCount > 0 ? "\u274C" : "\u2705"} Tests complete! ${summary}`);
|
|
5884
|
+
log2.info(` DevTools UI: http://${ctx.options.hostname}:${ctx.options.port}`);
|
|
4076
5885
|
}
|
|
4077
5886
|
async function waitForDevtoolsBrowserClose(ctx) {
|
|
4078
5887
|
if (!ctx.devtoolsBrowser) {
|
|
4079
5888
|
return;
|
|
4080
5889
|
}
|
|
4081
5890
|
;
|
|
4082
|
-
|
|
5891
|
+
logger2.setLevel(
|
|
4083
5892
|
"devtools",
|
|
4084
5893
|
"warn"
|
|
4085
5894
|
);
|
|
4086
5895
|
let exitBySignal = false;
|
|
4087
5896
|
const signalHandler = () => {
|
|
4088
5897
|
exitBySignal = true;
|
|
4089
|
-
|
|
5898
|
+
log2.info("\n\u2713 Exiting... Browser window will remain open");
|
|
4090
5899
|
process.exit(0);
|
|
4091
5900
|
};
|
|
4092
5901
|
process.once("SIGINT", signalHandler);
|
|
@@ -4097,7 +5906,7 @@ async function waitForDevtoolsBrowserClose(ctx) {
|
|
|
4097
5906
|
await new Promise((res) => setTimeout(res, TIMING.BROWSER_POLL_INTERVAL));
|
|
4098
5907
|
} catch {
|
|
4099
5908
|
if (!exitBySignal) {
|
|
4100
|
-
|
|
5909
|
+
log2.info("Browser window closed, stopping DevTools app");
|
|
4101
5910
|
break;
|
|
4102
5911
|
}
|
|
4103
5912
|
}
|
|
@@ -4107,7 +5916,7 @@ async function waitForDevtoolsBrowserClose(ctx) {
|
|
|
4107
5916
|
}
|
|
4108
5917
|
process.removeListener("SIGINT", signalHandler);
|
|
4109
5918
|
process.removeListener("SIGTERM", signalHandler);
|
|
4110
|
-
|
|
5919
|
+
logger2.setLevel(
|
|
4111
5920
|
"devtools",
|
|
4112
5921
|
"info"
|
|
4113
5922
|
);
|
|
@@ -4120,8 +5929,8 @@ async function waitForDevtoolsBrowserClose(ctx) {
|
|
|
4120
5929
|
}
|
|
4121
5930
|
|
|
4122
5931
|
// src/event-hub.ts
|
|
4123
|
-
import
|
|
4124
|
-
var
|
|
5932
|
+
import logger3 from "@wdio/logger";
|
|
5933
|
+
var log3 = logger3("@wdio/nightwatch-devtools:event-hub");
|
|
4125
5934
|
function makeSessionMetadataHandler(bindings) {
|
|
4126
5935
|
return (data) => {
|
|
4127
5936
|
try {
|
|
@@ -4142,23 +5951,23 @@ function makeSessionMetadataHandler(bindings) {
|
|
|
4142
5951
|
options: bindings.buildMetadataOptions()
|
|
4143
5952
|
});
|
|
4144
5953
|
} catch (err) {
|
|
4145
|
-
|
|
5954
|
+
log3.error(`Error in event handler: ${errorMessage(err)}`);
|
|
4146
5955
|
}
|
|
4147
5956
|
};
|
|
4148
5957
|
}
|
|
4149
5958
|
function registerEventHandlers(eventHub, bindings) {
|
|
4150
5959
|
bindings.setCucumberRunner(eventHub.runner === "cucumber");
|
|
4151
5960
|
if (eventHub.runner === "cucumber") {
|
|
4152
|
-
|
|
5961
|
+
log3.info("\u2713 Cucumber runner detected via NightwatchEventHub");
|
|
4153
5962
|
}
|
|
4154
|
-
|
|
5963
|
+
log3.info("\u2713 NightwatchEventHub registered \u2014 enriched metadata enabled");
|
|
4155
5964
|
const handler = makeSessionMetadataHandler(bindings);
|
|
4156
5965
|
eventHub.on("TestSuiteStarted", handler);
|
|
4157
5966
|
eventHub.on("TestRunStarted", handler);
|
|
4158
5967
|
}
|
|
4159
5968
|
|
|
4160
5969
|
// src/cucumber-lifecycle.ts
|
|
4161
|
-
import
|
|
5970
|
+
import logger4 from "@wdio/logger";
|
|
4162
5971
|
|
|
4163
5972
|
// src/helpers/cucumberResult.ts
|
|
4164
5973
|
function cucumberResultToTestState(result) {
|
|
@@ -4186,7 +5995,7 @@ function closeOpenSteps(suite, scenarioState, now = /* @__PURE__ */ new Date())
|
|
|
4186
5995
|
}
|
|
4187
5996
|
|
|
4188
5997
|
// src/helpers/cucumberScenarioBuilder.ts
|
|
4189
|
-
function buildScenarioStepTest(input, scenarioUid, i) {
|
|
5998
|
+
function buildScenarioStepTest(input, scenarioUid, attempt, i) {
|
|
4190
5999
|
const {
|
|
4191
6000
|
featureUri,
|
|
4192
6001
|
scenarioName,
|
|
@@ -4202,7 +6011,9 @@ function buildScenarioStepTest(input, scenarioUid, i) {
|
|
|
4202
6011
|
const stepDefLoc = findStepDefinitionLine(stepDefFiles, step.text);
|
|
4203
6012
|
const callSource = stepDefLoc ? `${stepDefLoc.filePath}:${stepDefLoc.line}` : featureAbsPath && stepLines[i] > 0 ? `${featureAbsPath}:${stepLines[i]}` : void 0;
|
|
4204
6013
|
return {
|
|
4205
|
-
uid
|
|
6014
|
+
// Scope by the scenario uid (which carries the scenario line) so identical
|
|
6015
|
+
// step text in sibling scenarios and outline example rows stays distinct.
|
|
6016
|
+
uid: deterministicUid(featureUri, `step:${scenarioUid}:${step.text}`),
|
|
4206
6017
|
cid: DEFAULTS.CID,
|
|
4207
6018
|
title: stepLabel,
|
|
4208
6019
|
fullTitle: `${scenarioName} ${stepLabel}`,
|
|
@@ -4212,7 +6023,9 @@ function buildScenarioStepTest(input, scenarioUid, i) {
|
|
|
4212
6023
|
end: null,
|
|
4213
6024
|
type: "test",
|
|
4214
6025
|
file: featureUri,
|
|
4215
|
-
|
|
6026
|
+
// Scenario-level attempt from the tracker (0 first run, +1 per retry);
|
|
6027
|
+
// flows to TestOutcome.attempt via collectSuiteTestMetadata.
|
|
6028
|
+
retries: attempt,
|
|
4216
6029
|
_duration: 0,
|
|
4217
6030
|
hooks: [],
|
|
4218
6031
|
callSource
|
|
@@ -4228,7 +6041,11 @@ function buildCucumberScenarioSuite(input) {
|
|
|
4228
6041
|
scenarioLine,
|
|
4229
6042
|
parentFeatureSuiteUid
|
|
4230
6043
|
} = input;
|
|
4231
|
-
const scenarioUid = deterministicUid(
|
|
6044
|
+
const scenarioUid = deterministicUid(
|
|
6045
|
+
featureUri,
|
|
6046
|
+
`scenario:${scenarioName}:${scenarioLine}`
|
|
6047
|
+
);
|
|
6048
|
+
const attempt = input.recordAttempt?.(scenarioUid, featureUri) ?? 0;
|
|
4232
6049
|
const scenarioSuite = {
|
|
4233
6050
|
uid: scenarioUid,
|
|
4234
6051
|
cid: DEFAULTS.CID,
|
|
@@ -4247,49 +6064,51 @@ function buildCucumberScenarioSuite(input) {
|
|
|
4247
6064
|
callSource: featureAbsPath && scenarioLine > 0 ? `${featureAbsPath}:${scenarioLine}` : void 0
|
|
4248
6065
|
};
|
|
4249
6066
|
for (let i = 0; i < steps.length; i++) {
|
|
4250
|
-
scenarioSuite.tests.push(
|
|
6067
|
+
scenarioSuite.tests.push(
|
|
6068
|
+
buildScenarioStepTest(input, scenarioUid, attempt, i)
|
|
6069
|
+
);
|
|
4251
6070
|
}
|
|
4252
6071
|
return scenarioSuite;
|
|
4253
6072
|
}
|
|
4254
6073
|
|
|
4255
6074
|
// src/helpers/featureFileScan.ts
|
|
4256
|
-
import
|
|
4257
|
-
import
|
|
6075
|
+
import fs14 from "fs";
|
|
6076
|
+
import path12 from "path";
|
|
4258
6077
|
function scanFeatureFile(featureUri) {
|
|
4259
|
-
const featureAbsPath =
|
|
6078
|
+
const featureAbsPath = path12.resolve(process.cwd(), featureUri);
|
|
4260
6079
|
const result = {
|
|
4261
|
-
featureName:
|
|
6080
|
+
featureName: path12.basename(featureUri, ".feature"),
|
|
4262
6081
|
featureContent: "",
|
|
4263
6082
|
featureAbsPath,
|
|
4264
6083
|
stepDefFiles: [],
|
|
4265
6084
|
capturedPaths: []
|
|
4266
6085
|
};
|
|
4267
|
-
if (featureUri === "unknown.feature" || !
|
|
6086
|
+
if (featureUri === "unknown.feature" || !fs14.existsSync(featureAbsPath)) {
|
|
4268
6087
|
return result;
|
|
4269
6088
|
}
|
|
4270
|
-
result.featureContent =
|
|
6089
|
+
result.featureContent = fs14.readFileSync(featureAbsPath, "utf-8");
|
|
4271
6090
|
const match = result.featureContent.match(/^\s*Feature:\s*(.+)/m);
|
|
4272
6091
|
if (match) {
|
|
4273
6092
|
result.featureName = match[1].trim();
|
|
4274
6093
|
}
|
|
4275
6094
|
result.capturedPaths.push(featureAbsPath);
|
|
4276
|
-
const featureDir =
|
|
6095
|
+
const featureDir = path12.dirname(featureAbsPath);
|
|
4277
6096
|
const stepDirCandidates = ["step_definitions", "steps", "support"];
|
|
4278
6097
|
for (const candidate of stepDirCandidates) {
|
|
4279
|
-
const stepDir =
|
|
4280
|
-
if (!
|
|
6098
|
+
const stepDir = path12.join(featureDir, candidate);
|
|
6099
|
+
if (!fs14.existsSync(stepDir) || !fs14.statSync(stepDir).isDirectory()) {
|
|
4281
6100
|
continue;
|
|
4282
6101
|
}
|
|
4283
|
-
for (const entry of
|
|
6102
|
+
for (const entry of fs14.readdirSync(stepDir)) {
|
|
4284
6103
|
if (!/\.(js|ts|mjs|cjs)$/.test(entry)) {
|
|
4285
6104
|
continue;
|
|
4286
6105
|
}
|
|
4287
|
-
const stepFilePath =
|
|
6106
|
+
const stepFilePath = path12.join(stepDir, entry);
|
|
4288
6107
|
result.capturedPaths.push(stepFilePath);
|
|
4289
6108
|
try {
|
|
4290
6109
|
result.stepDefFiles.push({
|
|
4291
6110
|
filePath: stepFilePath,
|
|
4292
|
-
content:
|
|
6111
|
+
content: fs14.readFileSync(stepFilePath, "utf-8")
|
|
4293
6112
|
});
|
|
4294
6113
|
} catch {
|
|
4295
6114
|
}
|
|
@@ -4298,8 +6117,50 @@ function scanFeatureFile(featureUri) {
|
|
|
4298
6117
|
return result;
|
|
4299
6118
|
}
|
|
4300
6119
|
|
|
6120
|
+
// src/trace-slices.ts
|
|
6121
|
+
function sliceActive(ctx) {
|
|
6122
|
+
return ctx.traceMode && ctx.traceGranularity === "test";
|
|
6123
|
+
}
|
|
6124
|
+
function boundaryContext(ctx) {
|
|
6125
|
+
return {
|
|
6126
|
+
specRanges: ctx.specRanges,
|
|
6127
|
+
flushedSpecs: ctx.flushedSpecs,
|
|
6128
|
+
capturer: ctx.sessionCapturer
|
|
6129
|
+
};
|
|
6130
|
+
}
|
|
6131
|
+
function flushPrevious(ctx, prevRange) {
|
|
6132
|
+
if (!prevRange) {
|
|
6133
|
+
return;
|
|
6134
|
+
}
|
|
6135
|
+
void ctx.flushTraceRange(prevRange);
|
|
6136
|
+
}
|
|
6137
|
+
function recordSpecSliceBoundary(ctx, specFile) {
|
|
6138
|
+
const prevRange = recordSpecBoundary(
|
|
6139
|
+
boundaryContext(ctx),
|
|
6140
|
+
specFile,
|
|
6141
|
+
ctx.traceGranularity
|
|
6142
|
+
);
|
|
6143
|
+
flushPrevious(ctx, prevRange);
|
|
6144
|
+
}
|
|
6145
|
+
function recordTestSliceBoundary(ctx, specFile, testUid) {
|
|
6146
|
+
if (!sliceActive(ctx)) {
|
|
6147
|
+
return;
|
|
6148
|
+
}
|
|
6149
|
+
recordSliceBoundary(boundaryContext(ctx), "test", specFile, testUid);
|
|
6150
|
+
}
|
|
6151
|
+
function flushTestSlice(ctx) {
|
|
6152
|
+
if (!sliceActive(ctx)) {
|
|
6153
|
+
return;
|
|
6154
|
+
}
|
|
6155
|
+
const range = findFlushableRange(ctx.specRanges);
|
|
6156
|
+
if (!range) {
|
|
6157
|
+
return;
|
|
6158
|
+
}
|
|
6159
|
+
void ctx.flushTraceRange(range);
|
|
6160
|
+
}
|
|
6161
|
+
|
|
4301
6162
|
// src/cucumber-lifecycle.ts
|
|
4302
|
-
var
|
|
6163
|
+
var log4 = logger4("@wdio/nightwatch-devtools:cucumber");
|
|
4303
6164
|
function attachScenarioToFeature(ctx, featureSuite, scenarioSuite) {
|
|
4304
6165
|
const existingIdx = featureSuite.suites.findIndex(
|
|
4305
6166
|
(s) => s.uid === scenarioSuite.uid
|
|
@@ -4335,6 +6196,12 @@ function createFeatureSuite(ctx, featureUri, featureName, featureContent, featur
|
|
|
4335
6196
|
function normalizeSteps(pickleSteps) {
|
|
4336
6197
|
return (pickleSteps ?? []).map((s) => ({ text: s.text ?? "" }));
|
|
4337
6198
|
}
|
|
6199
|
+
function captureFeatureSources(ctx, paths) {
|
|
6200
|
+
for (const p of paths) {
|
|
6201
|
+
ctx.sessionCapturer.captureSource(p).catch(() => {
|
|
6202
|
+
});
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
4338
6205
|
async function initCucumberScenario(ctx, browser, pickle) {
|
|
4339
6206
|
await ctx.ensureSessionInitialized(browser);
|
|
4340
6207
|
const featureUri = pickle.uri ?? "unknown.feature";
|
|
@@ -4347,10 +6214,7 @@ async function initCucumberScenario(ctx, browser, pickle) {
|
|
|
4347
6214
|
stepDefFiles,
|
|
4348
6215
|
capturedPaths
|
|
4349
6216
|
} = scanFeatureFile(featureUri);
|
|
4350
|
-
|
|
4351
|
-
ctx.sessionCapturer.captureSource(p).catch(() => {
|
|
4352
|
-
});
|
|
4353
|
-
}
|
|
6217
|
+
captureFeatureSources(ctx, capturedPaths);
|
|
4354
6218
|
const { featureSuite, scenarioLine, stepLines, stepKeywords } = createFeatureSuite(
|
|
4355
6219
|
ctx,
|
|
4356
6220
|
featureUri,
|
|
@@ -4370,15 +6234,17 @@ async function initCucumberScenario(ctx, browser, pickle) {
|
|
|
4370
6234
|
stepLines,
|
|
4371
6235
|
stepKeywords,
|
|
4372
6236
|
scenarioLine,
|
|
4373
|
-
parentFeatureSuiteUid: featureSuite.uid
|
|
6237
|
+
parentFeatureSuiteUid: featureSuite.uid,
|
|
6238
|
+
recordAttempt: (uid, specFile) => ctx.recordAttempt(uid, specFile)
|
|
4374
6239
|
});
|
|
4375
6240
|
attachScenarioToFeature(ctx, featureSuite, scenarioSuite);
|
|
6241
|
+
recordTestSliceBoundary(ctx, featureUri, scenarioSuite.uid);
|
|
4376
6242
|
ctx.setCurrentScenarioSuite(scenarioSuite);
|
|
4377
6243
|
ctx.setCurrentStep(null);
|
|
4378
6244
|
ctx.setCurrentTest(null);
|
|
4379
6245
|
ctx.testReporter.updateSuites();
|
|
4380
6246
|
ctx.wrapBrowserOnce(browser);
|
|
4381
|
-
|
|
6247
|
+
log4.info(`\u{1F952} Scenario: ${scenarioName}`);
|
|
4382
6248
|
}
|
|
4383
6249
|
async function finalizeCucumberScenario(ctx, browser, result, pickle) {
|
|
4384
6250
|
try {
|
|
@@ -4390,6 +6256,7 @@ async function finalizeCucumberScenario(ctx, browser, result, pickle) {
|
|
|
4390
6256
|
scenario.state = scenarioState;
|
|
4391
6257
|
scenario.end = now;
|
|
4392
6258
|
scenario._duration = duration;
|
|
6259
|
+
ctx.recordOutcome(scenario.uid, scenarioState);
|
|
4393
6260
|
closeOpenSteps(scenario, scenarioState, now);
|
|
4394
6261
|
const featureUri = pickle?.uri ?? "unknown.feature";
|
|
4395
6262
|
ctx.testManager.markTestAsProcessed(featureUri, pickle?.name ?? "");
|
|
@@ -4399,7 +6266,7 @@ async function finalizeCucumberScenario(ctx, browser, result, pickle) {
|
|
|
4399
6266
|
}
|
|
4400
6267
|
ctx.incrementCount(scenarioState);
|
|
4401
6268
|
const icon = ctx.testIcon(scenarioState);
|
|
4402
|
-
|
|
6269
|
+
log4.info(
|
|
4403
6270
|
` ${icon} ${pickle?.name ?? "Unknown"} (${(duration / 1e3).toFixed(2)}s)`
|
|
4404
6271
|
);
|
|
4405
6272
|
ctx.testReporter.updateSuites();
|
|
@@ -4408,8 +6275,13 @@ async function finalizeCucumberScenario(ctx, browser, result, pickle) {
|
|
|
4408
6275
|
ctx.setCurrentTest(null);
|
|
4409
6276
|
}
|
|
4410
6277
|
await ctx.sessionCapturer.captureTrace(browser);
|
|
6278
|
+
flushTestSlice(ctx);
|
|
6279
|
+
await ctx.emitTestArtifacts(
|
|
6280
|
+
scenario?.uid,
|
|
6281
|
+
scenarioState === TEST_STATE.FAILED
|
|
6282
|
+
);
|
|
4411
6283
|
} catch (err) {
|
|
4412
|
-
|
|
6284
|
+
log4.error(`Failed to finalize Cucumber scenario: ${errorMessage(err)}`);
|
|
4413
6285
|
}
|
|
4414
6286
|
}
|
|
4415
6287
|
async function cucumberBeforeStep(ctx, _browser, pickleStep, _pickle) {
|
|
@@ -4453,11 +6325,11 @@ async function cucumberAfter(ctx, browser, result, pickle) {
|
|
|
4453
6325
|
}
|
|
4454
6326
|
|
|
4455
6327
|
// src/test-lifecycle.ts
|
|
4456
|
-
import
|
|
6328
|
+
import logger6 from "@wdio/logger";
|
|
4457
6329
|
|
|
4458
6330
|
// src/helpers/specFileResolver.ts
|
|
4459
|
-
import
|
|
4460
|
-
import
|
|
6331
|
+
import fs15 from "fs";
|
|
6332
|
+
import path13 from "path";
|
|
4461
6333
|
function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
|
|
4462
6334
|
let fullPath = findTestFileFromStack() || null;
|
|
4463
6335
|
if (!fullPath && cachedPath && cachedPath.includes(testFile)) {
|
|
@@ -4473,27 +6345,27 @@ function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
|
|
|
4473
6345
|
const normalized = (modulePath || "").replace(/\\/g, "/");
|
|
4474
6346
|
const srcFolderPaths = srcFolders.flatMap(
|
|
4475
6347
|
(sf) => normalized ? [
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
6348
|
+
path13.resolve(workspaceRoot, sf, normalized + ".js"),
|
|
6349
|
+
path13.resolve(workspaceRoot, sf, normalized + ".ts"),
|
|
6350
|
+
path13.resolve(workspaceRoot, sf, normalized + ".cjs"),
|
|
6351
|
+
path13.resolve(workspaceRoot, sf, normalized)
|
|
4480
6352
|
] : []
|
|
4481
6353
|
);
|
|
4482
6354
|
const possiblePaths = [
|
|
4483
6355
|
...srcFolderPaths,
|
|
4484
6356
|
// Treat module path as relative to cwd (works when src_folders isn't nested)
|
|
4485
6357
|
...normalized ? [
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
6358
|
+
path13.resolve(workspaceRoot, normalized + ".js"),
|
|
6359
|
+
path13.resolve(workspaceRoot, normalized + ".ts"),
|
|
6360
|
+
path13.resolve(workspaceRoot, normalized + ".cjs"),
|
|
6361
|
+
path13.resolve(workspaceRoot, normalized)
|
|
4490
6362
|
] : [],
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
6363
|
+
path13.resolve(workspaceRoot, "tests", testFile + ".js"),
|
|
6364
|
+
path13.resolve(workspaceRoot, "test", testFile + ".js"),
|
|
6365
|
+
path13.resolve(workspaceRoot, testFile + ".js")
|
|
4494
6366
|
];
|
|
4495
6367
|
for (const candidate of possiblePaths) {
|
|
4496
|
-
if (
|
|
6368
|
+
if (fs15.existsSync(candidate)) {
|
|
4497
6369
|
return candidate;
|
|
4498
6370
|
}
|
|
4499
6371
|
}
|
|
@@ -4501,8 +6373,8 @@ function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
|
|
|
4501
6373
|
}
|
|
4502
6374
|
|
|
4503
6375
|
// src/helpers/closePreviousTest.ts
|
|
4504
|
-
import
|
|
4505
|
-
var
|
|
6376
|
+
import logger5 from "@wdio/logger";
|
|
6377
|
+
var log5 = logger5("@wdio/nightwatch-devtools:closePreviousTest");
|
|
4506
6378
|
async function closePreviousTest(input) {
|
|
4507
6379
|
const {
|
|
4508
6380
|
runningTest,
|
|
@@ -4521,7 +6393,7 @@ async function closePreviousTest(input) {
|
|
|
4521
6393
|
testManager.updateTestState(runningTest, testState);
|
|
4522
6394
|
testManager.markTestAsProcessed(testFile, runningTest.title);
|
|
4523
6395
|
incrementCount(testState);
|
|
4524
|
-
|
|
6396
|
+
log5.info(
|
|
4525
6397
|
` ${testIcon(testState)} ${runningTest.title} (${(runningTest._duration / 1e3).toFixed(2)}s)`
|
|
4526
6398
|
);
|
|
4527
6399
|
} else {
|
|
@@ -4535,13 +6407,13 @@ async function closePreviousTest(input) {
|
|
|
4535
6407
|
);
|
|
4536
6408
|
testManager.markTestAsProcessed(testFile, runningTest.title);
|
|
4537
6409
|
incrementCount(TEST_STATE.PASSED);
|
|
4538
|
-
|
|
6410
|
+
log5.info(` \u2705 ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
|
|
4539
6411
|
}
|
|
4540
6412
|
await new Promise((resolve2) => setTimeout(resolve2, TIMING.UI_RENDER_DELAY));
|
|
4541
6413
|
}
|
|
4542
6414
|
|
|
4543
6415
|
// src/test-lifecycle.ts
|
|
4544
|
-
var
|
|
6416
|
+
var log6 = logger6("@wdio/nightwatch-devtools:test-lifecycle");
|
|
4545
6417
|
function resolveSuiteMetadata(ctx, currentTest) {
|
|
4546
6418
|
const moduleName = currentTest.module ?? "";
|
|
4547
6419
|
const testFile = moduleName.split("/").pop() || moduleName || DEFAULTS.FILE_NAME;
|
|
@@ -4552,7 +6424,7 @@ function resolveSuiteMetadata(ctx, currentTest) {
|
|
|
4552
6424
|
ctx.browserProxy.getCurrentTestFullPath() || void 0
|
|
4553
6425
|
);
|
|
4554
6426
|
if (!fullPath) {
|
|
4555
|
-
|
|
6427
|
+
log6.warn(
|
|
4556
6428
|
`[beforeEach] Could not resolve file path for "${testFile}" \u2014 source view will be unavailable`
|
|
4557
6429
|
);
|
|
4558
6430
|
}
|
|
@@ -4586,21 +6458,25 @@ function pickCurrentTestName(currentTest, testNames, processedTests) {
|
|
|
4586
6458
|
) : void 0;
|
|
4587
6459
|
return matchedRuntimeTestName || testNames.find((name) => !processedTests.has(name));
|
|
4588
6460
|
}
|
|
4589
|
-
async function startNextTest(ctx, currentSuite, currentTestName, processedTests) {
|
|
6461
|
+
async function startNextTest(ctx, currentSuite, currentTestName, processedTests, specFile) {
|
|
4590
6462
|
if (processedTests.size === 0) {
|
|
4591
6463
|
ctx.suiteManager.markSuiteAsRunning(currentSuite);
|
|
4592
6464
|
}
|
|
4593
6465
|
const test = ctx.testManager.findTestInSuite(currentSuite, currentTestName);
|
|
4594
6466
|
if (test) {
|
|
6467
|
+
test.retries = ctx.recordAttempt(test.uid, specFile ?? void 0);
|
|
6468
|
+
if (specFile) {
|
|
6469
|
+
recordTestSliceBoundary(ctx, specFile, test.uid);
|
|
6470
|
+
}
|
|
4595
6471
|
test.state = TEST_STATE.RUNNING;
|
|
4596
6472
|
test.start = /* @__PURE__ */ new Date();
|
|
4597
6473
|
test.end = null;
|
|
4598
6474
|
ctx.testReporter.onTestStart(test);
|
|
4599
6475
|
ctx.setCurrentTest(test);
|
|
4600
|
-
|
|
6476
|
+
log6.info(` \u25B6 ${currentTestName}`);
|
|
4601
6477
|
await new Promise((resolve2) => setTimeout(resolve2, TIMING.TEST_START_DELAY));
|
|
4602
6478
|
} else {
|
|
4603
|
-
|
|
6479
|
+
log6.warn(
|
|
4604
6480
|
`Test "${currentTestName}" not found in suite "${currentSuite.title}"`
|
|
4605
6481
|
);
|
|
4606
6482
|
ctx.setCurrentTest(null);
|
|
@@ -4645,7 +6521,7 @@ function closeUnreportedRunningTest(ctx, currentSuite, testFile, results, proces
|
|
|
4645
6521
|
ctx.testManager.markTestAsProcessed(testFile, runningTest.title);
|
|
4646
6522
|
ctx.incrementCount(testState);
|
|
4647
6523
|
const icon = ctx.testIcon(testState);
|
|
4648
|
-
|
|
6524
|
+
log6.info(` ${icon} ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
|
|
4649
6525
|
}
|
|
4650
6526
|
async function closeReportedTestcases(ctx, currentSuite, testFile, testcases, processedTests) {
|
|
4651
6527
|
const testcaseNames = Object.keys(testcases);
|
|
@@ -4661,7 +6537,7 @@ async function closeReportedTestcases(ctx, currentSuite, testFile, testcases, pr
|
|
|
4661
6537
|
ctx.testManager.updateTestState(test, testState, /* @__PURE__ */ new Date(), dur);
|
|
4662
6538
|
ctx.incrementCount(testState);
|
|
4663
6539
|
const icon = ctx.testIcon(testState);
|
|
4664
|
-
|
|
6540
|
+
log6.info(` ${icon} ${currentTestName} (${(dur / 1e3).toFixed(2)}s)`);
|
|
4665
6541
|
}
|
|
4666
6542
|
ctx.testManager.markTestAsProcessed(testFile, currentTestName);
|
|
4667
6543
|
}
|
|
@@ -4703,11 +6579,11 @@ async function closeOutTestcases(ctx, browser) {
|
|
|
4703
6579
|
}
|
|
4704
6580
|
|
|
4705
6581
|
// src/session-init.ts
|
|
4706
|
-
import
|
|
6582
|
+
import logger12 from "@wdio/logger";
|
|
4707
6583
|
|
|
4708
6584
|
// src/session.ts
|
|
4709
6585
|
import http from "http";
|
|
4710
|
-
import
|
|
6586
|
+
import logger7 from "@wdio/logger";
|
|
4711
6587
|
|
|
4712
6588
|
// src/action-snapshot.ts
|
|
4713
6589
|
function captureActionSnapshot2(browser, command, takeScreenshot) {
|
|
@@ -4845,7 +6721,7 @@ function dedupeNetworkRequests(incoming, existing) {
|
|
|
4845
6721
|
}
|
|
4846
6722
|
|
|
4847
6723
|
// src/session.ts
|
|
4848
|
-
var
|
|
6724
|
+
var log7 = logger7("@wdio/nightwatch-devtools:SessionCapturer");
|
|
4849
6725
|
function unwrapDriverValue(result) {
|
|
4850
6726
|
if (result && typeof result === "object" && "value" in result) {
|
|
4851
6727
|
return result.value;
|
|
@@ -4853,7 +6729,7 @@ function unwrapDriverValue(result) {
|
|
|
4853
6729
|
return result;
|
|
4854
6730
|
}
|
|
4855
6731
|
var getProp = (obj, key) => obj && typeof obj === "object" ? obj[key] : void 0;
|
|
4856
|
-
var getPath = (obj, ...
|
|
6732
|
+
var getPath = (obj, ...path14) => path14.reduce((acc, k) => getProp(acc, k), obj);
|
|
4857
6733
|
var firstDefined = (obj, ...keys) => {
|
|
4858
6734
|
if (!obj || typeof obj !== "object") {
|
|
4859
6735
|
return void 0;
|
|
@@ -4901,13 +6777,13 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
4901
6777
|
this.patchStreams();
|
|
4902
6778
|
}
|
|
4903
6779
|
onWsOpen() {
|
|
4904
|
-
|
|
6780
|
+
log7.info("\u2713 Worker WebSocket connected to backend");
|
|
4905
6781
|
}
|
|
4906
6782
|
onWsError(err) {
|
|
4907
|
-
|
|
6783
|
+
log7.error(`Couldn't connect to devtools backend: ${errorMessage(err)}`);
|
|
4908
6784
|
}
|
|
4909
6785
|
onWsClose() {
|
|
4910
|
-
|
|
6786
|
+
log7.info("Worker WebSocket disconnected");
|
|
4911
6787
|
}
|
|
4912
6788
|
async captureCommand(command, args, result, error, testUid, callSource, timestamp) {
|
|
4913
6789
|
const serializedError = serializeError(error);
|
|
@@ -4928,7 +6804,7 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
4928
6804
|
);
|
|
4929
6805
|
if (isNavigationCommand && this.#browser && !error) {
|
|
4930
6806
|
this.#capturePerformanceData(commandLogEntry, args).catch((err) => {
|
|
4931
|
-
|
|
6807
|
+
log7.warn(`Failed to capture performance data: ${errorMessage(err)}`);
|
|
4932
6808
|
});
|
|
4933
6809
|
}
|
|
4934
6810
|
if (this.traceMode === "trace" && !error && this.#browser && mapCommandToAction(command)) {
|
|
@@ -4959,6 +6835,37 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
4959
6835
|
typeof args[0] === "string" ? args[0] : void 0
|
|
4960
6836
|
);
|
|
4961
6837
|
}
|
|
6838
|
+
/**
|
|
6839
|
+
* Ingest a pre-built assertion entry (native `browser.assert`/`verify`
|
|
6840
|
+
* synthesized from Nightwatch's results, or a node:assert capture) into the
|
|
6841
|
+
* same command stream driver commands use. Unlike {@link captureCommand} this
|
|
6842
|
+
* preserves the entry's `title` (the human assertion message) and never
|
|
6843
|
+
* dedups — each call is a distinct action row. Assigns a fresh `_id` and a
|
|
6844
|
+
* matching stable public `id` (so a later `sendReplaceCommand` can update
|
|
6845
|
+
* this exact row in place — native asserts stream a pending row at call time,
|
|
6846
|
+
* then finalize their pass/fail), pushes, and broadcasts.
|
|
6847
|
+
*/
|
|
6848
|
+
captureAssertCommand(entry) {
|
|
6849
|
+
const withId = entry;
|
|
6850
|
+
withId._id = this.commandCounter++;
|
|
6851
|
+
withId.id = withId._id;
|
|
6852
|
+
this.commandsLog.push(withId);
|
|
6853
|
+
if (this.traceMode === "trace" && !entry.error && this.#browser && mapCommandToAction(entry.command)) {
|
|
6854
|
+
const browser = this.#browser;
|
|
6855
|
+
this.snapshotCaptures.push(
|
|
6856
|
+
captureActionSnapshot2(
|
|
6857
|
+
browser,
|
|
6858
|
+
entry.command,
|
|
6859
|
+
() => this.takeScreenshotViaHttp(browser)
|
|
6860
|
+
).then((snap) => {
|
|
6861
|
+
if (snap) {
|
|
6862
|
+
this.actionSnapshots.push(snap);
|
|
6863
|
+
}
|
|
6864
|
+
})
|
|
6865
|
+
);
|
|
6866
|
+
}
|
|
6867
|
+
this.sendCommand(withId);
|
|
6868
|
+
}
|
|
4962
6869
|
/**
|
|
4963
6870
|
* Replace an already-captured command entry (used for retried commands so
|
|
4964
6871
|
* only the final execution result is shown in the UI).
|
|
@@ -5018,38 +6925,38 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
5018
6925
|
try {
|
|
5019
6926
|
const value = JSON.parse(body).value || null;
|
|
5020
6927
|
if (!value) {
|
|
5021
|
-
|
|
6928
|
+
log7.warn(`[screenshot] Empty response from ${endpoint}`);
|
|
5022
6929
|
}
|
|
5023
6930
|
resolve2(value);
|
|
5024
6931
|
} catch {
|
|
5025
|
-
|
|
6932
|
+
log7.warn(`[screenshot] Failed to parse response from ${endpoint}`);
|
|
5026
6933
|
resolve2(null);
|
|
5027
6934
|
}
|
|
5028
6935
|
});
|
|
5029
6936
|
});
|
|
5030
6937
|
req.on("error", (err) => {
|
|
5031
|
-
|
|
6938
|
+
log7.warn(
|
|
5032
6939
|
`[screenshot] HTTP request failed (${endpoint}): ${errorMessage(err)}`
|
|
5033
6940
|
);
|
|
5034
6941
|
resolve2(null);
|
|
5035
6942
|
});
|
|
5036
6943
|
req.setTimeout(5e3, () => {
|
|
5037
|
-
|
|
6944
|
+
log7.warn(`[screenshot] Request timed out (${endpoint})`);
|
|
5038
6945
|
req.destroy();
|
|
5039
6946
|
resolve2(null);
|
|
5040
6947
|
});
|
|
5041
6948
|
});
|
|
5042
6949
|
}
|
|
5043
6950
|
onSourceReadError(filePath, err) {
|
|
5044
|
-
|
|
6951
|
+
log7.warn(`Failed to read source file ${filePath}: ${errorMessage(err)}`);
|
|
5045
6952
|
}
|
|
5046
6953
|
onUpstreamDrop(event, reason, err) {
|
|
5047
6954
|
if (reason === "send-error") {
|
|
5048
|
-
|
|
6955
|
+
log7.warn(`[upstream] Failed to send "${event}": ${errorMessage(err)}`);
|
|
5049
6956
|
return;
|
|
5050
6957
|
}
|
|
5051
6958
|
if (this.hasEverConnected()) {
|
|
5052
|
-
|
|
6959
|
+
log7.warn(`[upstream] WebSocket not open \u2014 dropping "${event}" event`);
|
|
5053
6960
|
}
|
|
5054
6961
|
}
|
|
5055
6962
|
/**
|
|
@@ -5072,12 +6979,12 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
5072
6979
|
return unwrapDriverValue(checkResult) === true;
|
|
5073
6980
|
});
|
|
5074
6981
|
if (hasCollector) {
|
|
5075
|
-
|
|
6982
|
+
log7.info("\u2713 Script injected and collector ready");
|
|
5076
6983
|
} else {
|
|
5077
|
-
|
|
6984
|
+
log7.warn("Script injection may have failed \u2014 collector not found");
|
|
5078
6985
|
}
|
|
5079
6986
|
} catch (err) {
|
|
5080
|
-
|
|
6987
|
+
log7.error(`Failed to inject script: ${errorMessage(err)}`);
|
|
5081
6988
|
throw err;
|
|
5082
6989
|
}
|
|
5083
6990
|
}
|
|
@@ -5131,7 +7038,7 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
5131
7038
|
} catch (err) {
|
|
5132
7039
|
const msg = errorMessage(err) ?? "";
|
|
5133
7040
|
if (!msg.includes("log type") && !msg.includes("performance")) {
|
|
5134
|
-
|
|
7041
|
+
log7.warn(`Performance log capture failed: ${msg}`);
|
|
5135
7042
|
}
|
|
5136
7043
|
}
|
|
5137
7044
|
}
|
|
@@ -5158,10 +7065,10 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
5158
7065
|
traceData.mutations
|
|
5159
7066
|
) ? traceData.mutations.length : 0;
|
|
5160
7067
|
if (mutationCount > 0) {
|
|
5161
|
-
|
|
7068
|
+
log7.info(`[trace] Captured ${mutationCount} DOM mutation(s)`);
|
|
5162
7069
|
}
|
|
5163
7070
|
} catch (err) {
|
|
5164
|
-
|
|
7071
|
+
log7.error(
|
|
5165
7072
|
`Failed to capture trace from injected script: ${errorMessage(err)}`
|
|
5166
7073
|
);
|
|
5167
7074
|
}
|
|
@@ -5169,12 +7076,17 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
5169
7076
|
};
|
|
5170
7077
|
|
|
5171
7078
|
// src/reporter.ts
|
|
5172
|
-
import
|
|
5173
|
-
var
|
|
7079
|
+
import logger8 from "@wdio/logger";
|
|
7080
|
+
var log8 = logger8("@wdio/nightwatch-devtools:Reporter");
|
|
5174
7081
|
var TestReporter = class extends TestReporterBase {
|
|
5175
7082
|
#currentSpecFile;
|
|
5176
7083
|
#testNamesCache = /* @__PURE__ */ new Map();
|
|
5177
7084
|
#currentSuite;
|
|
7085
|
+
#attemptFor;
|
|
7086
|
+
constructor(report, attemptFor) {
|
|
7087
|
+
super(report);
|
|
7088
|
+
this.#attemptFor = attemptFor;
|
|
7089
|
+
}
|
|
5178
7090
|
onSuiteStart(suiteStats) {
|
|
5179
7091
|
this.#currentSpecFile = suiteStats.file;
|
|
5180
7092
|
this.#currentSuite = suiteStats;
|
|
@@ -5187,7 +7099,7 @@ var TestReporter = class extends TestReporterBase {
|
|
|
5187
7099
|
const testNames = rerunLabel ? metadata.testNames.filter((name) => name === rerunLabel) : metadata.testNames;
|
|
5188
7100
|
if (testNames.length > 0) {
|
|
5189
7101
|
this.#testNamesCache.set(this.#currentSpecFile, testNames);
|
|
5190
|
-
|
|
7102
|
+
log8.info(
|
|
5191
7103
|
`\u{1F4DD} Extracted ${testNames.length} test names from ${this.#currentSpecFile}`
|
|
5192
7104
|
);
|
|
5193
7105
|
}
|
|
@@ -5235,11 +7147,12 @@ var TestReporter = class extends TestReporterBase {
|
|
|
5235
7147
|
);
|
|
5236
7148
|
cachedNames.forEach((testName) => {
|
|
5237
7149
|
if (!processedTestNames.has(testName)) {
|
|
7150
|
+
const uid = generateStableUid2({
|
|
7151
|
+
file: suiteStats.file,
|
|
7152
|
+
fullTitle: `${suiteStats.title} ${testName}`
|
|
7153
|
+
});
|
|
5238
7154
|
const skippedTest = {
|
|
5239
|
-
uid
|
|
5240
|
-
file: suiteStats.file,
|
|
5241
|
-
fullTitle: `${suiteStats.title} ${testName}`
|
|
5242
|
-
}),
|
|
7155
|
+
uid,
|
|
5243
7156
|
cid: DEFAULTS.CID,
|
|
5244
7157
|
title: testName,
|
|
5245
7158
|
fullTitle: `${suiteStats.title} ${testName}`,
|
|
@@ -5249,12 +7162,12 @@ var TestReporter = class extends TestReporterBase {
|
|
|
5249
7162
|
end: /* @__PURE__ */ new Date(),
|
|
5250
7163
|
type: "test",
|
|
5251
7164
|
file: suiteStats.file,
|
|
5252
|
-
retries: DEFAULTS.RETRIES,
|
|
7165
|
+
retries: this.#attemptFor(uid) ?? DEFAULTS.RETRIES,
|
|
5253
7166
|
_duration: DEFAULTS.DURATION,
|
|
5254
7167
|
hooks: []
|
|
5255
7168
|
};
|
|
5256
7169
|
suiteStats.tests.push(skippedTest);
|
|
5257
|
-
|
|
7170
|
+
log8.info(`Created skipped test "${testName}" (never executed)`);
|
|
5258
7171
|
}
|
|
5259
7172
|
});
|
|
5260
7173
|
this.sendUpstream();
|
|
@@ -5273,10 +7186,12 @@ var TestReporter = class extends TestReporterBase {
|
|
|
5273
7186
|
|
|
5274
7187
|
// src/helpers/testManager.ts
|
|
5275
7188
|
var TestManager = class {
|
|
5276
|
-
constructor(testReporter) {
|
|
7189
|
+
constructor(testReporter, recordOutcome) {
|
|
5277
7190
|
this.testReporter = testReporter;
|
|
7191
|
+
this.recordOutcome = recordOutcome;
|
|
5278
7192
|
}
|
|
5279
7193
|
testReporter;
|
|
7194
|
+
recordOutcome;
|
|
5280
7195
|
processedTests = /* @__PURE__ */ new Map();
|
|
5281
7196
|
lastKnownTestName = null;
|
|
5282
7197
|
/**
|
|
@@ -5299,6 +7214,7 @@ var TestManager = class {
|
|
|
5299
7214
|
}
|
|
5300
7215
|
if (state !== TEST_STATE.RUNNING) {
|
|
5301
7216
|
this.testReporter.onTestEnd(test);
|
|
7217
|
+
this.recordOutcome?.(test.uid, state);
|
|
5302
7218
|
}
|
|
5303
7219
|
}
|
|
5304
7220
|
/**
|
|
@@ -5518,7 +7434,7 @@ var SuiteManager = class {
|
|
|
5518
7434
|
};
|
|
5519
7435
|
|
|
5520
7436
|
// src/helpers/browserProxy.ts
|
|
5521
|
-
import
|
|
7437
|
+
import logger10 from "@wdio/logger";
|
|
5522
7438
|
|
|
5523
7439
|
// src/helpers/serializeCommandResult.ts
|
|
5524
7440
|
function serializeCommandResult(callbackResult, methodName) {
|
|
@@ -5551,17 +7467,201 @@ function serializeCommandResult(callbackResult, methodName) {
|
|
|
5551
7467
|
return void 0;
|
|
5552
7468
|
}
|
|
5553
7469
|
|
|
7470
|
+
// src/helpers/nativeAssertions.ts
|
|
7471
|
+
import logger9 from "@wdio/logger";
|
|
7472
|
+
var log9 = logger9("@wdio/nightwatch-devtools:nativeAssertions");
|
|
7473
|
+
var ASSERT_CMD_RE = /^(assert|verify)\.\w+$/;
|
|
7474
|
+
function assertCommandTimings(commands, count) {
|
|
7475
|
+
const executed = commands.filter(
|
|
7476
|
+
(c) => typeof c?.name === "string" && ASSERT_CMD_RE.test(c.name)
|
|
7477
|
+
);
|
|
7478
|
+
if (executed.length !== count) {
|
|
7479
|
+
return new Array(count).fill(null);
|
|
7480
|
+
}
|
|
7481
|
+
return executed.map(
|
|
7482
|
+
(c) => typeof c.startTime === "number" && typeof c.endTime === "number" ? { startTime: c.startTime, endTime: c.endTime } : null
|
|
7483
|
+
);
|
|
7484
|
+
}
|
|
7485
|
+
function messageMatchesArgs(entry, args) {
|
|
7486
|
+
const text = String(entry.fullMsg ?? entry.message ?? "");
|
|
7487
|
+
const literals = args.filter(
|
|
7488
|
+
(a) => typeof a === "string" || typeof a === "number"
|
|
7489
|
+
);
|
|
7490
|
+
return literals.length > 0 && literals.every((a) => text.includes(String(a)));
|
|
7491
|
+
}
|
|
7492
|
+
function gatherResultAssertions(results) {
|
|
7493
|
+
const testcases = results?.testcases;
|
|
7494
|
+
if (testcases && typeof testcases === "object") {
|
|
7495
|
+
const all = [];
|
|
7496
|
+
for (const tc of Object.values(testcases)) {
|
|
7497
|
+
if (Array.isArray(tc?.assertions)) {
|
|
7498
|
+
all.push(...tc.assertions);
|
|
7499
|
+
}
|
|
7500
|
+
}
|
|
7501
|
+
if (all.length > 0) {
|
|
7502
|
+
return all;
|
|
7503
|
+
}
|
|
7504
|
+
}
|
|
7505
|
+
return Array.isArray(results?.assertions) ? results.assertions : [];
|
|
7506
|
+
}
|
|
7507
|
+
function correlate(calls, assertions) {
|
|
7508
|
+
const consumed = new Array(assertions.length).fill(false);
|
|
7509
|
+
const toOutcome = (idx) => {
|
|
7510
|
+
consumed[idx] = true;
|
|
7511
|
+
const entry = assertions[idx];
|
|
7512
|
+
return {
|
|
7513
|
+
passed: !entry.failure,
|
|
7514
|
+
message: stripAnsi(String(entry.fullMsg ?? entry.message ?? "")).trim()
|
|
7515
|
+
};
|
|
7516
|
+
};
|
|
7517
|
+
const matched = calls.map((call) => {
|
|
7518
|
+
const idx = assertions.findIndex(
|
|
7519
|
+
(entry, i) => !consumed[i] && messageMatchesArgs(entry, call.args)
|
|
7520
|
+
);
|
|
7521
|
+
return idx === -1 ? null : toOutcome(idx);
|
|
7522
|
+
});
|
|
7523
|
+
return matched.map((outcome) => {
|
|
7524
|
+
if (outcome) {
|
|
7525
|
+
return outcome;
|
|
7526
|
+
}
|
|
7527
|
+
const idx = consumed.findIndex((used) => !used);
|
|
7528
|
+
return idx === -1 ? null : toOutcome(idx);
|
|
7529
|
+
});
|
|
7530
|
+
}
|
|
7531
|
+
function latestResolvedScreenshot(capturer) {
|
|
7532
|
+
for (let i = capturer.commandsLog.length - 1; i >= 0; i--) {
|
|
7533
|
+
const shot = capturer.commandsLog[i]?.screenshot;
|
|
7534
|
+
if (shot) {
|
|
7535
|
+
return shot;
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
return null;
|
|
7539
|
+
}
|
|
7540
|
+
async function resolveAssertionScreenshot(capturer, browser) {
|
|
7541
|
+
return latestResolvedScreenshot(capturer) ?? await capturer.takeScreenshotViaHttp(browser);
|
|
7542
|
+
}
|
|
7543
|
+
function conciseTitle(call) {
|
|
7544
|
+
const preview = call.args.map(
|
|
7545
|
+
(a) => typeof a === "string" ? `'${a}'` : a !== null && typeof a === "object" ? "\u2026" : String(a)
|
|
7546
|
+
).join(", ");
|
|
7547
|
+
return `${call.prefix}.${call.method}(${preview})`;
|
|
7548
|
+
}
|
|
7549
|
+
function pendingAssertionCommand(call, testUid, screenshot) {
|
|
7550
|
+
const entry = {
|
|
7551
|
+
command: `${call.prefix}.${call.method}`,
|
|
7552
|
+
args: call.args.map(safeSerializeAssertArg),
|
|
7553
|
+
title: conciseTitle(call),
|
|
7554
|
+
timestamp: call.timestamp,
|
|
7555
|
+
startTime: call.timestamp
|
|
7556
|
+
};
|
|
7557
|
+
if (call.callSource) {
|
|
7558
|
+
entry.callSource = call.callSource;
|
|
7559
|
+
}
|
|
7560
|
+
if (testUid) {
|
|
7561
|
+
entry.testUid = testUid;
|
|
7562
|
+
}
|
|
7563
|
+
if (screenshot) {
|
|
7564
|
+
entry.screenshot = screenshot;
|
|
7565
|
+
}
|
|
7566
|
+
return entry;
|
|
7567
|
+
}
|
|
7568
|
+
function parseActualFromMessage(message) {
|
|
7569
|
+
const marker = "but got:";
|
|
7570
|
+
const idx = message.lastIndexOf(marker);
|
|
7571
|
+
if (idx === -1) {
|
|
7572
|
+
return void 0;
|
|
7573
|
+
}
|
|
7574
|
+
let rest = message.slice(idx + marker.length).trim();
|
|
7575
|
+
rest = rest.replace(/ \(\d+ms\)$/, "").trim();
|
|
7576
|
+
rest = rest.replace(/^["']/, "").replace(/["']$/, "").trim();
|
|
7577
|
+
return rest || void 0;
|
|
7578
|
+
}
|
|
7579
|
+
function collapsedAssertResult2(call, outcome) {
|
|
7580
|
+
const result = {
|
|
7581
|
+
passed: outcome.passed,
|
|
7582
|
+
expected: call.args.length <= 1 ? call.args[0] : call.args,
|
|
7583
|
+
message: outcome.message
|
|
7584
|
+
};
|
|
7585
|
+
const actual = outcome.passed ? void 0 : parseActualFromMessage(outcome.message);
|
|
7586
|
+
if (actual !== void 0) {
|
|
7587
|
+
result.actual = actual;
|
|
7588
|
+
}
|
|
7589
|
+
return result;
|
|
7590
|
+
}
|
|
7591
|
+
function finalizeAssertionRow(capturer, call, outcome, timing, screenshot, testUid) {
|
|
7592
|
+
const entry = call.entry;
|
|
7593
|
+
const finalized = matcherAssertionToCommandLog(
|
|
7594
|
+
{
|
|
7595
|
+
prefix: call.prefix,
|
|
7596
|
+
method: call.method,
|
|
7597
|
+
args: call.args,
|
|
7598
|
+
passed: outcome.passed,
|
|
7599
|
+
message: outcome.message || `${call.prefix}.${call.method} failed`,
|
|
7600
|
+
callSource: call.callSource,
|
|
7601
|
+
title: entry.title
|
|
7602
|
+
},
|
|
7603
|
+
testUid
|
|
7604
|
+
);
|
|
7605
|
+
entry.result = collapsedAssertResult2(call, outcome);
|
|
7606
|
+
entry.error = finalized.error;
|
|
7607
|
+
if (!entry.screenshot && screenshot) {
|
|
7608
|
+
entry.screenshot = screenshot;
|
|
7609
|
+
}
|
|
7610
|
+
if (timing) {
|
|
7611
|
+
entry.startTime = timing.startTime;
|
|
7612
|
+
entry.timestamp = timing.endTime > timing.startTime ? timing.endTime : timing.startTime;
|
|
7613
|
+
}
|
|
7614
|
+
capturer.captureAssertCommand(entry);
|
|
7615
|
+
log9.info(`[assert] ${entry.title} \u2192 ${outcome.passed ? "pass" : "fail"}`);
|
|
7616
|
+
}
|
|
7617
|
+
async function captureNativeAssertions(capturer, browser, currentTest, testUid, calls) {
|
|
7618
|
+
if (calls.length === 0) {
|
|
7619
|
+
return;
|
|
7620
|
+
}
|
|
7621
|
+
const results = currentTest?.results;
|
|
7622
|
+
const assertions = gatherResultAssertions(results);
|
|
7623
|
+
const outcomes = correlate(calls, assertions);
|
|
7624
|
+
const timings = assertCommandTimings(
|
|
7625
|
+
Array.isArray(results?.commands) ? results.commands : [],
|
|
7626
|
+
calls.length
|
|
7627
|
+
);
|
|
7628
|
+
const screenshot = await resolveAssertionScreenshot(capturer, browser);
|
|
7629
|
+
calls.forEach((call, index) => {
|
|
7630
|
+
if (!call.entry) {
|
|
7631
|
+
return;
|
|
7632
|
+
}
|
|
7633
|
+
const outcome = outcomes[index];
|
|
7634
|
+
if (outcome) {
|
|
7635
|
+
finalizeAssertionRow(
|
|
7636
|
+
capturer,
|
|
7637
|
+
call,
|
|
7638
|
+
outcome,
|
|
7639
|
+
timings[index],
|
|
7640
|
+
screenshot,
|
|
7641
|
+
testUid
|
|
7642
|
+
);
|
|
7643
|
+
return;
|
|
7644
|
+
}
|
|
7645
|
+
if (!call.entry.screenshot && screenshot) {
|
|
7646
|
+
call.entry.screenshot = screenshot;
|
|
7647
|
+
}
|
|
7648
|
+
capturer.captureAssertCommand(call.entry);
|
|
7649
|
+
});
|
|
7650
|
+
}
|
|
7651
|
+
|
|
5554
7652
|
// src/helpers/browserProxy.ts
|
|
5555
|
-
var
|
|
7653
|
+
var log10 = logger10("@wdio/nightwatch-devtools:browserProxy");
|
|
5556
7654
|
var BrowserProxy = class {
|
|
5557
|
-
constructor(sessionCapturer, testManager, getCurrentTest) {
|
|
7655
|
+
constructor(sessionCapturer, testManager, getCurrentTest, captureAssertions = true) {
|
|
5558
7656
|
this.sessionCapturer = sessionCapturer;
|
|
5559
7657
|
this.testManager = testManager;
|
|
5560
7658
|
this.getCurrentTest = getCurrentTest;
|
|
7659
|
+
this.captureAssertions = captureAssertions;
|
|
5561
7660
|
}
|
|
5562
7661
|
sessionCapturer;
|
|
5563
7662
|
testManager;
|
|
5564
7663
|
getCurrentTest;
|
|
7664
|
+
captureAssertions;
|
|
5565
7665
|
/** Tracks which browser *instances* have already been proxied to avoid double-wrapping. */
|
|
5566
7666
|
proxiedBrowsers = /* @__PURE__ */ new WeakSet();
|
|
5567
7667
|
commandStack = [];
|
|
@@ -5573,6 +7673,14 @@ var BrowserProxy = class {
|
|
|
5573
7673
|
* rather than appending, showing only the final execution result.
|
|
5574
7674
|
*/
|
|
5575
7675
|
retryTracker = new RetryTracker();
|
|
7676
|
+
/**
|
|
7677
|
+
* Per-test buffer of explicit `browser.assert.*` / `browser.verify.*` calls,
|
|
7678
|
+
* recorded synchronously at call time by {@link wrapAssertionNamespaces}.
|
|
7679
|
+
* Nightwatch exposes no per-assertion hook and its test-end results carry no
|
|
7680
|
+
* source location for passing assertions, so call-time capture is the only
|
|
7681
|
+
* way to get real args + callSource. Drained (and cleared) each `afterEach`.
|
|
7682
|
+
*/
|
|
7683
|
+
nativeAssertCalls = [];
|
|
5576
7684
|
/**
|
|
5577
7685
|
* Update the session capturer reference after a WebDriver session change.
|
|
5578
7686
|
* Does NOT re-wrap browser methods — wrapping is permanent per browser object.
|
|
@@ -5584,12 +7692,98 @@ var BrowserProxy = class {
|
|
|
5584
7692
|
this.commandStack = [];
|
|
5585
7693
|
this.lastCommandSig = null;
|
|
5586
7694
|
this.retryTracker.reset();
|
|
7695
|
+
this.nativeAssertCalls = [];
|
|
7696
|
+
}
|
|
7697
|
+
/** Hand off this test's recorded native-assertion calls and clear the
|
|
7698
|
+
* buffer so the next test starts fresh. */
|
|
7699
|
+
drainNativeAssertCalls() {
|
|
7700
|
+
const calls = this.nativeAssertCalls;
|
|
7701
|
+
this.nativeAssertCalls = [];
|
|
7702
|
+
return calls;
|
|
7703
|
+
}
|
|
7704
|
+
/**
|
|
7705
|
+
* Replace `browser.assert` / `browser.verify` (both are Nightwatch Proxies
|
|
7706
|
+
* whose `get` returns a fresh function per access) with a recording Proxy:
|
|
7707
|
+
* on each method call it captures `{prefix, method, args, callSource}` from a
|
|
7708
|
+
* user-code frame, streams a neutral pending row immediately (live), then
|
|
7709
|
+
* delegates to the ORIGINAL namespace method so Nightwatch's queue, chaining,
|
|
7710
|
+
* and abortOnFailure semantics (assert aborts, verify does not) are
|
|
7711
|
+
* byte-for-byte unchanged. Called once per browser.
|
|
7712
|
+
*/
|
|
7713
|
+
wrapAssertionNamespaces(browser) {
|
|
7714
|
+
if (!this.captureAssertions) {
|
|
7715
|
+
return;
|
|
7716
|
+
}
|
|
7717
|
+
const b = browser;
|
|
7718
|
+
["assert", "verify"].forEach((prefix) => {
|
|
7719
|
+
const original = b[prefix];
|
|
7720
|
+
if (!original || typeof original !== "object") {
|
|
7721
|
+
return;
|
|
7722
|
+
}
|
|
7723
|
+
b[prefix] = this.recordingNamespaceProxy(original, prefix, []);
|
|
7724
|
+
});
|
|
7725
|
+
}
|
|
7726
|
+
/**
|
|
7727
|
+
* Recording Proxy over one assert/verify namespace. A function property
|
|
7728
|
+
* becomes a call-time recorder keyed by its full dotted path
|
|
7729
|
+
* (`titleContains`, `not.titleContains`); a nested namespace object recurses
|
|
7730
|
+
* through the SAME wrapper — Nightwatch exposes `assert.not` as its own Proxy,
|
|
7731
|
+
* so negated asserts are recorded via the identical mechanism as positive
|
|
7732
|
+
* ones instead of a parallel path. The recorder buffers a pending row, then
|
|
7733
|
+
* delegates to the ORIGINAL method so Nightwatch's queue, chaining, and
|
|
7734
|
+
* abort/negate semantics stay byte-for-byte unchanged. Non-method,
|
|
7735
|
+
* non-namespace props pass through untouched.
|
|
7736
|
+
*/
|
|
7737
|
+
recordingNamespaceProxy(target, prefix, path14) {
|
|
7738
|
+
return new Proxy(target, {
|
|
7739
|
+
get: (t, name, receiver) => {
|
|
7740
|
+
const orig = Reflect.get(t, name, receiver);
|
|
7741
|
+
if (typeof name !== "string") {
|
|
7742
|
+
return orig;
|
|
7743
|
+
}
|
|
7744
|
+
if (orig !== null && typeof orig === "object") {
|
|
7745
|
+
return this.recordingNamespaceProxy(orig, prefix, [...path14, name]);
|
|
7746
|
+
}
|
|
7747
|
+
if (typeof orig !== "function") {
|
|
7748
|
+
return orig;
|
|
7749
|
+
}
|
|
7750
|
+
return (...args) => {
|
|
7751
|
+
const callInfo = getCallSourceFromStack();
|
|
7752
|
+
if (callInfo.filePath !== void 0) {
|
|
7753
|
+
this.emitPendingAssertion({
|
|
7754
|
+
prefix,
|
|
7755
|
+
method: [...path14, name].join("."),
|
|
7756
|
+
args,
|
|
7757
|
+
callSource: callInfo.callSource,
|
|
7758
|
+
timestamp: Date.now()
|
|
7759
|
+
});
|
|
7760
|
+
}
|
|
7761
|
+
return orig(...args);
|
|
7762
|
+
};
|
|
7763
|
+
}
|
|
7764
|
+
});
|
|
7765
|
+
}
|
|
7766
|
+
/** Buffer an explicit assert/verify call at invocation time (prebuilding its
|
|
7767
|
+
* row with args/callSource/screenshot), but do NOT stream it yet. Nightwatch
|
|
7768
|
+
* exposes no per-assertion result hook, so streaming here would show every
|
|
7769
|
+
* assert as a neutral (green) row while the test is still Running — before
|
|
7770
|
+
* its pass/fail is known. `captureNativeAssertions` emits the rows at test-end
|
|
7771
|
+
* with real outcomes + execution timing instead. */
|
|
7772
|
+
emitPendingAssertion(call) {
|
|
7773
|
+
const testUid = this.getCurrentTest()?.uid;
|
|
7774
|
+
const entry = pendingAssertionCommand(
|
|
7775
|
+
call,
|
|
7776
|
+
testUid,
|
|
7777
|
+
latestResolvedScreenshot(this.sessionCapturer)
|
|
7778
|
+
);
|
|
7779
|
+
call.entry = entry;
|
|
7780
|
+
this.nativeAssertCalls.push(call);
|
|
5587
7781
|
}
|
|
5588
7782
|
getCurrentTestFullPath() {
|
|
5589
7783
|
return this.currentTestFullPath;
|
|
5590
7784
|
}
|
|
5591
|
-
setCurrentTestFullPath(
|
|
5592
|
-
this.currentTestFullPath =
|
|
7785
|
+
setCurrentTestFullPath(path14) {
|
|
7786
|
+
this.currentTestFullPath = path14;
|
|
5593
7787
|
}
|
|
5594
7788
|
/**
|
|
5595
7789
|
* Wrap browser navigation methods (url / navigate / navigateTo) to inject
|
|
@@ -5610,9 +7804,9 @@ var BrowserProxy = class {
|
|
|
5610
7804
|
b[methodName] = function(...args) {
|
|
5611
7805
|
const result = original(...args);
|
|
5612
7806
|
const injectAndCapture = () => {
|
|
5613
|
-
|
|
7807
|
+
log10.info(`[nav] ${methodName}(${args[0] ?? ""}) \u2014 injecting script`);
|
|
5614
7808
|
return sessionCapturer.injectScript(browser).then(() => sessionCapturer.captureTrace(browser)).catch(
|
|
5615
|
-
(err) =>
|
|
7809
|
+
(err) => log10.error(`Failed to inject script: ${err.message}`)
|
|
5616
7810
|
);
|
|
5617
7811
|
};
|
|
5618
7812
|
const chainable = result;
|
|
@@ -5629,7 +7823,7 @@ var BrowserProxy = class {
|
|
|
5629
7823
|
wrapNav("url");
|
|
5630
7824
|
wrapNav("navigate");
|
|
5631
7825
|
wrapNav("navigateTo");
|
|
5632
|
-
|
|
7826
|
+
log10.info("\u2713 Script injection wrapped");
|
|
5633
7827
|
}
|
|
5634
7828
|
wrapBrowserCommands(browser) {
|
|
5635
7829
|
if (this.proxiedBrowsers.has(browser)) {
|
|
@@ -5662,8 +7856,9 @@ var BrowserProxy = class {
|
|
|
5662
7856
|
};
|
|
5663
7857
|
wrappedMethods.push(methodName);
|
|
5664
7858
|
});
|
|
7859
|
+
this.wrapAssertionNamespaces(browser);
|
|
5665
7860
|
this.proxiedBrowsers.add(browser);
|
|
5666
|
-
|
|
7861
|
+
log10.info(`\u2713 Wrapped ${wrappedMethods.length} browser methods`);
|
|
5667
7862
|
}
|
|
5668
7863
|
handleRetryReplacement(browser, methodName, logArgs, serializedResult, effectiveUid, callSource, commandTimestamp) {
|
|
5669
7864
|
const { entry, oldTimestamp } = this.sessionCapturer.replaceCommand(
|
|
@@ -5692,13 +7887,13 @@ var BrowserProxy = class {
|
|
|
5692
7887
|
callSource,
|
|
5693
7888
|
commandTimestamp
|
|
5694
7889
|
).catch(
|
|
5695
|
-
(err) =>
|
|
7890
|
+
(err) => log10.error(`Failed to capture ${methodName}: ${err.message}`)
|
|
5696
7891
|
);
|
|
5697
7892
|
const lastCommand = this.sessionCapturer.commandsLog[this.sessionCapturer.commandsLog.length - 1];
|
|
5698
7893
|
if (lastCommand) {
|
|
5699
7894
|
this.retryTracker.setLastId(lastCommand._id ?? null);
|
|
5700
7895
|
this.sessionCapturer.sendCommand(lastCommand);
|
|
5701
|
-
|
|
7896
|
+
log10.info(`[command] ${methodName}`);
|
|
5702
7897
|
this.attachScreenshot(browser, lastCommand, methodName);
|
|
5703
7898
|
}
|
|
5704
7899
|
this.maybeRepollMutations(browser, methodName);
|
|
@@ -5735,7 +7930,7 @@ var BrowserProxy = class {
|
|
|
5735
7930
|
}
|
|
5736
7931
|
// Result-capturing callback factory — called by Nightwatch's async queue
|
|
5737
7932
|
// when the command completes. This is where we get the *actual* result.
|
|
5738
|
-
makeCaptureCallback(browser, methodName, logArgs, cmdSig, callSource, commandTimestamp, testUid, userCallback) {
|
|
7933
|
+
makeCaptureCallback(browser, methodName, logArgs, cmdSig, callSource, hasUserSource, commandTimestamp, testUid, userCallback) {
|
|
5739
7934
|
return (callbackResult) => {
|
|
5740
7935
|
this.popCommandStackIfMatches(methodName, cmdSig);
|
|
5741
7936
|
const serializedResult = serializeCommandResult(
|
|
@@ -5743,7 +7938,7 @@ var BrowserProxy = class {
|
|
|
5743
7938
|
methodName
|
|
5744
7939
|
);
|
|
5745
7940
|
const effectiveUid = this.getCurrentTest()?.uid ?? testUid;
|
|
5746
|
-
if (effectiveUid) {
|
|
7941
|
+
if (effectiveUid && hasUserSource) {
|
|
5747
7942
|
if (this.retryTracker.isRetry(cmdSig)) {
|
|
5748
7943
|
this.handleRetryReplacement(
|
|
5749
7944
|
browser,
|
|
@@ -5811,6 +8006,7 @@ var BrowserProxy = class {
|
|
|
5811
8006
|
logArgs,
|
|
5812
8007
|
cmdSig,
|
|
5813
8008
|
callSource,
|
|
8009
|
+
callInfo.filePath !== void 0,
|
|
5814
8010
|
commandTimestamp,
|
|
5815
8011
|
this.getCurrentTest()?.uid,
|
|
5816
8012
|
userCallback
|
|
@@ -5830,7 +8026,7 @@ var BrowserProxy = class {
|
|
|
5830
8026
|
return;
|
|
5831
8027
|
}
|
|
5832
8028
|
const normalizedError = toError(error);
|
|
5833
|
-
|
|
8029
|
+
log10.error(`[command error] ${methodName}: ${normalizedError.message}`);
|
|
5834
8030
|
this.sessionCapturer.captureCommand(
|
|
5835
8031
|
methodName,
|
|
5836
8032
|
args,
|
|
@@ -5839,7 +8035,7 @@ var BrowserProxy = class {
|
|
|
5839
8035
|
currentTest.uid,
|
|
5840
8036
|
callSource
|
|
5841
8037
|
).catch(
|
|
5842
|
-
(err) =>
|
|
8038
|
+
(err) => log10.error(`Failed to capture ${methodName}: ${err.message}`)
|
|
5843
8039
|
);
|
|
5844
8040
|
const lastCommand = this.sessionCapturer.commandsLog[this.sessionCapturer.commandsLog.length - 1];
|
|
5845
8041
|
if (lastCommand) {
|
|
@@ -5862,7 +8058,7 @@ var BrowserProxy = class {
|
|
|
5862
8058
|
if (screenshot) {
|
|
5863
8059
|
entry.screenshot = screenshot;
|
|
5864
8060
|
this.sessionCapturer.sendReplaceCommand(ts, entry);
|
|
5865
|
-
|
|
8061
|
+
log10.info(`[screenshot] Attached to ${methodName}${suffix}`);
|
|
5866
8062
|
}
|
|
5867
8063
|
}).catch(() => {
|
|
5868
8064
|
});
|
|
@@ -5870,8 +8066,8 @@ var BrowserProxy = class {
|
|
|
5870
8066
|
};
|
|
5871
8067
|
|
|
5872
8068
|
// src/screencast.ts
|
|
5873
|
-
import
|
|
5874
|
-
var
|
|
8069
|
+
import logger11 from "@wdio/logger";
|
|
8070
|
+
var log11 = logger11("@wdio/nightwatch-devtools:ScreencastRecorder");
|
|
5875
8071
|
var ScreencastRecorder = class extends ScreencastRecorderBase {
|
|
5876
8072
|
#sessionCapturer;
|
|
5877
8073
|
constructor(sessionCapturer, options) {
|
|
@@ -5879,15 +8075,15 @@ var ScreencastRecorder = class extends ScreencastRecorderBase {
|
|
|
5879
8075
|
this.#sessionCapturer = sessionCapturer;
|
|
5880
8076
|
}
|
|
5881
8077
|
onPollingStarted(intervalMs) {
|
|
5882
|
-
|
|
8078
|
+
log11.info(
|
|
5883
8079
|
`\u2713 Screencast recording started (polling mode, ${intervalMs} ms interval)`
|
|
5884
8080
|
);
|
|
5885
8081
|
}
|
|
5886
8082
|
onPollingStopped(frameCount) {
|
|
5887
|
-
|
|
8083
|
+
log11.info(`\u2713 Screencast stopped \u2014 ${frameCount} frame(s) collected`);
|
|
5888
8084
|
}
|
|
5889
8085
|
onUnavailable(err) {
|
|
5890
|
-
|
|
8086
|
+
log11.warn(
|
|
5891
8087
|
`Screencast unavailable (${errorMessage(err)}). Recording skipped.`
|
|
5892
8088
|
);
|
|
5893
8089
|
}
|
|
@@ -5901,9 +8097,9 @@ var ScreencastRecorder = class extends ScreencastRecorderBase {
|
|
|
5901
8097
|
};
|
|
5902
8098
|
|
|
5903
8099
|
// src/session-init.ts
|
|
5904
|
-
var
|
|
8100
|
+
var log12 = logger12("@wdio/nightwatch-devtools:session-init");
|
|
5905
8101
|
async function handleSessionChange(ctx, finalizeCurrent) {
|
|
5906
|
-
|
|
8102
|
+
log12.info("Browser session changed \u2014 reconnecting WebSocket only");
|
|
5907
8103
|
ctx.isScriptInjected = false;
|
|
5908
8104
|
ctx.bidiAttachAttempted = false;
|
|
5909
8105
|
await finalizeCurrent();
|
|
@@ -5911,17 +8107,24 @@ async function handleSessionChange(ctx, finalizeCurrent) {
|
|
|
5911
8107
|
ctx.sessionCapturer = null;
|
|
5912
8108
|
}
|
|
5913
8109
|
function initReporterChain(ctx) {
|
|
5914
|
-
ctx.testReporter = new TestReporter(
|
|
5915
|
-
|
|
5916
|
-
ctx.sessionCapturer
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
8110
|
+
ctx.testReporter = new TestReporter(
|
|
8111
|
+
(suitesData) => {
|
|
8112
|
+
if (ctx.sessionCapturer) {
|
|
8113
|
+
ctx.sessionCapturer.sendUpstream("suites", suitesData);
|
|
8114
|
+
}
|
|
8115
|
+
},
|
|
8116
|
+
(uid) => ctx.attemptFor(uid)
|
|
8117
|
+
);
|
|
8118
|
+
ctx.testManager = new TestManager(
|
|
8119
|
+
ctx.testReporter,
|
|
8120
|
+
(uid, state) => ctx.recordOutcome(uid, state)
|
|
8121
|
+
);
|
|
5920
8122
|
ctx.suiteManager = new SuiteManager(ctx.testReporter);
|
|
5921
8123
|
ctx.browserProxy = new BrowserProxy(
|
|
5922
8124
|
ctx.sessionCapturer,
|
|
5923
8125
|
ctx.testManager,
|
|
5924
|
-
() => ctx.getCurrentTest() ?? ctx.getCurrentScenarioSuite()
|
|
8126
|
+
() => ctx.getCurrentTest() ?? ctx.getCurrentScenarioSuite(),
|
|
8127
|
+
ctx.captureAssertions
|
|
5925
8128
|
);
|
|
5926
8129
|
}
|
|
5927
8130
|
function rebindReporterToNewSession(ctx) {
|
|
@@ -5956,12 +8159,12 @@ function broadcastSessionMetadata(ctx, browser) {
|
|
|
5956
8159
|
ctx.sessionCapturer.sendUpstream("metadata", metadata);
|
|
5957
8160
|
const browserName = capabilities.browserName || desiredCapabilities.browserName || "unknown";
|
|
5958
8161
|
const browserVersion = capabilities.browserVersion || capabilities.version || "";
|
|
5959
|
-
|
|
8162
|
+
log12.info(
|
|
5960
8163
|
`\u2713 Browser: ${browserName}${browserVersion ? " " + browserVersion : ""} (session: ${sessionId})`
|
|
5961
8164
|
);
|
|
5962
8165
|
const loggingPrefs = capabilities["goog:loggingPrefs"] || desiredCapabilities["goog:loggingPrefs"] || {};
|
|
5963
8166
|
if (!loggingPrefs.performance && !ctx.bidiEnabled) {
|
|
5964
|
-
|
|
8167
|
+
log12.warn(
|
|
5965
8168
|
"\u26A0 Network tab will be empty \u2014 add 'goog:loggingPrefs': { performance: 'ALL' } to your capabilities (or enable bidi:true)"
|
|
5966
8169
|
);
|
|
5967
8170
|
}
|
|
@@ -5973,17 +8176,17 @@ async function tryAttachBidi(ctx, browser) {
|
|
|
5973
8176
|
ctx.bidiAttachAttempted = true;
|
|
5974
8177
|
const driver = browser.driver;
|
|
5975
8178
|
if (!driver) {
|
|
5976
|
-
|
|
8179
|
+
log12.warn("bidi:true set but browser.driver unavailable \u2014 skipping");
|
|
5977
8180
|
return;
|
|
5978
8181
|
}
|
|
5979
|
-
const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-
|
|
8182
|
+
const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-5ALZJJNC.js");
|
|
5980
8183
|
const ok = await attachBidiHandlers(
|
|
5981
8184
|
driver,
|
|
5982
8185
|
buildBidiSinks(ctx.sessionCapturer)
|
|
5983
8186
|
);
|
|
5984
8187
|
if (ok) {
|
|
5985
8188
|
ctx.sessionCapturer.bidiActive = true;
|
|
5986
|
-
|
|
8189
|
+
log12.info("\u2713 BiDi attached \u2014 perf-log network capture disabled");
|
|
5987
8190
|
}
|
|
5988
8191
|
}
|
|
5989
8192
|
async function tryStartScreencast(ctx, browser, sessionId) {
|
|
@@ -5995,7 +8198,7 @@ async function tryStartScreencast(ctx, browser, sessionId) {
|
|
|
5995
8198
|
ctx.screencastOptions
|
|
5996
8199
|
);
|
|
5997
8200
|
ctx.screencastSessionId = sessionId;
|
|
5998
|
-
|
|
8201
|
+
log12.info(`\u{1F3AC} Starting screencast for session ${sessionId}`);
|
|
5999
8202
|
await ctx.screencastRecorder.start(browser);
|
|
6000
8203
|
}
|
|
6001
8204
|
async function ensureSessionInitialized(ctx, browser, finalizeCurrentScreencast2) {
|
|
@@ -6025,7 +8228,7 @@ async function ensureSessionInitialized(ctx, browser, finalizeCurrentScreencast2
|
|
|
6025
8228
|
if (ctx.mode !== "trace") {
|
|
6026
8229
|
const connected = await ctx.sessionCapturer.waitForConnection(3e3);
|
|
6027
8230
|
if (!connected) {
|
|
6028
|
-
|
|
8231
|
+
log12.error("\u274C Worker WebSocket failed to connect!");
|
|
6029
8232
|
}
|
|
6030
8233
|
}
|
|
6031
8234
|
if (!ctx.testReporter) {
|
|
@@ -6041,24 +8244,28 @@ async function finalizeCurrentScreencast(ctx) {
|
|
|
6041
8244
|
if (!ctx.screencastRecorder || !ctx.screencastSessionId) {
|
|
6042
8245
|
return;
|
|
6043
8246
|
}
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
8247
|
+
if (ctx.mode === "trace") {
|
|
8248
|
+
await ctx.screencastRecorder.stop();
|
|
8249
|
+
} else {
|
|
8250
|
+
await finalizeScreencast({
|
|
8251
|
+
recorder: ctx.screencastRecorder,
|
|
8252
|
+
sessionId: ctx.screencastSessionId,
|
|
8253
|
+
filenamePrefix: "nightwatch-video",
|
|
8254
|
+
outputDir: resolveAdapterOutputDir({
|
|
8255
|
+
testFilePath: ctx.browserProxy?.getCurrentTestFullPath?.() ?? void 0,
|
|
8256
|
+
configPath: ctx.configPath
|
|
8257
|
+
}),
|
|
8258
|
+
captureFormat: ctx.screencastOptions.captureFormat,
|
|
8259
|
+
sendUpstream: (scope, data) => ctx.sessionCapturer?.sendUpstream(scope, data),
|
|
8260
|
+
onLog: (level, message) => log12[level](message)
|
|
8261
|
+
});
|
|
8262
|
+
}
|
|
6056
8263
|
ctx.screencastRecorder = void 0;
|
|
6057
8264
|
ctx.screencastSessionId = void 0;
|
|
6058
8265
|
}
|
|
6059
8266
|
|
|
6060
8267
|
// src/index.ts
|
|
6061
|
-
var
|
|
8268
|
+
var log13 = logger13("@wdio/nightwatch-devtools");
|
|
6062
8269
|
var NightwatchDevToolsPlugin = class {
|
|
6063
8270
|
options;
|
|
6064
8271
|
sessionCapturer;
|
|
@@ -6083,30 +8290,60 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6083
8290
|
#specRanges = [];
|
|
6084
8291
|
/** Set of spec files already flushed to disk. */
|
|
6085
8292
|
#flushedSpecs = /* @__PURE__ */ new Set();
|
|
8293
|
+
/** Every trace/video artifact seen this run (retained or not), for the
|
|
8294
|
+
* end-of-run artifacts manifest. Populated via the context's onArtifact. */
|
|
8295
|
+
#artifacts = [];
|
|
8296
|
+
/** In-flight fire-and-forget eager/boundary slice flushes, awaited at finalize
|
|
8297
|
+
* so the last slice's write + its manifest entry land before teardown. */
|
|
8298
|
+
#traceFlushes = [];
|
|
6086
8299
|
#getRerunLabel() {
|
|
6087
8300
|
return process.env[REUSE_ENV.RERUN_ENTRY_TYPE] === "test" ? process.env[REUSE_ENV.RERUN_LABEL]?.trim() : void 0;
|
|
6088
8301
|
}
|
|
6089
8302
|
#screencastOptions;
|
|
6090
8303
|
#screencastRecorder;
|
|
6091
8304
|
#screencastSessionId;
|
|
8305
|
+
// Snapshotted before each recorder is nulled, so the export isn't blank.
|
|
8306
|
+
#filmstripFrames = [];
|
|
6092
8307
|
#bidiEnabled = false;
|
|
6093
8308
|
#bidiAttachAttempted = false;
|
|
8309
|
+
/** Wall-clock ms at the current test/scenario start — the lower bound of that
|
|
8310
|
+
* test's action-snapshot and video-frame window (per-test artifact slicing). */
|
|
8311
|
+
#currentTestStartWallTime = 0;
|
|
8312
|
+
// Nightwatch `--retries` and cross-worker reruns may reset this in-process
|
|
8313
|
+
// tracker; only retries that re-enter this process's start hook are counted.
|
|
8314
|
+
#attemptTracker = new TestAttemptTracker();
|
|
6094
8315
|
constructor(options = {}) {
|
|
6095
8316
|
const mode = options.mode ?? "live";
|
|
6096
|
-
const
|
|
8317
|
+
const wantRecorder = mode === "trace" && (options.filmstrip === true || options.video !== void 0 && options.video !== "off");
|
|
8318
|
+
const ignore = mode === "trace" && !wantRecorder && options.screencast?.enabled === true;
|
|
6097
8319
|
if (ignore) {
|
|
6098
|
-
|
|
8320
|
+
log13.warn("trace mode: ignoring screencast option (live-mode feature)");
|
|
8321
|
+
}
|
|
8322
|
+
let screencast = ignore ? {} : options.screencast ?? {};
|
|
8323
|
+
if (wantRecorder) {
|
|
8324
|
+
screencast = { ...options.screencast ?? {}, enabled: true };
|
|
6099
8325
|
}
|
|
6100
|
-
const screencast = ignore ? {} : options.screencast ?? {};
|
|
6101
8326
|
this.options = {
|
|
6102
8327
|
port: options.port ?? 3e3,
|
|
6103
8328
|
hostname: options.hostname ?? "localhost",
|
|
6104
8329
|
screencast,
|
|
6105
8330
|
bidi: options.bidi ?? false,
|
|
8331
|
+
captureAssertions: options.captureAssertions ?? true,
|
|
6106
8332
|
mode,
|
|
6107
8333
|
traceFormat: options.traceFormat ?? "zip",
|
|
6108
|
-
traceGranularity: options.traceGranularity ?? "session"
|
|
8334
|
+
traceGranularity: options.traceGranularity ?? "session",
|
|
8335
|
+
tracePolicy: options.tracePolicy ?? "on",
|
|
8336
|
+
filmstrip: options.filmstrip ?? true,
|
|
8337
|
+
screenshot: options.screenshot ?? "off",
|
|
8338
|
+
video: options.video ?? "off",
|
|
8339
|
+
// No live Allure signal in Nightwatch (produce-only), so no auto-detect:
|
|
8340
|
+
// off by default, opt-in via the option.
|
|
8341
|
+
emitArtifactsManifest: options.emitArtifactsManifest ?? false
|
|
6109
8342
|
};
|
|
8343
|
+
const policyWarning = tracePolicyModeWarning(options.tracePolicy, mode);
|
|
8344
|
+
if (policyWarning) {
|
|
8345
|
+
log13.warn(policyWarning);
|
|
8346
|
+
}
|
|
6110
8347
|
this.#screencastOptions = { ...SCREENCAST_DEFAULTS, ...screencast };
|
|
6111
8348
|
this.#bidiEnabled = options.bidi === true;
|
|
6112
8349
|
}
|
|
@@ -6140,6 +8377,9 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6140
8377
|
get bidiEnabled() {
|
|
6141
8378
|
return self.#bidiEnabled;
|
|
6142
8379
|
},
|
|
8380
|
+
get captureAssertions() {
|
|
8381
|
+
return self.options.captureAssertions;
|
|
8382
|
+
},
|
|
6143
8383
|
get sessionCapturer() {
|
|
6144
8384
|
return self.sessionCapturer;
|
|
6145
8385
|
},
|
|
@@ -6254,7 +8494,11 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6254
8494
|
clearExecutionData: () => {
|
|
6255
8495
|
self.testReporter.clearExecutionData();
|
|
6256
8496
|
self.suiteManager.clearExecutionData();
|
|
8497
|
+
self.#attemptTracker.reset();
|
|
6257
8498
|
},
|
|
8499
|
+
recordAttempt: (uid, specFile) => self.#attemptTracker.recordStart(uid, specFile),
|
|
8500
|
+
recordOutcome: (uid, state) => self.#attemptTracker.recordOutcome(uid, state),
|
|
8501
|
+
attemptFor: (uid) => self.#attemptTracker.attemptFor(uid),
|
|
6258
8502
|
buildMetadataOptions: () => self.#buildMetadataOptions(),
|
|
6259
8503
|
ensureSessionInitialized: (b) => self.#ensureSessionInitialized(b),
|
|
6260
8504
|
wrapBrowserOnce: (b) => self.#wrapBrowserOnce(b),
|
|
@@ -6263,10 +8507,28 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6263
8507
|
setCucumberRunner: (v) => {
|
|
6264
8508
|
self.#isCucumberRunner = v;
|
|
6265
8509
|
},
|
|
6266
|
-
getRerunLabel: () => self.#getRerunLabel()
|
|
8510
|
+
getRerunLabel: () => self.#getRerunLabel(),
|
|
8511
|
+
get traceMode() {
|
|
8512
|
+
return self.options.mode === "trace";
|
|
8513
|
+
},
|
|
8514
|
+
get traceGranularity() {
|
|
8515
|
+
return self.options.traceGranularity;
|
|
8516
|
+
},
|
|
8517
|
+
get specRanges() {
|
|
8518
|
+
return self.#specRanges;
|
|
8519
|
+
},
|
|
8520
|
+
get flushedSpecs() {
|
|
8521
|
+
return self.#flushedSpecs;
|
|
8522
|
+
},
|
|
8523
|
+
flushTraceRange: (range) => self.#flushSpecTrace(range),
|
|
8524
|
+
emitTestArtifacts: (uid, failed) => self.#emitTestArtifacts(uid, failed)
|
|
6267
8525
|
};
|
|
6268
8526
|
return this.#internals;
|
|
6269
8527
|
}
|
|
8528
|
+
/** Boundary cast: currentTest is Nightwatch's loose bag; only uid is read. */
|
|
8529
|
+
#currentTestUid() {
|
|
8530
|
+
return this.#currentTest?.uid;
|
|
8531
|
+
}
|
|
6270
8532
|
#handleReuseMode() {
|
|
6271
8533
|
handleReuseMode(this.#getInternals());
|
|
6272
8534
|
}
|
|
@@ -6282,6 +8544,12 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6282
8544
|
internals.handleReuse = () => this.#handleReuseMode();
|
|
6283
8545
|
internals.plugin = this;
|
|
6284
8546
|
await runPluginBefore(internals);
|
|
8547
|
+
if (this.options.captureAssertions) {
|
|
8548
|
+
wireAssertCapture(
|
|
8549
|
+
() => this.sessionCapturer,
|
|
8550
|
+
() => this.#currentTestUid()
|
|
8551
|
+
);
|
|
8552
|
+
}
|
|
6285
8553
|
}
|
|
6286
8554
|
async #ensureSessionInitialized(browser) {
|
|
6287
8555
|
await ensureSessionInitialized(
|
|
@@ -6291,9 +8559,13 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6291
8559
|
);
|
|
6292
8560
|
}
|
|
6293
8561
|
async #finalizeCurrentScreencast() {
|
|
8562
|
+
if ((this.options.filmstrip || this.options.video !== "off") && this.#screencastRecorder) {
|
|
8563
|
+
this.#filmstripFrames.push(...this.#screencastRecorder.frames);
|
|
8564
|
+
}
|
|
6294
8565
|
await finalizeCurrentScreencast(this.#getInternals());
|
|
6295
8566
|
}
|
|
6296
8567
|
async cucumberBefore(browser, pickle) {
|
|
8568
|
+
this.#currentTestStartWallTime = Date.now();
|
|
6297
8569
|
await cucumberBefore(this.#getInternals(), browser, pickle);
|
|
6298
8570
|
}
|
|
6299
8571
|
async cucumberAfter(browser, result, pickle) {
|
|
@@ -6322,12 +8594,13 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6322
8594
|
#pickCurrentTestName(currentTest, testNames, processedTests) {
|
|
6323
8595
|
return pickCurrentTestName(currentTest, testNames, processedTests);
|
|
6324
8596
|
}
|
|
6325
|
-
async #startNextTest(currentSuite, currentTestName, processedTests) {
|
|
8597
|
+
async #startNextTest(currentSuite, currentTestName, processedTests, specFile) {
|
|
6326
8598
|
await startNextTest(
|
|
6327
8599
|
this.#getInternals(),
|
|
6328
8600
|
currentSuite,
|
|
6329
8601
|
currentTestName,
|
|
6330
|
-
processedTests
|
|
8602
|
+
processedTests,
|
|
8603
|
+
specFile
|
|
6331
8604
|
);
|
|
6332
8605
|
}
|
|
6333
8606
|
async #closePreviousRunningTest(currentSuite, testFile, currentTest) {
|
|
@@ -6345,6 +8618,7 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6345
8618
|
if (this.#isCucumberRunner) {
|
|
6346
8619
|
return;
|
|
6347
8620
|
}
|
|
8621
|
+
this.#currentTestStartWallTime = Date.now();
|
|
6348
8622
|
await this.#ensureSessionInitialized(browser);
|
|
6349
8623
|
const currentTest = browser.currentTest;
|
|
6350
8624
|
if (!currentTest) {
|
|
@@ -6364,23 +8638,7 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6364
8638
|
});
|
|
6365
8639
|
}
|
|
6366
8640
|
if (fullPath) {
|
|
6367
|
-
|
|
6368
|
-
{
|
|
6369
|
-
specRanges: this.#specRanges,
|
|
6370
|
-
flushedSpecs: this.#flushedSpecs,
|
|
6371
|
-
capturer: this.sessionCapturer,
|
|
6372
|
-
actionSnapshots: this.sessionCapturer.actionSnapshots
|
|
6373
|
-
},
|
|
6374
|
-
fullPath,
|
|
6375
|
-
this.options.traceGranularity
|
|
6376
|
-
);
|
|
6377
|
-
if (prevRange) {
|
|
6378
|
-
void this.#flushSpecTrace(prevRange).catch(
|
|
6379
|
-
(err) => log11.warn(
|
|
6380
|
-
`Failed to flush trace for spec "${prevRange.specFile}": ${errorMessage(err)}`
|
|
6381
|
-
)
|
|
6382
|
-
);
|
|
6383
|
-
}
|
|
8641
|
+
recordSpecSliceBoundary(this.#getInternals(), fullPath);
|
|
6384
8642
|
}
|
|
6385
8643
|
await this.#closePreviousRunningTest(currentSuite, testFile, currentTest);
|
|
6386
8644
|
const processedTests = this.testManager.getProcessedTests(testFile);
|
|
@@ -6390,7 +8648,12 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6390
8648
|
processedTests
|
|
6391
8649
|
);
|
|
6392
8650
|
if (currentTestName) {
|
|
6393
|
-
await this.#startNextTest(
|
|
8651
|
+
await this.#startNextTest(
|
|
8652
|
+
currentSuite,
|
|
8653
|
+
currentTestName,
|
|
8654
|
+
processedTests,
|
|
8655
|
+
fullPath
|
|
8656
|
+
);
|
|
6394
8657
|
}
|
|
6395
8658
|
this.#wrapBrowserOnce(browser);
|
|
6396
8659
|
}
|
|
@@ -6401,36 +8664,81 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6401
8664
|
if (browser && this.sessionCapturer) {
|
|
6402
8665
|
try {
|
|
6403
8666
|
await this.#closeOutTestcases(browser);
|
|
8667
|
+
if (this.options.captureAssertions) {
|
|
8668
|
+
await captureNativeAssertions(
|
|
8669
|
+
this.sessionCapturer,
|
|
8670
|
+
browser,
|
|
8671
|
+
browser.currentTest,
|
|
8672
|
+
this.#currentTestUid(),
|
|
8673
|
+
this.browserProxy.drainNativeAssertCalls()
|
|
8674
|
+
);
|
|
8675
|
+
}
|
|
6404
8676
|
await this.sessionCapturer.captureTrace(browser);
|
|
8677
|
+
flushTestSlice(this.#getInternals());
|
|
8678
|
+
const results = browser.currentTest?.results;
|
|
8679
|
+
const failed = !!results && ((results.errors ?? 0) > 0 || (results.failed ?? 0) > 0);
|
|
8680
|
+
await this.#emitTestArtifacts(this.#currentTestUid(), failed);
|
|
6405
8681
|
} catch (err) {
|
|
6406
|
-
|
|
8682
|
+
log13.error(`Failed to capture trace: ${errorMessage(err)}`);
|
|
6407
8683
|
}
|
|
6408
8684
|
}
|
|
6409
8685
|
}
|
|
6410
8686
|
async #closeOutTestcases(browser) {
|
|
6411
8687
|
await closeOutTestcases(this.#getInternals(), browser);
|
|
6412
8688
|
}
|
|
8689
|
+
/** Directory for produced artifacts — next to the test file, else the config,
|
|
8690
|
+
* else cwd (mirrors #traceContext's resolution). */
|
|
8691
|
+
get #outputDir() {
|
|
8692
|
+
return resolveAdapterOutputDir({
|
|
8693
|
+
testFilePath: this.browserProxy?.getCurrentTestFullPath?.() ?? void 0,
|
|
8694
|
+
configPath: this.#configPath
|
|
8695
|
+
});
|
|
8696
|
+
}
|
|
8697
|
+
/** Produce (never attach — no live Allure API) this test's per-test screenshot
|
|
8698
|
+
* and video slice. No-op outside trace mode + `test` granularity (core-gated).
|
|
8699
|
+
* Shared by the per-test `afterEach` and the cucumber per-scenario finalize. */
|
|
8700
|
+
async #emitTestArtifacts(uid, failed) {
|
|
8701
|
+
const attempt = uid ? this.#attemptTracker.attemptFor(uid) : void 0;
|
|
8702
|
+
await emitTestArtifacts({
|
|
8703
|
+
mode: this.options.mode,
|
|
8704
|
+
granularity: this.options.traceGranularity,
|
|
8705
|
+
screenshotPolicy: this.options.screenshot,
|
|
8706
|
+
videoPolicy: this.options.video,
|
|
8707
|
+
failed,
|
|
8708
|
+
actionSnapshots: this.sessionCapturer.actionSnapshots,
|
|
8709
|
+
frames: this.#screencastRecorder?.frames ?? this.#filmstripFrames,
|
|
8710
|
+
startWallTime: this.#currentTestStartWallTime,
|
|
8711
|
+
outcomes: uid ? this.#attemptTracker.forTest(uid, attempt) : [],
|
|
8712
|
+
uid,
|
|
8713
|
+
attempt,
|
|
8714
|
+
sessionId: this.sessionCapturer?.metadata?.sessionId,
|
|
8715
|
+
outputDir: this.#outputDir,
|
|
8716
|
+
captureFormat: this.#screencastOptions.captureFormat,
|
|
8717
|
+
onArtifact: (a) => this.#artifacts.push(a),
|
|
8718
|
+
onLog: (level, msg) => log13[level](msg)
|
|
8719
|
+
});
|
|
8720
|
+
}
|
|
6413
8721
|
async after(browser) {
|
|
6414
8722
|
await this.#finalizeCurrentScreencast();
|
|
6415
8723
|
try {
|
|
6416
8724
|
await this.#finalizeAllSuites(browser);
|
|
6417
8725
|
this.#logRunSummary();
|
|
6418
8726
|
if (this.options.mode === "trace") {
|
|
6419
|
-
await this.#
|
|
8727
|
+
await this.#writeTraceIfNeeded();
|
|
6420
8728
|
await this.sessionCapturer?.closeWebSocket();
|
|
6421
8729
|
await stopBackend();
|
|
6422
8730
|
return;
|
|
6423
8731
|
}
|
|
6424
8732
|
if (!this.#devtoolsBrowser) {
|
|
6425
8733
|
this.testReporter?.updateSuites();
|
|
6426
|
-
|
|
8734
|
+
log13.info("\u267B Rerun complete \u2014 flushing WebSocket...");
|
|
6427
8735
|
await this.sessionCapturer?.closeWebSocket();
|
|
6428
8736
|
return;
|
|
6429
8737
|
}
|
|
6430
|
-
|
|
8738
|
+
log13.info("\u{1F4A1} Please close the DevTools browser window to finish...");
|
|
6431
8739
|
await this.#waitForDevtoolsBrowserClose();
|
|
6432
8740
|
} catch (err) {
|
|
6433
|
-
|
|
8741
|
+
log13.error(`Failed to stop backend: ${errorMessage(err)}`);
|
|
6434
8742
|
}
|
|
6435
8743
|
}
|
|
6436
8744
|
async #finalizeAllSuites(browser) {
|
|
@@ -6439,90 +8747,56 @@ var NightwatchDevToolsPlugin = class {
|
|
|
6439
8747
|
#logRunSummary() {
|
|
6440
8748
|
logRunSummary(this.#getInternals());
|
|
6441
8749
|
}
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
this.#flushedSpecs.add(range.specFile);
|
|
8750
|
+
/** Thin wrapper so boundary flushes and the final flush share one path.
|
|
8751
|
+
* flushRangeLogged logs+swallows a failed flush (shared spec/test string) so
|
|
8752
|
+
* the fire-and-forget boundary callers don't each re-implement the guard. */
|
|
8753
|
+
#flushSpecTrace(range) {
|
|
6447
8754
|
const sessionId = this.sessionCapturer.metadata?.sessionId;
|
|
6448
8755
|
if (!sessionId) {
|
|
6449
|
-
return void 0;
|
|
6450
|
-
}
|
|
6451
|
-
const allMetadata = /* @__PURE__ */ new Map();
|
|
6452
|
-
if (this.suiteManager) {
|
|
6453
|
-
for (const suite of this.suiteManager.getAllSuites().values()) {
|
|
6454
|
-
for (const entry of suite.tests) {
|
|
6455
|
-
if (typeof entry === "string") {
|
|
6456
|
-
continue;
|
|
6457
|
-
}
|
|
6458
|
-
allMetadata.set(entry.uid, {
|
|
6459
|
-
title: entry.fullTitle,
|
|
6460
|
-
specFile: entry.file
|
|
6461
|
-
});
|
|
6462
|
-
}
|
|
6463
|
-
}
|
|
8756
|
+
return Promise.resolve(void 0);
|
|
6464
8757
|
}
|
|
6465
|
-
const
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
capturer: this.sessionCapturer,
|
|
6469
|
-
actionSnapshots: this.sessionCapturer.actionSnapshots,
|
|
6470
|
-
sessionId,
|
|
6471
|
-
outputDir: resolveAdapterOutputDir({ configPath: this.#configPath }),
|
|
6472
|
-
format: this.options.traceFormat,
|
|
6473
|
-
testMetadata: allMetadata
|
|
6474
|
-
});
|
|
6475
|
-
log11.info(`Trace for spec "${range.specFile}" saved to ${tracePath}`);
|
|
6476
|
-
return tracePath;
|
|
8758
|
+
const flush = flushRangeLogged(this.#traceContext(sessionId), range);
|
|
8759
|
+
this.#traceFlushes.push(flush);
|
|
8760
|
+
return flush;
|
|
6477
8761
|
}
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
try {
|
|
6487
|
-
if (this.sessionCapturer.snapshotCaptures.length) {
|
|
6488
|
-
await Promise.allSettled(this.sessionCapturer.snapshotCaptures);
|
|
6489
|
-
}
|
|
6490
|
-
if (this.options.traceGranularity === "spec") {
|
|
6491
|
-
for (const range of this.#specRanges) {
|
|
6492
|
-
if (!this.#flushedSpecs.has(range.specFile)) {
|
|
6493
|
-
await this.#flushSpecTrace(range);
|
|
6494
|
-
}
|
|
6495
|
-
}
|
|
6496
|
-
return;
|
|
6497
|
-
}
|
|
6498
|
-
const testMetadata = /* @__PURE__ */ new Map();
|
|
6499
|
-
if (this.suiteManager) {
|
|
6500
|
-
for (const suite of this.suiteManager.getAllSuites().values()) {
|
|
6501
|
-
for (const entry of suite.tests) {
|
|
6502
|
-
if (typeof entry === "string") {
|
|
6503
|
-
continue;
|
|
6504
|
-
}
|
|
6505
|
-
testMetadata.set(entry.uid, {
|
|
6506
|
-
title: entry.fullTitle,
|
|
6507
|
-
specFile: entry.file
|
|
6508
|
-
});
|
|
6509
|
-
}
|
|
6510
|
-
}
|
|
6511
|
-
}
|
|
6512
|
-
const snapshots = this.sessionCapturer.actionSnapshots;
|
|
6513
|
-
const tracePath = await writeTraceZip(this.sessionCapturer, {
|
|
6514
|
-
outputDir: resolveAdapterOutputDir({
|
|
6515
|
-
configPath: this.#configPath
|
|
6516
|
-
}),
|
|
6517
|
-
sessionId,
|
|
6518
|
-
actionSnapshots: snapshots.length ? snapshots : void 0,
|
|
8762
|
+
/** Assemble the framework-agnostic trace-export context from plugin state.
|
|
8763
|
+
* Output dir ignores the spec range — nightwatch writes next to config. */
|
|
8764
|
+
#traceContext(sessionId) {
|
|
8765
|
+
return buildTraceContext(
|
|
8766
|
+
{
|
|
8767
|
+
mode: this.options.mode,
|
|
8768
|
+
policy: this.options.tracePolicy,
|
|
8769
|
+
granularity: this.options.traceGranularity,
|
|
6519
8770
|
format: this.options.traceFormat,
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
8771
|
+
capturer: this.sessionCapturer,
|
|
8772
|
+
suites: this.suiteManager.getAllSuites().values(),
|
|
8773
|
+
outcomes: this.#attemptTracker,
|
|
8774
|
+
ranges: this.#specRanges,
|
|
8775
|
+
flushed: this.#flushedSpecs,
|
|
8776
|
+
artifacts: this.#artifacts,
|
|
8777
|
+
traceFlushes: this.#traceFlushes,
|
|
8778
|
+
emitArtifactsManifest: this.options.emitArtifactsManifest,
|
|
8779
|
+
// Accumulated (finalized-session) frames plus the live recorder's — a
|
|
8780
|
+
// mid-run per-spec/per-test flush fires before the recorder is drained
|
|
8781
|
+
// into #filmstripFrames, so its frames live only on the recorder still;
|
|
8782
|
+
// at the final write the recorder is already nulled, so no double-count.
|
|
8783
|
+
screencastFrames: this.options.filmstrip ? [
|
|
8784
|
+
...this.#filmstripFrames,
|
|
8785
|
+
...this.#screencastRecorder?.frames ?? []
|
|
8786
|
+
] : void 0,
|
|
8787
|
+
configPath: this.#configPath,
|
|
8788
|
+
testFilePath: this.browserProxy?.getCurrentTestFullPath?.() ?? void 0,
|
|
8789
|
+
log: (level, msg) => log13[level](msg)
|
|
8790
|
+
},
|
|
8791
|
+
sessionId
|
|
8792
|
+
);
|
|
8793
|
+
}
|
|
8794
|
+
async #writeTraceIfNeeded() {
|
|
8795
|
+
const sessionId = this.sessionCapturer?.metadata?.sessionId;
|
|
8796
|
+
if (this.options.mode !== "trace" || !this.sessionCapturer || !sessionId) {
|
|
8797
|
+
return;
|
|
6525
8798
|
}
|
|
8799
|
+
await finalizeTraceExport(this.#traceContext(sessionId));
|
|
6526
8800
|
}
|
|
6527
8801
|
async #waitForDevtoolsBrowserClose() {
|
|
6528
8802
|
await waitForDevtoolsBrowserClose(this.#getInternals());
|