@piwitests/reporter 0.26.1 → 0.28.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 +22 -286
- package/dist/cli/index.js +95 -22
- package/dist/global-setup-module.js +96 -7
- package/dist/index.d.ts +57 -2
- package/dist/index.js +1700 -171
- package/dist/internal/capture/attachments.d.ts +2 -0
- package/dist/internal/capture/attachments.js +2 -0
- package/dist/internal/capture/capture-fixtures.d.ts +8 -1
- package/dist/internal/capture/capture-fixtures.js +156 -4
- package/dist/internal/capture/locator-healing.js +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ __export(index_exports, {
|
|
|
43
43
|
module.exports = __toCommonJS(index_exports);
|
|
44
44
|
|
|
45
45
|
// src/public/reporter.ts
|
|
46
|
-
var
|
|
46
|
+
var path15 = __toESM(require("path"));
|
|
47
47
|
|
|
48
48
|
// src/internal/config/desktop.ts
|
|
49
49
|
var fs = __toESM(require("fs"));
|
|
@@ -77,9 +77,12 @@ var DEFAULTS = {
|
|
|
77
77
|
captureLocators: true,
|
|
78
78
|
capturePageState: true,
|
|
79
79
|
captureServerTraces: true,
|
|
80
|
+
sampleAriaOnPass: true,
|
|
81
|
+
defaultCapture: true,
|
|
80
82
|
streaming: true,
|
|
81
83
|
streamingBatchSize: 5,
|
|
82
84
|
streamingBatchDelay: 2e3,
|
|
85
|
+
maxStreamBufferBytes: 100 * 1024 * 1024,
|
|
83
86
|
failOnFlakyTests: false,
|
|
84
87
|
username: null,
|
|
85
88
|
password: null,
|
|
@@ -99,6 +102,7 @@ var PIWI_ENV_KEYS = {
|
|
|
99
102
|
streaming: "PIWI_STREAMING",
|
|
100
103
|
streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
|
|
101
104
|
streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
|
|
105
|
+
maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
|
|
102
106
|
liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
|
|
103
107
|
failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
|
|
104
108
|
uploadTraces: "PIWI_UPLOAD_TRACES",
|
|
@@ -106,6 +110,8 @@ var PIWI_ENV_KEYS = {
|
|
|
106
110
|
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
107
111
|
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
108
112
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
113
|
+
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
114
|
+
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
109
115
|
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
110
116
|
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
111
117
|
outputFile: "PIWI_OUTPUT_FILE",
|
|
@@ -119,6 +125,7 @@ var PIWI_ENV_KEYS = {
|
|
|
119
125
|
aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
|
|
120
126
|
};
|
|
121
127
|
var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
128
|
+
var PIWI_DEFAULTED_CAPTURE_ENV = "PIWI_DEFAULTED_CAPTURE";
|
|
122
129
|
var PIWI_SELECTION_ENV = {
|
|
123
130
|
key: "PIWI_SELECTION",
|
|
124
131
|
version: "PIWI_SELECTION_VERSION",
|
|
@@ -141,6 +148,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
141
148
|
{ option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
|
|
142
149
|
{ option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
|
|
143
150
|
{ option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
|
|
151
|
+
{ option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
|
|
144
152
|
{ option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
|
|
145
153
|
{ option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
|
|
146
154
|
{ option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
|
|
@@ -148,6 +156,8 @@ var ENV_FALLBACK_SPECS = [
|
|
|
148
156
|
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
149
157
|
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
150
158
|
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
159
|
+
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
160
|
+
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
151
161
|
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
152
162
|
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
153
163
|
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
@@ -203,6 +213,8 @@ function applyOptionsToEnv(options) {
|
|
|
203
213
|
if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
|
|
204
214
|
env[PIWI_ENV_KEYS.captureServerTraces] = "false";
|
|
205
215
|
else if (options.captureServerTraces === true) env[PIWI_ENV_KEYS.captureServerTraces] = "true";
|
|
216
|
+
if (options.sampleAriaOnPass === false) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "false";
|
|
217
|
+
else if (options.sampleAriaOnPass === true) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "true";
|
|
206
218
|
if (options.inspectOnFailure !== void 0) env[PIWI_ENV_KEYS.inspectOnFailure] = String(options.inspectOnFailure);
|
|
207
219
|
if (options.pickLocatorOnFailure !== void 0)
|
|
208
220
|
env[PIWI_ENV_KEYS.pickLocatorOnFailure] = String(options.pickLocatorOnFailure);
|
|
@@ -342,6 +354,29 @@ var HttpClient = class {
|
|
|
342
354
|
this.logger.debug("Logged in successfully");
|
|
343
355
|
return cookie;
|
|
344
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Send a JSON GET request, returning the parsed body, or `null` on any non-2xx
|
|
359
|
+
* status or parse failure. Unlike `postJSON` this never throws — its callers
|
|
360
|
+
* treat a missing or unreachable endpoint as "feature unavailable".
|
|
361
|
+
*/
|
|
362
|
+
async getJSON(pathname, auth) {
|
|
363
|
+
let res;
|
|
364
|
+
try {
|
|
365
|
+
res = await this.request("GET", pathname, { auth });
|
|
366
|
+
} catch (error) {
|
|
367
|
+
this.logger.debug(`GET ${pathname} failed: ${error.message}`);
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
if (res.status < 200 || res.status >= 300) {
|
|
371
|
+
this.logger.debug(`GET ${pathname} returned ${res.status}`);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
return JSON.parse(res.text);
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
345
380
|
/** Send a JSON POST request. `auth` can be an API key (prefix `pd_`) or a session cookie string. */
|
|
346
381
|
async postJSON(pathname, payload, auth) {
|
|
347
382
|
const body = JSON.stringify(payload);
|
|
@@ -392,7 +427,7 @@ var HttpClient = class {
|
|
|
392
427
|
{
|
|
393
428
|
hostname: url.hostname,
|
|
394
429
|
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
395
|
-
path: url.pathname,
|
|
430
|
+
path: url.pathname + url.search,
|
|
396
431
|
method,
|
|
397
432
|
headers
|
|
398
433
|
},
|
|
@@ -404,6 +439,12 @@ var HttpClient = class {
|
|
|
404
439
|
res.on("end", () => {
|
|
405
440
|
resolve5({ status: res.statusCode ?? 0, text: data, headers: res.headers });
|
|
406
441
|
});
|
|
442
|
+
res.on("error", reject);
|
|
443
|
+
res.on("close", () => {
|
|
444
|
+
if (!res.complete) {
|
|
445
|
+
reject(new Error(`Connection to ${pathname} closed before the response completed`));
|
|
446
|
+
}
|
|
447
|
+
});
|
|
407
448
|
}
|
|
408
449
|
);
|
|
409
450
|
req.on("error", reject);
|
|
@@ -472,7 +513,9 @@ function toWireTestCase(tc) {
|
|
|
472
513
|
pageState: rest.pageState || null,
|
|
473
514
|
aiUsage: rest.aiUsage || null,
|
|
474
515
|
consoleLogs: rest.consoleLogs || null,
|
|
516
|
+
dialogs: rest.dialogs || null,
|
|
475
517
|
ariaSnapshot: rest.ariaSnapshot || null,
|
|
518
|
+
ariaSnapshotJson: rest.ariaSnapshotJson || null,
|
|
476
519
|
testSource: rest.testSource || null,
|
|
477
520
|
testSourceFrames: rest.testSourceFrames || null,
|
|
478
521
|
browser: rest.browser || null,
|
|
@@ -480,6 +523,7 @@ function toWireTestCase(tc) {
|
|
|
480
523
|
suiteConfig: rest.suiteConfig ?? null,
|
|
481
524
|
testAnnotations: rest.testAnnotations ?? null,
|
|
482
525
|
tags: rest.tags ?? null,
|
|
526
|
+
locks: rest.locks ?? null,
|
|
483
527
|
testMeta: rest.testMeta ?? null,
|
|
484
528
|
locatorSnapshots: rest.locatorSnapshots || null,
|
|
485
529
|
didNotRunReason: rest.didNotRunReason ?? null,
|
|
@@ -831,6 +875,7 @@ var CrashRecovery = class {
|
|
|
831
875
|
|
|
832
876
|
// src/internal/files/file-handler.ts
|
|
833
877
|
var fs6 = __toESM(require("fs"));
|
|
878
|
+
var os5 = __toESM(require("os"));
|
|
834
879
|
var path6 = __toESM(require("path"));
|
|
835
880
|
var crypto2 = __toESM(require("crypto"));
|
|
836
881
|
|
|
@@ -867,7 +912,9 @@ async function compressDirectory(sourceDir) {
|
|
|
867
912
|
var ATTACHMENT_NAMES = {
|
|
868
913
|
locators: "piwi-locators",
|
|
869
914
|
ariaSnapshot: "piwi-aria-snapshot",
|
|
915
|
+
ariaSnapshotJson: "piwi-aria-snapshot-json",
|
|
870
916
|
console: "piwi-console",
|
|
917
|
+
dialogs: "piwi-dialogs",
|
|
871
918
|
network: "piwi-network",
|
|
872
919
|
webVitals: "piwi-web-vitals",
|
|
873
920
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
@@ -881,9 +928,30 @@ var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
|
881
928
|
var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
|
|
882
929
|
|
|
883
930
|
// src/internal/files/file-handler.ts
|
|
931
|
+
var MAX_ATTACHMENT_BYTES = 500 * 1024 * 1024;
|
|
932
|
+
var BODY_EXTENSIONS = {
|
|
933
|
+
"application/json": ".json",
|
|
934
|
+
"application/pdf": ".pdf",
|
|
935
|
+
"application/zip": ".zip",
|
|
936
|
+
"image/jpeg": ".jpg",
|
|
937
|
+
"image/png": ".png",
|
|
938
|
+
"image/svg+xml": ".svg",
|
|
939
|
+
"image/webp": ".webp",
|
|
940
|
+
"text/csv": ".csv",
|
|
941
|
+
"text/html": ".html",
|
|
942
|
+
"text/markdown": ".md",
|
|
943
|
+
"text/plain": ".txt"
|
|
944
|
+
};
|
|
884
945
|
var FileHandler = class {
|
|
885
|
-
constructor(logger = new Logger()) {
|
|
946
|
+
constructor(logger = new Logger(), maxAttachmentBytes = MAX_ATTACHMENT_BYTES) {
|
|
886
947
|
this.logger = logger;
|
|
948
|
+
this.maxAttachmentBytes = maxAttachmentBytes;
|
|
949
|
+
/** Temp files written for body-only attachments, keyed by attachment so repeated lookups reuse one file. */
|
|
950
|
+
this.bodyFiles = /* @__PURE__ */ new WeakMap();
|
|
951
|
+
this.bodyDir = null;
|
|
952
|
+
this.bodyFileCount = 0;
|
|
953
|
+
/** Attachments already reported as oversized, so each one warns once per run. */
|
|
954
|
+
this.oversizedWarned = /* @__PURE__ */ new Set();
|
|
887
955
|
}
|
|
888
956
|
/** Locate a Playwright HTML report directory containing `index.html`. Optionally override the search path. */
|
|
889
957
|
findHTMLReportDirectory(customDir) {
|
|
@@ -922,25 +990,83 @@ var FileHandler = class {
|
|
|
922
990
|
}
|
|
923
991
|
return Array.from(set);
|
|
924
992
|
}
|
|
925
|
-
/**
|
|
993
|
+
/**
|
|
994
|
+
* Return all non-trace, non-internal attachments from a test case as files
|
|
995
|
+
* on disk. Skips `trace` and `piwi-*` attachments. A body-only attachment
|
|
996
|
+
* (`testInfo.attach(name, { body })`) is written to a temp file under
|
|
997
|
+
* `os.tmpdir()` once and reused by later calls; anything above
|
|
998
|
+
* `maxAttachmentBytes` is skipped with a single warning.
|
|
999
|
+
*/
|
|
926
1000
|
findAllAttachments(testCase) {
|
|
927
1001
|
const result = [];
|
|
928
1002
|
if (testCase.attachments) {
|
|
929
1003
|
for (const a of testCase.attachments) {
|
|
930
1004
|
if (a.name === "trace") continue;
|
|
931
1005
|
if (a.name && INTERNAL_ATTACHMENT_NAMES.has(a.name)) continue;
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
name: a.name || "attachment",
|
|
935
|
-
path: path6.resolve(a.path),
|
|
936
|
-
contentType: a.contentType || "application/octet-stream",
|
|
937
|
-
originalName: path6.basename(a.path)
|
|
938
|
-
});
|
|
939
|
-
}
|
|
1006
|
+
const resolved = a.path ? this.resolvePathAttachment(a, testCase) : this.resolveBodyAttachment(a, testCase);
|
|
1007
|
+
if (resolved) result.push(resolved);
|
|
940
1008
|
}
|
|
941
1009
|
}
|
|
942
1010
|
return result;
|
|
943
1011
|
}
|
|
1012
|
+
/** Delete the temp files written for body-only attachments. Call once the run's uploads are done. */
|
|
1013
|
+
cleanupBodyAttachments() {
|
|
1014
|
+
if (!this.bodyDir) return;
|
|
1015
|
+
try {
|
|
1016
|
+
fs6.rmSync(this.bodyDir, { recursive: true, force: true });
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
this.logger.debug(`Could not remove temp attachments at ${this.bodyDir}: ${errorMessage(error)}`);
|
|
1019
|
+
}
|
|
1020
|
+
this.bodyDir = null;
|
|
1021
|
+
}
|
|
1022
|
+
resolvePathAttachment(a, testCase) {
|
|
1023
|
+
if (!a.path || !fs6.existsSync(a.path)) return null;
|
|
1024
|
+
const size = fs6.statSync(a.path).size;
|
|
1025
|
+
if (size > this.maxAttachmentBytes) {
|
|
1026
|
+
this.warnOversized(a, testCase, size);
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
name: a.name || "attachment",
|
|
1031
|
+
path: path6.resolve(a.path),
|
|
1032
|
+
contentType: a.contentType || "application/octet-stream",
|
|
1033
|
+
originalName: path6.basename(a.path)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
resolveBodyAttachment(a, testCase) {
|
|
1037
|
+
if (!a.body) return null;
|
|
1038
|
+
const body = Buffer.isBuffer(a.body) ? a.body : Buffer.from(a.body);
|
|
1039
|
+
if (body.length > this.maxAttachmentBytes) {
|
|
1040
|
+
this.warnOversized(a, testCase, body.length);
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
const contentType = a.contentType || "application/octet-stream";
|
|
1044
|
+
const originalName = `${safeFileName(a.name || "attachment")}${BODY_EXTENSIONS[contentType] ?? ""}`;
|
|
1045
|
+
let filePath = this.bodyFiles.get(a);
|
|
1046
|
+
if (!filePath) {
|
|
1047
|
+
try {
|
|
1048
|
+
filePath = path6.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
|
|
1049
|
+
fs6.writeFileSync(filePath, body);
|
|
1050
|
+
this.bodyFiles.set(a, filePath);
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
this.logger.warn(`Could not stage attachment "${a.name}" for upload: ${errorMessage(error)}`);
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return { name: a.name || "attachment", path: filePath, contentType, originalName };
|
|
1057
|
+
}
|
|
1058
|
+
bodyAttachmentDir() {
|
|
1059
|
+
if (!this.bodyDir) this.bodyDir = fs6.mkdtempSync(path6.join(os5.tmpdir(), "piwi-dashboard-attachments-"));
|
|
1060
|
+
return this.bodyDir;
|
|
1061
|
+
}
|
|
1062
|
+
warnOversized(a, testCase, size) {
|
|
1063
|
+
const key = `${testCase.location}\0${a.name}`;
|
|
1064
|
+
if (this.oversizedWarned.has(key)) return;
|
|
1065
|
+
this.oversizedWarned.add(key);
|
|
1066
|
+
this.logger.warn(
|
|
1067
|
+
`Skipping attachment "${a.name}" on "${testCase.title}": ${formatMiB(size)} exceeds the ${formatMiB(this.maxAttachmentBytes)} upload limit.`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
944
1070
|
/** Mapping of well-known report type names to their default output directories */
|
|
945
1071
|
getDefaultReportDirs() {
|
|
946
1072
|
return {
|
|
@@ -950,7 +1076,7 @@ var FileHandler = class {
|
|
|
950
1076
|
blob: "blob-report"
|
|
951
1077
|
};
|
|
952
1078
|
}
|
|
953
|
-
/** Parse Piwi-internal attachment bodies (`piwi-network`, `piwi-web-vitals`, `piwi-console`, `piwi-aria-snapshot`) into structured fields on the test case */
|
|
1079
|
+
/** Parse Piwi-internal attachment bodies (`piwi-network`, `piwi-web-vitals`, `piwi-console`, `piwi-aria-snapshot`, `piwi-aria-snapshot-json`) into structured fields on the test case */
|
|
954
1080
|
parsePerformanceAttachments(testCase, attachments) {
|
|
955
1081
|
const find = (name) => attachments.find((a) => a.name === name);
|
|
956
1082
|
const net = find(ATTACHMENT_NAMES.network);
|
|
@@ -974,8 +1100,17 @@ var FileHandler = class {
|
|
|
974
1100
|
} catch {
|
|
975
1101
|
}
|
|
976
1102
|
}
|
|
1103
|
+
const dialogs = find(ATTACHMENT_NAMES.dialogs);
|
|
1104
|
+
if (dialogs?.body) {
|
|
1105
|
+
try {
|
|
1106
|
+
testCase.dialogs = JSON.parse(dialogs.body.toString());
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
977
1110
|
const aria = find(ATTACHMENT_NAMES.ariaSnapshot);
|
|
978
1111
|
if (aria?.body) testCase.ariaSnapshot = aria.body.toString();
|
|
1112
|
+
const ariaJson = find(ATTACHMENT_NAMES.ariaSnapshotJson);
|
|
1113
|
+
if (ariaJson?.body) testCase.ariaSnapshotJson = ariaJson.body.toString();
|
|
979
1114
|
const pageState = find(ATTACHMENT_NAMES.pageState);
|
|
980
1115
|
if (pageState?.body) {
|
|
981
1116
|
try {
|
|
@@ -1022,6 +1157,12 @@ var FileHandler = class {
|
|
|
1022
1157
|
}
|
|
1023
1158
|
}
|
|
1024
1159
|
};
|
|
1160
|
+
function safeFileName(name) {
|
|
1161
|
+
return name.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "attachment";
|
|
1162
|
+
}
|
|
1163
|
+
function formatMiB(bytes) {
|
|
1164
|
+
return `${Math.round(bytes / (1024 * 1024) * 10) / 10} MiB`;
|
|
1165
|
+
}
|
|
1025
1166
|
|
|
1026
1167
|
// src/internal/collect/metadata-collector.ts
|
|
1027
1168
|
var import_node_child_process = require("child_process");
|
|
@@ -1055,6 +1196,20 @@ function resolveScmPrNumber(env) {
|
|
|
1055
1196
|
const trimmed = raw?.trim();
|
|
1056
1197
|
return trimmed && /^\d+$/.test(trimmed) ? trimmed : void 0;
|
|
1057
1198
|
}
|
|
1199
|
+
function resolveScmBaseBranch(env) {
|
|
1200
|
+
const override = env.PIWI_BASE_BRANCH?.trim();
|
|
1201
|
+
if (override) return override;
|
|
1202
|
+
let target;
|
|
1203
|
+
if (env.JENKINS_URL) target = env.CHANGE_TARGET;
|
|
1204
|
+
else if (env.GITHUB_ACTIONS) target = env.GITHUB_BASE_REF;
|
|
1205
|
+
else if (env.GITLAB_CI) target = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
1206
|
+
else if (env.TRAVIS)
|
|
1207
|
+
target = env.TRAVIS_PULL_REQUEST && env.TRAVIS_PULL_REQUEST !== "false" ? env.TRAVIS_BRANCH : void 0;
|
|
1208
|
+
else if (env.TF_BUILD) target = env.SYSTEM_PULLREQUEST_TARGETBRANCH;
|
|
1209
|
+
else if (env.BITBUCKET_BUILD_NUMBER) target = env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
1210
|
+
const trimmed = target?.trim();
|
|
1211
|
+
return trimmed ? normalizeRef(trimmed) : void 0;
|
|
1212
|
+
}
|
|
1058
1213
|
function normalizeRef(ref) {
|
|
1059
1214
|
return ref.replace(/^refs\/heads\//, "");
|
|
1060
1215
|
}
|
|
@@ -1122,6 +1277,7 @@ var MetadataCollector = class {
|
|
|
1122
1277
|
if (use.colorScheme) config.colorScheme = use.colorScheme;
|
|
1123
1278
|
if (use.reducedMotion) config.reducedMotion = use.reducedMotion;
|
|
1124
1279
|
if (use.forcedColors) config.forcedColors = use.forcedColors;
|
|
1280
|
+
if (use.contrast) config.contrast = use.contrast;
|
|
1125
1281
|
if (use.offline) config.offline = use.offline;
|
|
1126
1282
|
if (use.bypassCSP) config.bypassCSP = use.bypassCSP;
|
|
1127
1283
|
if (use.javaScriptEnabled === false) config.javaScriptEnabled = false;
|
|
@@ -1186,6 +1342,8 @@ var MetadataCollector = class {
|
|
|
1186
1342
|
if (branch) scm.branch = branch;
|
|
1187
1343
|
const prNumber = resolveScmPrNumber(process.env);
|
|
1188
1344
|
if (prNumber) scm.prNumber = prNumber;
|
|
1345
|
+
const baseBranch = resolveScmBaseBranch(process.env);
|
|
1346
|
+
if (baseBranch) scm.baseBranch = baseBranch;
|
|
1189
1347
|
return Object.keys(scm).length > 0 ? scm : void 0;
|
|
1190
1348
|
}
|
|
1191
1349
|
collectCiInfo() {
|
|
@@ -1264,6 +1422,132 @@ var MetadataCollector = class {
|
|
|
1264
1422
|
// src/internal/streaming/stream-manager.ts
|
|
1265
1423
|
var fs8 = __toESM(require("fs"));
|
|
1266
1424
|
|
|
1425
|
+
// src/internal/streaming/bounded-queue.ts
|
|
1426
|
+
var DROP_TIER = {
|
|
1427
|
+
"step-begin": 0,
|
|
1428
|
+
"step-end": 0,
|
|
1429
|
+
begin: 1,
|
|
1430
|
+
complete: 2
|
|
1431
|
+
};
|
|
1432
|
+
var CRITICAL_TIER = 2;
|
|
1433
|
+
var BoundedEventQueue = class {
|
|
1434
|
+
/**
|
|
1435
|
+
* @param maxBytes Byte budget for the buffered events. `<= 0` or non-finite
|
|
1436
|
+
* disables the bound (unbounded, i.e. the pre-existing behavior).
|
|
1437
|
+
*/
|
|
1438
|
+
constructor(maxBytes) {
|
|
1439
|
+
this.maxBytes = maxBytes;
|
|
1440
|
+
this.items = [];
|
|
1441
|
+
this._bytes = 0;
|
|
1442
|
+
this._peakBytes = 0;
|
|
1443
|
+
this._dropped = 0;
|
|
1444
|
+
this._droppedByType = {};
|
|
1445
|
+
this._lostResults = false;
|
|
1446
|
+
}
|
|
1447
|
+
/** Number of buffered events. */
|
|
1448
|
+
get length() {
|
|
1449
|
+
return this.items.length;
|
|
1450
|
+
}
|
|
1451
|
+
/** Whether the queue holds no events. */
|
|
1452
|
+
get isEmpty() {
|
|
1453
|
+
return this.items.length === 0;
|
|
1454
|
+
}
|
|
1455
|
+
/** Approximate byte size of the buffered events. */
|
|
1456
|
+
get bytes() {
|
|
1457
|
+
return this._bytes;
|
|
1458
|
+
}
|
|
1459
|
+
/** Highest byte size the queue reached over its lifetime. */
|
|
1460
|
+
get peakBytes() {
|
|
1461
|
+
return this._peakBytes;
|
|
1462
|
+
}
|
|
1463
|
+
/** How many events have been dropped by eviction. */
|
|
1464
|
+
get droppedCount() {
|
|
1465
|
+
return this._dropped;
|
|
1466
|
+
}
|
|
1467
|
+
/** Dropped-event counts broken down by event type. */
|
|
1468
|
+
get droppedByType() {
|
|
1469
|
+
return this._droppedByType;
|
|
1470
|
+
}
|
|
1471
|
+
/** True once a test-result (`complete`) event had to be shed — real live-data loss. */
|
|
1472
|
+
get lostResults() {
|
|
1473
|
+
return this._lostResults;
|
|
1474
|
+
}
|
|
1475
|
+
/** Append one event to the back of the queue, then evict if over budget. */
|
|
1476
|
+
enqueue(event) {
|
|
1477
|
+
const size = estimateSize(event);
|
|
1478
|
+
this.items.push({ event, size });
|
|
1479
|
+
this._bytes += size;
|
|
1480
|
+
this.afterGrow();
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Put events back at the front (oldest position), preserving their order, then
|
|
1484
|
+
* evict. Used to re-queue a failed flush ahead of anything that arrived while
|
|
1485
|
+
* it was in flight, and to replay events reloaded from the on-disk buffer.
|
|
1486
|
+
*/
|
|
1487
|
+
prepend(events) {
|
|
1488
|
+
if (events.length === 0) return;
|
|
1489
|
+
const head = [];
|
|
1490
|
+
let added = 0;
|
|
1491
|
+
for (const event of events) {
|
|
1492
|
+
const size = estimateSize(event);
|
|
1493
|
+
head.push({ event, size });
|
|
1494
|
+
added += size;
|
|
1495
|
+
}
|
|
1496
|
+
this.items = head.concat(this.items);
|
|
1497
|
+
this._bytes += added;
|
|
1498
|
+
this.afterGrow();
|
|
1499
|
+
}
|
|
1500
|
+
/** Remove and return every queued event, resetting the size counter. */
|
|
1501
|
+
takeAll() {
|
|
1502
|
+
const out = this.items.map((it) => it.event);
|
|
1503
|
+
this.items = [];
|
|
1504
|
+
this._bytes = 0;
|
|
1505
|
+
return out;
|
|
1506
|
+
}
|
|
1507
|
+
/** Read the queued events without removing them. */
|
|
1508
|
+
snapshot() {
|
|
1509
|
+
return this.items.map((it) => it.event);
|
|
1510
|
+
}
|
|
1511
|
+
/** Drop every queued event without counting it as an eviction. */
|
|
1512
|
+
clear() {
|
|
1513
|
+
this.items = [];
|
|
1514
|
+
this._bytes = 0;
|
|
1515
|
+
}
|
|
1516
|
+
afterGrow() {
|
|
1517
|
+
if (this._bytes > this._peakBytes) this._peakBytes = this._bytes;
|
|
1518
|
+
this.evict();
|
|
1519
|
+
}
|
|
1520
|
+
// Shed events tier by tier, oldest-first within a tier, until the buffer fits
|
|
1521
|
+
// its budget. A single event larger than the whole budget is kept rather than
|
|
1522
|
+
// leaving the queue empty, so the most recent result always survives.
|
|
1523
|
+
evict() {
|
|
1524
|
+
if (this.maxBytes <= 0 || !Number.isFinite(this.maxBytes)) return;
|
|
1525
|
+
for (let tier = 0; tier <= CRITICAL_TIER && this._bytes > this.maxBytes; tier++) {
|
|
1526
|
+
let i = 0;
|
|
1527
|
+
while (i < this.items.length && this._bytes > this.maxBytes) {
|
|
1528
|
+
if (tier === CRITICAL_TIER && this.items.length <= 1) break;
|
|
1529
|
+
const it = this.items[i];
|
|
1530
|
+
if (DROP_TIER[it.event.type] === tier) {
|
|
1531
|
+
this.items.splice(i, 1);
|
|
1532
|
+
this._bytes -= it.size;
|
|
1533
|
+
this._dropped++;
|
|
1534
|
+
this._droppedByType[it.event.type] = (this._droppedByType[it.event.type] ?? 0) + 1;
|
|
1535
|
+
if (tier === CRITICAL_TIER) this._lostResults = true;
|
|
1536
|
+
} else {
|
|
1537
|
+
i++;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
function estimateSize(event) {
|
|
1544
|
+
try {
|
|
1545
|
+
return JSON.stringify(event).length;
|
|
1546
|
+
} catch {
|
|
1547
|
+
return 0;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1267
1551
|
// src/internal/support/limiter.ts
|
|
1268
1552
|
function createLimiter(maxConcurrent) {
|
|
1269
1553
|
const limitValue = Math.max(1, Math.floor(maxConcurrent));
|
|
@@ -1288,10 +1572,10 @@ function createLimiter(maxConcurrent) {
|
|
|
1288
1572
|
|
|
1289
1573
|
// src/internal/support/setup-file.ts
|
|
1290
1574
|
var path7 = __toESM(require("path"));
|
|
1291
|
-
var
|
|
1575
|
+
var os6 = __toESM(require("os"));
|
|
1292
1576
|
var fs7 = __toESM(require("fs"));
|
|
1293
1577
|
function getSetupFilePath(projectName) {
|
|
1294
|
-
return path7.join(
|
|
1578
|
+
return path7.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
|
|
1295
1579
|
}
|
|
1296
1580
|
function readSetupInfo(projectName) {
|
|
1297
1581
|
const setupFile = getSetupFilePath(projectName);
|
|
@@ -1330,8 +1614,8 @@ var StreamManager = class {
|
|
|
1330
1614
|
this.fileHandler = fileHandler;
|
|
1331
1615
|
this.options = options;
|
|
1332
1616
|
this.logger = logger;
|
|
1333
|
-
|
|
1334
|
-
this.
|
|
1617
|
+
/** Guards `reportBufferPressure` so its summary is logged at most once. */
|
|
1618
|
+
this.bufferPressureReported = false;
|
|
1335
1619
|
this.flushTimer = null;
|
|
1336
1620
|
this.flushPromises = [];
|
|
1337
1621
|
this.liveUploadPromises = [];
|
|
@@ -1356,6 +1640,9 @@ var StreamManager = class {
|
|
|
1356
1640
|
this._token = null;
|
|
1357
1641
|
this._auth = null;
|
|
1358
1642
|
this._startPromise = null;
|
|
1643
|
+
const maxBytes = this.options.maxStreamBufferBytes ?? 0;
|
|
1644
|
+
this.pendingEvents = new BoundedEventQueue(maxBytes);
|
|
1645
|
+
this.pendingBeginEvents = new BoundedEventQueue(maxBytes);
|
|
1359
1646
|
}
|
|
1360
1647
|
/** Whether the streaming session is active */
|
|
1361
1648
|
get enabled() {
|
|
@@ -1377,6 +1664,15 @@ var StreamManager = class {
|
|
|
1377
1664
|
get startPromise() {
|
|
1378
1665
|
return this._startPromise;
|
|
1379
1666
|
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Whether buffer pressure forced a test-result (`complete`) event to be
|
|
1669
|
+
* dropped from the live stream. When true the submitter finalizes via the
|
|
1670
|
+
* end-of-run batch (which re-sends the full run from the reporter's own
|
|
1671
|
+
* memory) instead of `/finish`, so the dropped detail is not lost.
|
|
1672
|
+
*/
|
|
1673
|
+
get bufferLostResults() {
|
|
1674
|
+
return this.pendingEvents.lostResults || this.pendingBeginEvents.lostResults;
|
|
1675
|
+
}
|
|
1380
1676
|
/** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
|
|
1381
1677
|
start(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails) {
|
|
1382
1678
|
this._startPromise = this._doStart(
|
|
@@ -1470,8 +1766,7 @@ var StreamManager = class {
|
|
|
1470
1766
|
this.lastActivityAt = Date.now();
|
|
1471
1767
|
this.scheduleHeartbeat();
|
|
1472
1768
|
if (this.pendingBeginEvents.length > 0) {
|
|
1473
|
-
this.pendingEvents
|
|
1474
|
-
this.pendingBeginEvents = [];
|
|
1769
|
+
this.pendingEvents.prepend(this.pendingBeginEvents.takeAll());
|
|
1475
1770
|
}
|
|
1476
1771
|
if (this.pendingEvents.length > 0) this.flush();
|
|
1477
1772
|
}
|
|
@@ -1481,7 +1776,9 @@ var StreamManager = class {
|
|
|
1481
1776
|
`Live streaming could not start: the dashboard at ${this.options.serverUrl} requires authentication. Set the reporter's \`apiKey\` option (or PIWI_API_KEY) \u2014 create a key under Settings \u2192 Users on the dashboard. Falling back to batch upload at the end of the run.`
|
|
1482
1777
|
);
|
|
1483
1778
|
} else {
|
|
1484
|
-
this.logger.
|
|
1779
|
+
this.logger.warn(
|
|
1780
|
+
`Live streaming is unavailable (${errorMessage(error)}) \u2014 results will be submitted in one batch when the run finishes.`
|
|
1781
|
+
);
|
|
1485
1782
|
}
|
|
1486
1783
|
this._enabled = false;
|
|
1487
1784
|
}
|
|
@@ -1493,12 +1790,12 @@ var StreamManager = class {
|
|
|
1493
1790
|
if (this._enabled && this._runId) {
|
|
1494
1791
|
this.queueEvent(event);
|
|
1495
1792
|
} else {
|
|
1496
|
-
this.pendingBeginEvents.
|
|
1793
|
+
this.pendingBeginEvents.enqueue(event);
|
|
1497
1794
|
}
|
|
1498
1795
|
}
|
|
1499
1796
|
/** Queue a test-case event. Triggers an immediate flush when the batch size is reached, otherwise schedules a timer-based flush. */
|
|
1500
1797
|
queueEvent(event) {
|
|
1501
|
-
this.pendingEvents.
|
|
1798
|
+
this.pendingEvents.enqueue(event);
|
|
1502
1799
|
if (this.pendingEvents.length >= this.options.streamingBatchSize) {
|
|
1503
1800
|
this.flush();
|
|
1504
1801
|
} else if (!this.flushTimer) {
|
|
@@ -1511,26 +1808,29 @@ var StreamManager = class {
|
|
|
1511
1808
|
clearTimeout(this.flushTimer);
|
|
1512
1809
|
this.flushTimer = null;
|
|
1513
1810
|
}
|
|
1514
|
-
if (this.pendingEvents.
|
|
1515
|
-
const events = this.pendingEvents.
|
|
1811
|
+
if (this.pendingEvents.isEmpty || !this._enabled || !this._runId) return null;
|
|
1812
|
+
const events = this.pendingEvents.takeAll();
|
|
1516
1813
|
const promise = this.httpClient.postJSON(`/api/test-runs/${this._runId}/events`, { streamToken: this._token, testCases: events }, this._auth).then(
|
|
1517
1814
|
() => {
|
|
1518
1815
|
this.retryCount = 0;
|
|
1519
1816
|
this.lastActivityAt = Date.now();
|
|
1520
1817
|
return true;
|
|
1521
1818
|
},
|
|
1522
|
-
() => {
|
|
1523
|
-
this.pendingEvents
|
|
1524
|
-
this.scheduleRetry();
|
|
1819
|
+
(error) => {
|
|
1820
|
+
this.pendingEvents.prepend(events);
|
|
1821
|
+
this.scheduleRetry(errorMessage(error));
|
|
1525
1822
|
return false;
|
|
1526
1823
|
}
|
|
1527
1824
|
);
|
|
1528
1825
|
this.flushPromises.push(promise);
|
|
1529
1826
|
return promise;
|
|
1530
1827
|
}
|
|
1531
|
-
scheduleRetry() {
|
|
1828
|
+
scheduleRetry(reason) {
|
|
1532
1829
|
if (this.retryTimer) return;
|
|
1533
1830
|
this.retryCount++;
|
|
1831
|
+
if (this.retryCount === 1) {
|
|
1832
|
+
this.logger.warn(`Streaming to the dashboard was interrupted (${reason}) \u2014 events are buffered and retried.`);
|
|
1833
|
+
}
|
|
1534
1834
|
const delay = Math.min(1e3 * Math.pow(2, this.retryCount - 1), this.maxRetryDelay);
|
|
1535
1835
|
this.logger.debug(`Will retry streaming flush in ${delay}ms (attempt ${this.retryCount})`);
|
|
1536
1836
|
this.retryTimer = setTimeout(() => {
|
|
@@ -1538,7 +1838,7 @@ var StreamManager = class {
|
|
|
1538
1838
|
const buffered = this.streamBuffer.load();
|
|
1539
1839
|
if (buffered.length > 0) {
|
|
1540
1840
|
this.streamBuffer.clear();
|
|
1541
|
-
this.pendingEvents
|
|
1841
|
+
this.pendingEvents.prepend(buffered);
|
|
1542
1842
|
}
|
|
1543
1843
|
if (this.pendingEvents.length > 0) this.flush();
|
|
1544
1844
|
}, delay);
|
|
@@ -1581,38 +1881,89 @@ var StreamManager = class {
|
|
|
1581
1881
|
this.heartbeatTimer = null;
|
|
1582
1882
|
}
|
|
1583
1883
|
}
|
|
1884
|
+
/** Clear a scheduled flush retry so its timer cannot fire after the run wraps up. */
|
|
1885
|
+
clearRetryTimer() {
|
|
1886
|
+
if (this.retryTimer) {
|
|
1887
|
+
clearTimeout(this.retryTimer);
|
|
1888
|
+
this.retryTimer = null;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1584
1891
|
/** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
|
|
1585
1892
|
async drain() {
|
|
1893
|
+
try {
|
|
1894
|
+
await this._drain();
|
|
1895
|
+
} finally {
|
|
1896
|
+
this.reportBufferPressure();
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
async _drain() {
|
|
1586
1900
|
this.stopHeartbeat();
|
|
1901
|
+
this.clearRetryTimer();
|
|
1587
1902
|
if (!this._enabled) {
|
|
1588
|
-
this.pendingEvents
|
|
1903
|
+
this.pendingEvents.clear();
|
|
1589
1904
|
this.flushPromises = [];
|
|
1590
1905
|
return;
|
|
1591
1906
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
if (this.pendingEvents.length === 0) {
|
|
1600
|
-
const buffered = this.streamBuffer.load();
|
|
1601
|
-
if (buffered.length > 0) {
|
|
1602
|
-
this.pendingEvents = buffered;
|
|
1603
|
-
this.streamBuffer.clear();
|
|
1604
|
-
continue;
|
|
1907
|
+
try {
|
|
1908
|
+
const MAX_ATTEMPTS = 10;
|
|
1909
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1910
|
+
if (this._enabled && !this.pendingEvents.isEmpty) this.flush();
|
|
1911
|
+
if (this.flushPromises.length > 0) {
|
|
1912
|
+
await Promise.allSettled(this.flushPromises);
|
|
1913
|
+
this.flushPromises = [];
|
|
1605
1914
|
}
|
|
1606
|
-
|
|
1915
|
+
if (this.pendingEvents.isEmpty) {
|
|
1916
|
+
const buffered = this.streamBuffer.load();
|
|
1917
|
+
if (buffered.length > 0) {
|
|
1918
|
+
this.streamBuffer.clear();
|
|
1919
|
+
this.pendingEvents.prepend(buffered);
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
if (attempt === 0) {
|
|
1925
|
+
this.logger.warn(
|
|
1926
|
+
`The dashboard has not accepted ${this.pendingEvents.length} live event(s) yet \u2014 retrying delivery before the final submit (this can take a few minutes)...`
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
this.logger.debugError(
|
|
1930
|
+
`${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`
|
|
1931
|
+
);
|
|
1932
|
+
await new Promise((resolve5) => setTimeout(resolve5, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
|
|
1933
|
+
}
|
|
1934
|
+
if (!this.pendingEvents.isEmpty) {
|
|
1935
|
+
const remaining = this.pendingEvents.takeAll();
|
|
1936
|
+
this.logger.warn(
|
|
1937
|
+
`Could not deliver ${remaining.length} live event(s) to the dashboard \u2014 continuing with the end-of-run submit.`
|
|
1938
|
+
);
|
|
1939
|
+
this.streamBuffer.append(remaining);
|
|
1607
1940
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1941
|
+
} finally {
|
|
1942
|
+
this.clearRetryTimer();
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
// Emit a single summary when buffer pressure shed events, so a full disk or a
|
|
1946
|
+
// stalled server does not silently swallow data. Reported once per drain.
|
|
1947
|
+
reportBufferPressure() {
|
|
1948
|
+
if (this.bufferPressureReported) return;
|
|
1949
|
+
const dropped = this.pendingEvents.droppedCount + this.pendingBeginEvents.droppedCount;
|
|
1950
|
+
if (dropped === 0) return;
|
|
1951
|
+
this.bufferPressureReported = true;
|
|
1952
|
+
const byType = this.pendingEvents.droppedByType;
|
|
1953
|
+
const beginByType = this.pendingBeginEvents.droppedByType;
|
|
1954
|
+
const count = (type) => (byType[type] ?? 0) + (beginByType[type] ?? 0);
|
|
1955
|
+
const steps = count("step-begin") + count("step-end");
|
|
1956
|
+
const begins = count("begin");
|
|
1957
|
+
const results = count("complete");
|
|
1958
|
+
const limitMb = ((this.options.maxStreamBufferBytes ?? 0) / (1024 * 1024)).toFixed(0);
|
|
1959
|
+
if (results > 0) {
|
|
1960
|
+
this.logger.warn(
|
|
1961
|
+
`Stream buffer hit its ${limitMb} MB limit \u2014 dropped ${dropped} live event(s), including ${results} test result(s) that will not show live on the dashboard (the run's final counts stay accurate, and the end-of-run submit still carries the full run). Raise \`maxStreamBufferBytes\` or check the dashboard connection.`
|
|
1962
|
+
);
|
|
1963
|
+
} else {
|
|
1964
|
+
this.logger.warn(
|
|
1965
|
+
`Stream buffer hit its ${limitMb} MB limit \u2014 dropped ${dropped} live progress event(s) (${steps} step, ${begins} begin) to stay within budget. No test results were lost.`
|
|
1610
1966
|
);
|
|
1611
|
-
await new Promise((resolve5) => setTimeout(resolve5, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
|
|
1612
|
-
}
|
|
1613
|
-
if (this.pendingEvents.length > 0) {
|
|
1614
|
-
this.streamBuffer.append(this.pendingEvents);
|
|
1615
|
-
this.pendingEvents = [];
|
|
1616
1967
|
}
|
|
1617
1968
|
}
|
|
1618
1969
|
/** Schedule a live upload of trace and attachment files for a test case. Skips cases with no files. Concurrency is limited to 2 simultaneous uploads. */
|
|
@@ -1681,15 +2032,26 @@ var StreamManager = class {
|
|
|
1681
2032
|
}
|
|
1682
2033
|
};
|
|
1683
2034
|
|
|
2035
|
+
// ../core/src/mask.ts
|
|
2036
|
+
var DATA_URI_RE = /\bdata:[a-z0-9.+-]+\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/gi;
|
|
2037
|
+
var JWT_RE = /\beyJ[\w-]{10,}\.[\w-]{5,}\.[\w-]{5,}\b/g;
|
|
2038
|
+
var LONG_HEX_RE = /\b[0-9a-f]{32,}\b/gi;
|
|
2039
|
+
function maskTokenLike(text) {
|
|
2040
|
+
return text.replace(DATA_URI_RE, "data:[masked]").replace(JWT_RE, "[masked-token]").replace(LONG_HEX_RE, "[masked-hex]");
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1684
2043
|
// ../core/src/step-analysis.ts
|
|
1685
|
-
|
|
2044
|
+
var MAX_STEP_PARAM_KEYS = 20;
|
|
2045
|
+
var MAX_STEP_PARAM_VALUE_CHARS = 200;
|
|
2046
|
+
function categorizeStep(title, pwCategory, params) {
|
|
1686
2047
|
if (!title) return "other";
|
|
1687
2048
|
if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
|
|
1688
2049
|
if (pwCategory === "expect") return "assertion";
|
|
1689
2050
|
const lower = title.toLowerCase();
|
|
1690
2051
|
if (lower.startsWith("wait for") || lower.startsWith("locator.waitfor") || lower.startsWith("page.waitfor") || lower.startsWith("frame.waitfor"))
|
|
1691
2052
|
return "wait";
|
|
1692
|
-
if (
|
|
2053
|
+
if (typeof params?.url === "string" && pwCategory !== "expect") return "navigation";
|
|
2054
|
+
if (lower.startsWith("navigate") || lower.startsWith("go back") || lower.startsWith("go forward") || lower.startsWith("reload") || lower.startsWith("page.goto") || lower.startsWith("page.reload") || lower.startsWith("page.goback") || lower.startsWith("page.goforward"))
|
|
1693
2055
|
return "navigation";
|
|
1694
2056
|
if (lower.startsWith("click") || lower.startsWith("double click") || lower.startsWith("check") || lower.startsWith("uncheck") || lower.startsWith("tap") || lower.startsWith("hover") || lower.startsWith("select option") || lower.startsWith("drag") || lower.startsWith("locator.click") || lower.startsWith("locator.dblclick") || lower.startsWith("locator.check") || lower.startsWith("locator.uncheck") || lower.startsWith("locator.selectoption") || lower.startsWith("locator.tap"))
|
|
1695
2057
|
return "action";
|
|
@@ -1701,14 +2063,39 @@ function categorizeStep(title, pwCategory) {
|
|
|
1701
2063
|
if (lower === "before hooks" || lower === "after hooks" || lower.startsWith("fixture:")) return "hook";
|
|
1702
2064
|
return "other";
|
|
1703
2065
|
}
|
|
2066
|
+
function normalizeStepParams(raw) {
|
|
2067
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
2068
|
+
const out = {};
|
|
2069
|
+
let count = 0;
|
|
2070
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2071
|
+
if (count >= MAX_STEP_PARAM_KEYS) break;
|
|
2072
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
2073
|
+
out[key] = value;
|
|
2074
|
+
count++;
|
|
2075
|
+
} else if (typeof value === "string") {
|
|
2076
|
+
out[key] = maskTokenLike(value).slice(0, MAX_STEP_PARAM_VALUE_CHARS);
|
|
2077
|
+
count++;
|
|
2078
|
+
} else if (value != null) {
|
|
2079
|
+
try {
|
|
2080
|
+
out[key] = maskTokenLike(JSON.stringify(value)).slice(0, MAX_STEP_PARAM_VALUE_CHARS);
|
|
2081
|
+
count++;
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return count > 0 ? out : void 0;
|
|
2087
|
+
}
|
|
1704
2088
|
function flattenSteps(steps) {
|
|
1705
2089
|
const result = [];
|
|
1706
2090
|
for (const step of steps) {
|
|
1707
2091
|
const flat = {
|
|
1708
2092
|
title: step.title,
|
|
1709
2093
|
duration: step.duration,
|
|
1710
|
-
category: categorizeStep(step.title, step.category)
|
|
2094
|
+
category: categorizeStep(step.title, step.category, step.params)
|
|
1711
2095
|
};
|
|
2096
|
+
if (typeof step.subtitle === "string" && step.subtitle.length > 0) flat.subtitle = maskTokenLike(step.subtitle);
|
|
2097
|
+
const params = normalizeStepParams(step.params);
|
|
2098
|
+
if (params) flat.params = params;
|
|
1712
2099
|
if (step.error?.message) {
|
|
1713
2100
|
flat.error = { message: step.error.message };
|
|
1714
2101
|
flat.failed = true;
|
|
@@ -1864,7 +2251,7 @@ function readSourceSnippet(file, declLine, context, failingLine) {
|
|
|
1864
2251
|
return null;
|
|
1865
2252
|
}
|
|
1866
2253
|
}
|
|
1867
|
-
function collectSourceFrames(errorText,
|
|
2254
|
+
function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
|
|
1868
2255
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
1869
2256
|
const context = opts.context ?? 8;
|
|
1870
2257
|
const maxFrames = opts.maxFrames ?? 4;
|
|
@@ -1893,9 +2280,9 @@ function collectSourceFrames(errorText, testFile, declLine, opts = {}) {
|
|
|
1893
2280
|
if (!isNaN(line)) add(abs, line);
|
|
1894
2281
|
}
|
|
1895
2282
|
}
|
|
1896
|
-
const absTest = path9.resolve(
|
|
2283
|
+
const absTest = path9.resolve(testFile2);
|
|
1897
2284
|
if (!picked.some((p) => p.absFile === absTest)) {
|
|
1898
|
-
add(absTest, extractFailingLine(errorText,
|
|
2285
|
+
add(absTest, extractFailingLine(errorText, testFile2, declLine));
|
|
1899
2286
|
}
|
|
1900
2287
|
const frames = [];
|
|
1901
2288
|
for (const p of picked.slice(0, maxFrames)) {
|
|
@@ -1906,9 +2293,9 @@ function collectSourceFrames(errorText, testFile, declLine, opts = {}) {
|
|
|
1906
2293
|
}
|
|
1907
2294
|
return frames;
|
|
1908
2295
|
}
|
|
1909
|
-
function extractFailingLine(errorText,
|
|
2296
|
+
function extractFailingLine(errorText, testFile2, declarationLine) {
|
|
1910
2297
|
if (!errorText) return declarationLine;
|
|
1911
|
-
const expectedFile = path9.resolve(
|
|
2298
|
+
const expectedFile = path9.resolve(testFile2);
|
|
1912
2299
|
const stackRe = /^\s+at (?:[^(]*\()?(.+?):(\d+):\d+\)?\s*$/gm;
|
|
1913
2300
|
let m;
|
|
1914
2301
|
while ((m = stackRe.exec(errorText)) !== null) {
|
|
@@ -1996,10 +2383,6 @@ function readSelectionStamp(env = process.env) {
|
|
|
1996
2383
|
return { key, version, resolvedHash, resolvedCount };
|
|
1997
2384
|
}
|
|
1998
2385
|
|
|
1999
|
-
// src/public/global-setup.ts
|
|
2000
|
-
var path10 = __toESM(require("path"));
|
|
2001
|
-
var fs11 = __toESM(require("fs"));
|
|
2002
|
-
|
|
2003
2386
|
// src/internal/support/run-mode.ts
|
|
2004
2387
|
var PW_UI_FLAGS = ["--ui", "--ui-host", "--ui-port"];
|
|
2005
2388
|
function isUiMode(argv = process.argv) {
|
|
@@ -2008,18 +2391,78 @@ function isUiMode(argv = process.argv) {
|
|
|
2008
2391
|
const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
|
|
2009
2392
|
return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
|
|
2010
2393
|
}
|
|
2394
|
+
var PW_LIST_FLAGS = ["--list"];
|
|
2395
|
+
function isListMode(argv = process.argv) {
|
|
2396
|
+
const args = argv.slice(2);
|
|
2397
|
+
const testIdx = args.indexOf("test");
|
|
2398
|
+
const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
|
|
2399
|
+
return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// src/public/global-setup.ts
|
|
2403
|
+
var path11 = __toESM(require("path"));
|
|
2404
|
+
var fs12 = __toESM(require("fs"));
|
|
2405
|
+
|
|
2406
|
+
// src/internal/support/aria-sampling.ts
|
|
2407
|
+
var path10 = __toESM(require("path"));
|
|
2408
|
+
var os7 = __toESM(require("os"));
|
|
2409
|
+
var fs11 = __toESM(require("fs"));
|
|
2410
|
+
function ariaSampleIdentity(filePath, title) {
|
|
2411
|
+
return `${filePath}\0${title}`;
|
|
2412
|
+
}
|
|
2413
|
+
function getAriaSampleFilePath(projectName) {
|
|
2414
|
+
return path10.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
|
|
2415
|
+
}
|
|
2416
|
+
function writeAriaSampleFile(projectName, identities) {
|
|
2417
|
+
try {
|
|
2418
|
+
fs11.writeFileSync(getAriaSampleFilePath(projectName), JSON.stringify({ projectName, identities }));
|
|
2419
|
+
} catch {
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
function clearAriaSampleFile(projectName) {
|
|
2423
|
+
try {
|
|
2424
|
+
fs11.rmSync(getAriaSampleFilePath(projectName), { force: true });
|
|
2425
|
+
} catch {
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
var cachedSets = /* @__PURE__ */ new Map();
|
|
2429
|
+
function loadAriaSampleSet(projectName) {
|
|
2430
|
+
if (cachedSets.has(projectName)) return cachedSets.get(projectName);
|
|
2431
|
+
let set = null;
|
|
2432
|
+
try {
|
|
2433
|
+
const raw = fs11.readFileSync(getAriaSampleFilePath(projectName), "utf8");
|
|
2434
|
+
const parsed = JSON.parse(raw);
|
|
2435
|
+
if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
|
|
2436
|
+
set = new Set(parsed.identities.filter((x) => typeof x === "string"));
|
|
2437
|
+
}
|
|
2438
|
+
} catch {
|
|
2439
|
+
set = null;
|
|
2440
|
+
}
|
|
2441
|
+
cachedSets.set(projectName, set);
|
|
2442
|
+
return set;
|
|
2443
|
+
}
|
|
2444
|
+
function relativeTestFile(file) {
|
|
2445
|
+
return path10.relative(process.cwd(), file).split(path10.sep).join("/");
|
|
2446
|
+
}
|
|
2447
|
+
function isDueForAriaSample(testInfo) {
|
|
2448
|
+
const projectName = process.env.PIWI_PROJECT_NAME;
|
|
2449
|
+
if (!projectName) return false;
|
|
2450
|
+
const set = loadAriaSampleSet(projectName);
|
|
2451
|
+
if (!set || set.size === 0) return false;
|
|
2452
|
+
return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
|
|
2453
|
+
}
|
|
2011
2454
|
|
|
2012
2455
|
// src/public/global-setup.ts
|
|
2013
2456
|
function createGlobalSetup(options, userSetup) {
|
|
2014
2457
|
return async function globalSetupFn(config) {
|
|
2015
|
-
const piwiReporterPath =
|
|
2458
|
+
const piwiReporterPath = path11.resolve(__dirname, "./index.js");
|
|
2016
2459
|
let inlineReporterOptions = {};
|
|
2017
2460
|
if (Array.isArray(config?.reporter)) {
|
|
2018
2461
|
for (const r of config.reporter) {
|
|
2019
2462
|
if (!Array.isArray(r) || typeof r[0] !== "string") continue;
|
|
2020
2463
|
const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
|
|
2021
2464
|
try {
|
|
2022
|
-
return
|
|
2465
|
+
return path11.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
2023
2466
|
} catch {
|
|
2024
2467
|
return false;
|
|
2025
2468
|
}
|
|
@@ -2037,6 +2480,11 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2037
2480
|
if (userSetup) return userSetup(config);
|
|
2038
2481
|
return;
|
|
2039
2482
|
}
|
|
2483
|
+
if (isListMode()) {
|
|
2484
|
+
logger.debug("List mode detected \u2014 skipping run registration.");
|
|
2485
|
+
if (userSetup) return userSetup(config);
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2040
2488
|
if (opts.enabled === false || !opts.serverUrl) {
|
|
2041
2489
|
logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
2042
2490
|
if (userSetup) return userSetup(config);
|
|
@@ -2046,7 +2494,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2046
2494
|
if (!Array.isArray(r) || typeof r[0] !== "string") return false;
|
|
2047
2495
|
if (r[0].toLowerCase().includes("piwi")) return true;
|
|
2048
2496
|
try {
|
|
2049
|
-
return
|
|
2497
|
+
return path11.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
2050
2498
|
} catch {
|
|
2051
2499
|
return false;
|
|
2052
2500
|
}
|
|
@@ -2078,7 +2526,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2078
2526
|
auth
|
|
2079
2527
|
);
|
|
2080
2528
|
if (response?.runId && response?.setupToken) {
|
|
2081
|
-
|
|
2529
|
+
fs12.writeFileSync(
|
|
2082
2530
|
getSetupFilePath(opts.projectName),
|
|
2083
2531
|
JSON.stringify({
|
|
2084
2532
|
runId: response.runId,
|
|
@@ -2088,6 +2536,22 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2088
2536
|
);
|
|
2089
2537
|
logger.debug(`Global setup: initializing run #${response.runId}`);
|
|
2090
2538
|
}
|
|
2539
|
+
if (opts.projectName) clearAriaSampleFile(opts.projectName);
|
|
2540
|
+
if (opts.sampleAriaOnPass !== false && opts.projectName) {
|
|
2541
|
+
const menu = await httpClient.getJSON("/api/projects/menu", auth);
|
|
2542
|
+
const projectId = menu?.items?.find(
|
|
2543
|
+
(p) => p.name.toLowerCase() === opts.projectName.toLowerCase()
|
|
2544
|
+
)?.id;
|
|
2545
|
+
if (projectId != null) {
|
|
2546
|
+
const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
|
|
2547
|
+
const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
|
|
2548
|
+
if (tests) {
|
|
2549
|
+
const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
|
|
2550
|
+
writeAriaSampleFile(opts.projectName, identities);
|
|
2551
|
+
logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2091
2555
|
} catch (error) {
|
|
2092
2556
|
logger.warn(`Could not register global setup: ${errorMessage(error)}`);
|
|
2093
2557
|
}
|
|
@@ -2096,9 +2560,56 @@ function createGlobalSetup(options, userSetup) {
|
|
|
2096
2560
|
}
|
|
2097
2561
|
|
|
2098
2562
|
// src/public/config-wrapper.ts
|
|
2099
|
-
var
|
|
2100
|
-
var
|
|
2563
|
+
var path12 = __toESM(require("path"));
|
|
2564
|
+
var fs13 = __toESM(require("fs"));
|
|
2101
2565
|
var PIWI_MODULE = "@piwitests/reporter";
|
|
2566
|
+
var CAPTURE_DEFAULTS = {
|
|
2567
|
+
screenshot: "only-on-failure",
|
|
2568
|
+
trace: "retain-on-failure"
|
|
2569
|
+
};
|
|
2570
|
+
var TRACE_SNAPSHOTS_MIN = { major: 1, minor: 63 };
|
|
2571
|
+
function installedPlaywrightVersion() {
|
|
2572
|
+
try {
|
|
2573
|
+
const nodeRequire = require;
|
|
2574
|
+
return nodeRequire("@playwright/test/package.json").version;
|
|
2575
|
+
} catch {
|
|
2576
|
+
return void 0;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
var readPlaywrightVersion = installedPlaywrightVersion;
|
|
2580
|
+
function supportsTraceSnapshots(version) {
|
|
2581
|
+
const match = version ? /^(\d+)\.(\d+)/.exec(version) : null;
|
|
2582
|
+
if (!match) return false;
|
|
2583
|
+
const major = Number(match[1]);
|
|
2584
|
+
const minor = Number(match[2]);
|
|
2585
|
+
return major > TRACE_SNAPSHOTS_MIN.major || major === TRACE_SNAPSHOTS_MIN.major && minor >= TRACE_SNAPSHOTS_MIN.minor;
|
|
2586
|
+
}
|
|
2587
|
+
function applyCaptureDefaults(use, piwiOptions) {
|
|
2588
|
+
delete process.env[PIWI_DEFAULTED_CAPTURE_ENV];
|
|
2589
|
+
const enabled = piwiOptions?.defaultCapture ?? readBool(process.env[PIWI_ENV_KEYS.defaultCapture]) ?? true;
|
|
2590
|
+
if (!enabled) return use;
|
|
2591
|
+
const ariaSnapshots = supportsTraceSnapshots(readPlaywrightVersion());
|
|
2592
|
+
const traceValue = ariaSnapshots ? { mode: CAPTURE_DEFAULTS.trace, snapshots: { dom: true, aria: true } } : CAPTURE_DEFAULTS.trace;
|
|
2593
|
+
const defaults = [
|
|
2594
|
+
{ key: "screenshot", value: CAPTURE_DEFAULTS.screenshot, display: `screenshot: '${CAPTURE_DEFAULTS.screenshot}'` },
|
|
2595
|
+
{
|
|
2596
|
+
key: "trace",
|
|
2597
|
+
value: traceValue,
|
|
2598
|
+
display: ariaSnapshots ? `trace: '${CAPTURE_DEFAULTS.trace}' with dom and aria snapshots` : `trace: '${CAPTURE_DEFAULTS.trace}'`
|
|
2599
|
+
}
|
|
2600
|
+
];
|
|
2601
|
+
const next = { ...use };
|
|
2602
|
+
const applied = [];
|
|
2603
|
+
for (const { key, value, display } of defaults) {
|
|
2604
|
+
if (use?.[key] === void 0) {
|
|
2605
|
+
next[key] = value;
|
|
2606
|
+
applied.push(display);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (applied.length === 0) return use;
|
|
2610
|
+
process.env[PIWI_DEFAULTED_CAPTURE_ENV] = applied.join(", ");
|
|
2611
|
+
return next;
|
|
2612
|
+
}
|
|
2102
2613
|
function isPiwiReporterEntry(entry) {
|
|
2103
2614
|
if (typeof entry === "string") return entry.toLowerCase().includes("piwi");
|
|
2104
2615
|
if (Array.isArray(entry) && typeof entry[0] === "string") return entry[0].toLowerCase().includes("piwi");
|
|
@@ -2115,28 +2626,29 @@ function injectReporter(reporter, piwiOptions) {
|
|
|
2115
2626
|
}
|
|
2116
2627
|
function resolveSetupModule() {
|
|
2117
2628
|
const candidates = [
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2629
|
+
path12.join(__dirname, "global-setup-module.js"),
|
|
2630
|
+
path12.join(__dirname, "..", "global-setup-module.js"),
|
|
2631
|
+
path12.join(__dirname, "..", "global-setup-module.ts")
|
|
2121
2632
|
];
|
|
2122
|
-
return candidates.find((candidate) =>
|
|
2633
|
+
return candidates.find((candidate) => fs13.existsSync(candidate)) ?? candidates[0];
|
|
2123
2634
|
}
|
|
2124
2635
|
function wrapConfig(config, piwiOptions) {
|
|
2125
2636
|
if (piwiOptions) applyOptionsToEnv(piwiOptions);
|
|
2126
|
-
const globalSetupModules = [];
|
|
2637
|
+
const globalSetupModules = [resolveSetupModule()];
|
|
2127
2638
|
if (config.globalSetup) {
|
|
2128
2639
|
const orig = Array.isArray(config.globalSetup) ? config.globalSetup : [config.globalSetup];
|
|
2129
2640
|
globalSetupModules.push(...orig);
|
|
2130
2641
|
}
|
|
2131
|
-
globalSetupModules.push(resolveSetupModule());
|
|
2132
2642
|
const forwarded = {};
|
|
2133
2643
|
const failOnFlaky = piwiOptions?.failOnFlakyTests ?? readBool(process.env[PIWI_ENV_KEYS.failOnFlakyTests]);
|
|
2134
2644
|
if (failOnFlaky === true) forwarded.failOnFlakyTests = true;
|
|
2645
|
+
const use = applyCaptureDefaults(config.use, piwiOptions);
|
|
2135
2646
|
return {
|
|
2136
2647
|
...config,
|
|
2137
2648
|
...forwarded,
|
|
2138
2649
|
reporter: injectReporter(config.reporter, piwiOptions),
|
|
2139
|
-
globalSetup: globalSetupModules.length === 1 ? globalSetupModules[0] : globalSetupModules
|
|
2650
|
+
globalSetup: globalSetupModules.length === 1 ? globalSetupModules[0] : globalSetupModules,
|
|
2651
|
+
...use === config.use ? {} : { use }
|
|
2140
2652
|
};
|
|
2141
2653
|
}
|
|
2142
2654
|
|
|
@@ -2204,6 +2716,8 @@ var PIWI_ANNOTATION_PREFIX = "piwi:";
|
|
|
2204
2716
|
var TEST_PRIORITIES = ["critical", "high", "medium", "low"];
|
|
2205
2717
|
var MAX_TEST_TAGS = 20;
|
|
2206
2718
|
var MAX_TEST_TAG_CHARS = 60;
|
|
2719
|
+
var MAX_TEST_LOCKS = 20;
|
|
2720
|
+
var MAX_TEST_LOCK_CHARS = 100;
|
|
2207
2721
|
var MAX_TEST_META_CHARS = 120;
|
|
2208
2722
|
var MAX_TEST_LINK_CHARS = 500;
|
|
2209
2723
|
var PRIORITY_SET = new Set(TEST_PRIORITIES);
|
|
@@ -2227,6 +2741,20 @@ function normalizeTestTags(raw) {
|
|
|
2227
2741
|
}
|
|
2228
2742
|
return out;
|
|
2229
2743
|
}
|
|
2744
|
+
function normalizeTestLocks(raw) {
|
|
2745
|
+
if (!Array.isArray(raw)) return [];
|
|
2746
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2747
|
+
const out = [];
|
|
2748
|
+
for (const entry of raw) {
|
|
2749
|
+
if (typeof entry !== "string") continue;
|
|
2750
|
+
const lock = entry.trim().slice(0, MAX_TEST_LOCK_CHARS);
|
|
2751
|
+
if (!lock || seen.has(lock)) continue;
|
|
2752
|
+
seen.add(lock);
|
|
2753
|
+
out.push(lock);
|
|
2754
|
+
if (out.length >= MAX_TEST_LOCKS) break;
|
|
2755
|
+
}
|
|
2756
|
+
return out;
|
|
2757
|
+
}
|
|
2230
2758
|
function normalizeLink(value) {
|
|
2231
2759
|
const raw = cleanString(value, MAX_TEST_LINK_CHARS);
|
|
2232
2760
|
if (!raw) return void 0;
|
|
@@ -2279,12 +2807,16 @@ function parseTestMetadata(annotations) {
|
|
|
2279
2807
|
function collectTestTags(test) {
|
|
2280
2808
|
return normalizeTestTags(test.tags);
|
|
2281
2809
|
}
|
|
2810
|
+
function collectTestLocks(test) {
|
|
2811
|
+
const locks = test?._locks;
|
|
2812
|
+
return normalizeTestLocks(locks);
|
|
2813
|
+
}
|
|
2282
2814
|
function collectTestMetadata(annotations) {
|
|
2283
2815
|
return parseTestMetadata(annotations);
|
|
2284
2816
|
}
|
|
2285
2817
|
|
|
2286
2818
|
// src/internal/collect/error-text.ts
|
|
2287
|
-
var
|
|
2819
|
+
var path13 = __toESM(require("path"));
|
|
2288
2820
|
|
|
2289
2821
|
// ../core/src/error-text.ts
|
|
2290
2822
|
function joinErrorMessages(errors) {
|
|
@@ -2313,13 +2845,14 @@ function buildErrorText(result) {
|
|
|
2313
2845
|
if (!text) return null;
|
|
2314
2846
|
const loc = result.error?.location;
|
|
2315
2847
|
if (!loc?.file) return text;
|
|
2316
|
-
const rel =
|
|
2848
|
+
const rel = path13.relative(process.cwd(), loc.file).split(path13.sep).join("/");
|
|
2317
2849
|
return appendErrorLocation(text, { file: rel, line: loc.line, column: loc.column });
|
|
2318
2850
|
}
|
|
2319
2851
|
|
|
2320
2852
|
// src/internal/support/ci-output.ts
|
|
2321
|
-
var
|
|
2322
|
-
var
|
|
2853
|
+
var fs14 = __toESM(require("fs"));
|
|
2854
|
+
var path14 = __toESM(require("path"));
|
|
2855
|
+
var SUMMARY_MAX_FAILURES = 20;
|
|
2323
2856
|
function emitRunOutputs(output, logger, outputFile, env = process.env) {
|
|
2324
2857
|
logger.info(`View run: ${output.runUrl}`);
|
|
2325
2858
|
if (outputFile) writeOutputFile(outputFile, output, logger);
|
|
@@ -2328,9 +2861,9 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
|
|
|
2328
2861
|
}
|
|
2329
2862
|
function writeOutputFile(file, output, logger) {
|
|
2330
2863
|
try {
|
|
2331
|
-
const dir =
|
|
2332
|
-
if (dir && dir !== ".")
|
|
2333
|
-
|
|
2864
|
+
const dir = path14.dirname(file);
|
|
2865
|
+
if (dir && dir !== ".") fs14.mkdirSync(dir, { recursive: true });
|
|
2866
|
+
fs14.writeFileSync(
|
|
2334
2867
|
file,
|
|
2335
2868
|
JSON.stringify(
|
|
2336
2869
|
{
|
|
@@ -2339,7 +2872,9 @@ function writeOutputFile(file, output, logger) {
|
|
|
2339
2872
|
projectId: output.projectId ?? null,
|
|
2340
2873
|
projectName: output.projectName,
|
|
2341
2874
|
status: output.status,
|
|
2342
|
-
ciBuildUrl: output.ciBuildUrl ?? null
|
|
2875
|
+
ciBuildUrl: output.ciBuildUrl ?? null,
|
|
2876
|
+
failedCount: output.failures.length,
|
|
2877
|
+
failures: output.failures
|
|
2343
2878
|
},
|
|
2344
2879
|
null,
|
|
2345
2880
|
2
|
|
@@ -2354,7 +2889,8 @@ function emitGitHubActions(output, env, logger) {
|
|
|
2354
2889
|
const pairs = [
|
|
2355
2890
|
["piwi_run_url", output.runUrl],
|
|
2356
2891
|
["piwi_run_id", String(output.runId)],
|
|
2357
|
-
["piwi_run_status", output.status]
|
|
2892
|
+
["piwi_run_status", output.status],
|
|
2893
|
+
["piwi_failed_count", String(output.failures.length)]
|
|
2358
2894
|
];
|
|
2359
2895
|
if (output.projectId != null) pairs.push(["piwi_project_id", String(output.projectId)]);
|
|
2360
2896
|
if (env.GITHUB_OUTPUT) {
|
|
@@ -2368,7 +2904,13 @@ function emitGitHubActions(output, env, logger) {
|
|
|
2368
2904
|
if (env.GITHUB_STEP_SUMMARY) {
|
|
2369
2905
|
appendFileLines(
|
|
2370
2906
|
env.GITHUB_STEP_SUMMARY,
|
|
2371
|
-
[
|
|
2907
|
+
[
|
|
2908
|
+
"### Piwi test run",
|
|
2909
|
+
"",
|
|
2910
|
+
`[View run](${output.runUrl}) \u2014 **${output.status}**`,
|
|
2911
|
+
"",
|
|
2912
|
+
...summaryFailureLines(output)
|
|
2913
|
+
],
|
|
2372
2914
|
logger,
|
|
2373
2915
|
"step summary"
|
|
2374
2916
|
);
|
|
@@ -2376,13 +2918,32 @@ function emitGitHubActions(output, env, logger) {
|
|
|
2376
2918
|
process.stdout.write(`::notice title=Piwi test run::${output.runUrl}
|
|
2377
2919
|
`);
|
|
2378
2920
|
}
|
|
2921
|
+
function summaryFailureLines(output) {
|
|
2922
|
+
if (output.failures.length === 0) return [];
|
|
2923
|
+
const lines = output.failures.slice(0, SUMMARY_MAX_FAILURES).map((f) => {
|
|
2924
|
+
const headline = f.headline ? ` \u2014 ${escapeMarkdown(f.headline)}` : "";
|
|
2925
|
+
return `- \u274C [${escapeMarkdown(f.title)}](${f.url})${headline} \u2014 \`${f.file}\``;
|
|
2926
|
+
});
|
|
2927
|
+
const hidden = output.failures.length - SUMMARY_MAX_FAILURES;
|
|
2928
|
+
if (hidden > 0) lines.push(`- +${hidden} more`);
|
|
2929
|
+
lines.push("");
|
|
2930
|
+
return lines;
|
|
2931
|
+
}
|
|
2932
|
+
function escapeMarkdown(text) {
|
|
2933
|
+
return text.replace(/[\\`*_[\]]/g, (ch) => `\\${ch}`);
|
|
2934
|
+
}
|
|
2379
2935
|
function emitGitLabDotenv(output, env, logger) {
|
|
2380
2936
|
const file = env.PIWI_DOTENV_FILE || "piwi.env";
|
|
2381
|
-
const lines = [
|
|
2937
|
+
const lines = [
|
|
2938
|
+
`PIWI_RUN_URL=${output.runUrl}`,
|
|
2939
|
+
`PIWI_RUN_ID=${output.runId}`,
|
|
2940
|
+
`PIWI_RUN_STATUS=${output.status}`,
|
|
2941
|
+
`PIWI_FAILED_COUNT=${output.failures.length}`
|
|
2942
|
+
];
|
|
2382
2943
|
if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
|
|
2383
2944
|
if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
|
|
2384
2945
|
try {
|
|
2385
|
-
|
|
2946
|
+
fs14.writeFileSync(file, lines.join("\n") + "\n");
|
|
2386
2947
|
logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
|
|
2387
2948
|
} catch (error) {
|
|
2388
2949
|
logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
|
|
@@ -2390,7 +2951,7 @@ function emitGitLabDotenv(output, env, logger) {
|
|
|
2390
2951
|
}
|
|
2391
2952
|
function appendFileLines(file, lines, logger, label) {
|
|
2392
2953
|
try {
|
|
2393
|
-
|
|
2954
|
+
fs14.appendFileSync(file, lines.join("\n") + "\n");
|
|
2394
2955
|
} catch (error) {
|
|
2395
2956
|
logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
|
|
2396
2957
|
}
|
|
@@ -2409,13 +2970,15 @@ var RunSubmitter = class {
|
|
|
2409
2970
|
* @param recovery Crash-recovery persistence.
|
|
2410
2971
|
* @param streamManager Streaming session (may be `null` when streaming is disabled).
|
|
2411
2972
|
* @param logger Prefixed logger.
|
|
2973
|
+
* @param failureLinks Failed tests collected during the run, for the per-failure links.
|
|
2412
2974
|
*/
|
|
2413
|
-
constructor(httpClient, uploader, recovery, streamManager, logger = new Logger()) {
|
|
2975
|
+
constructor(httpClient, uploader, recovery, streamManager, logger = new Logger(), failureLinks = null) {
|
|
2414
2976
|
this.httpClient = httpClient;
|
|
2415
2977
|
this.uploader = uploader;
|
|
2416
2978
|
this.recovery = recovery;
|
|
2417
2979
|
this.streamManager = streamManager;
|
|
2418
2980
|
this.logger = logger;
|
|
2981
|
+
this.failureLinks = failureLinks;
|
|
2419
2982
|
}
|
|
2420
2983
|
/** Run the fallback ladder for a completed test run. */
|
|
2421
2984
|
async submit(run, result) {
|
|
@@ -2443,10 +3006,12 @@ var RunSubmitter = class {
|
|
|
2443
3006
|
auth = sm?.auth ?? await this.httpClient.resolveAuth(run.options);
|
|
2444
3007
|
} catch (error) {
|
|
2445
3008
|
this.logger.error(`Authentication failed: ${errorMessage(error)}`);
|
|
3009
|
+
this.saveRecovery(this.buildRunPayload(run, overallStatus, duration));
|
|
2446
3010
|
throw error;
|
|
2447
3011
|
}
|
|
3012
|
+
if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
|
|
2448
3013
|
let outcome = { done: false, output: null };
|
|
2449
|
-
if (sm?.enabled && sm?.runId != null) {
|
|
3014
|
+
if (sm?.enabled && sm?.runId != null && !sm.bufferLostResults) {
|
|
2450
3015
|
outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
|
|
2451
3016
|
}
|
|
2452
3017
|
if (!outcome.done && (this.hasReports(run) || run.options.uploadTraces)) {
|
|
@@ -2455,7 +3020,10 @@ var RunSubmitter = class {
|
|
|
2455
3020
|
if (!outcome.done) {
|
|
2456
3021
|
outcome = await this.tryUploadJSON(run, overallStatus, duration, auth);
|
|
2457
3022
|
}
|
|
2458
|
-
if (outcome.output)
|
|
3023
|
+
if (outcome.output) {
|
|
3024
|
+
this.failureLinks?.printPending(outcome.output.runId);
|
|
3025
|
+
emitRunOutputs(outcome.output, this.logger, run.options.outputFile);
|
|
3026
|
+
}
|
|
2459
3027
|
}
|
|
2460
3028
|
/** Assemble a CI-facing run output, or `null` when the server returned no run id. */
|
|
2461
3029
|
buildOutput(runId, projectId, run, status) {
|
|
@@ -2466,7 +3034,8 @@ var RunSubmitter = class {
|
|
|
2466
3034
|
projectId,
|
|
2467
3035
|
projectName: run.options.projectName,
|
|
2468
3036
|
status,
|
|
2469
|
-
ciBuildUrl: ciBuildUrlFromMetadata(run.metadata)
|
|
3037
|
+
ciBuildUrl: ciBuildUrlFromMetadata(run.metadata),
|
|
3038
|
+
failures: this.failureLinks?.resolve(runId) ?? []
|
|
2470
3039
|
};
|
|
2471
3040
|
}
|
|
2472
3041
|
hasReports(run) {
|
|
@@ -2560,17 +3129,15 @@ var RunSubmitter = class {
|
|
|
2560
3129
|
}
|
|
2561
3130
|
}
|
|
2562
3131
|
async tryUploadWithFiles(run, overallStatus, duration, auth) {
|
|
3132
|
+
const payload = this.buildRunPayload(run, overallStatus, duration);
|
|
2563
3133
|
try {
|
|
2564
|
-
const response = await this.uploader.uploadWithFiles(
|
|
2565
|
-
this.buildRunPayload(run, overallStatus, duration),
|
|
2566
|
-
this.reportOptions(run),
|
|
2567
|
-
auth
|
|
2568
|
-
);
|
|
3134
|
+
const response = await this.uploader.uploadWithFiles(payload, this.reportOptions(run), auth);
|
|
2569
3135
|
this.recovery.clear();
|
|
2570
3136
|
return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
|
|
2571
3137
|
} catch (error) {
|
|
2572
3138
|
if (error instanceof HttpError && error.status === 401 && !auth) {
|
|
2573
3139
|
this.logAuthRequired(run.options.serverUrl);
|
|
3140
|
+
this.saveRecovery(payload);
|
|
2574
3141
|
throw error;
|
|
2575
3142
|
}
|
|
2576
3143
|
this.logger.warn(`Failed to upload with files: ${errorMessage(error)}`);
|
|
@@ -2587,16 +3154,24 @@ var RunSubmitter = class {
|
|
|
2587
3154
|
} catch (error) {
|
|
2588
3155
|
if (error instanceof HttpError && error.status === 401 && !auth) {
|
|
2589
3156
|
this.logAuthRequired(run.options.serverUrl);
|
|
3157
|
+
this.saveRecovery(payload);
|
|
2590
3158
|
throw error;
|
|
2591
3159
|
}
|
|
2592
3160
|
this.logger.error(`All upload methods failed: ${errorMessage(error)}`);
|
|
2593
3161
|
this.logger.info(
|
|
2594
3162
|
`Saved a local recovery copy \u2014 it will be uploaded automatically on your next test run. If this keeps happening, check that serverUrl (${run.options.serverUrl ?? "not set"}) is correct and reachable.`
|
|
2595
3163
|
);
|
|
2596
|
-
this.
|
|
3164
|
+
this.saveRecovery(payload);
|
|
2597
3165
|
return { done: true, output: null };
|
|
2598
3166
|
}
|
|
2599
3167
|
}
|
|
3168
|
+
/**
|
|
3169
|
+
* Persist the wire-serialized payload (no raw attachments / internal fields)
|
|
3170
|
+
* so a later run can retry the submit.
|
|
3171
|
+
*/
|
|
3172
|
+
saveRecovery(payload) {
|
|
3173
|
+
this.recovery.save(serializeRun(payload, { includeTestCases: true }));
|
|
3174
|
+
}
|
|
2600
3175
|
/** Log one actionable line explaining how to fix a 401 caused by a missing credential. */
|
|
2601
3176
|
logAuthRequired(serverUrl) {
|
|
2602
3177
|
this.logger.error(
|
|
@@ -2605,10 +3180,833 @@ var RunSubmitter = class {
|
|
|
2605
3180
|
}
|
|
2606
3181
|
};
|
|
2607
3182
|
|
|
3183
|
+
// ../core/src/locator-methods.ts
|
|
3184
|
+
var LOCATOR_BUILDER_METHODS = [
|
|
3185
|
+
"getByRole",
|
|
3186
|
+
"getByTestId",
|
|
3187
|
+
"getByText",
|
|
3188
|
+
"getByLabel",
|
|
3189
|
+
"getByPlaceholder",
|
|
3190
|
+
"getByAltText",
|
|
3191
|
+
"getByTitle",
|
|
3192
|
+
"locator"
|
|
3193
|
+
];
|
|
3194
|
+
|
|
3195
|
+
// ../core/src/error-parse.ts
|
|
3196
|
+
var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
|
|
3197
|
+
function stripAnsi(text) {
|
|
3198
|
+
return text.replace(ANSI_RE, "");
|
|
3199
|
+
}
|
|
3200
|
+
var SELECTOR_FN_RE = /\b(?:locator|frameLocator|getByRole|getByTestId|getByText|getByLabel|getByPlaceholder|getByAltText|getByTitle)\(/;
|
|
3201
|
+
var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
|
|
3202
|
+
...LOCATOR_BUILDER_METHODS,
|
|
3203
|
+
"frameLocator",
|
|
3204
|
+
"contentFrame",
|
|
3205
|
+
"filter",
|
|
3206
|
+
"first",
|
|
3207
|
+
"last",
|
|
3208
|
+
"nth",
|
|
3209
|
+
"and",
|
|
3210
|
+
"or",
|
|
3211
|
+
"describe"
|
|
3212
|
+
]);
|
|
3213
|
+
function extractMessageHead(text) {
|
|
3214
|
+
let head = text;
|
|
3215
|
+
const callLogIdx = head.indexOf("\nCall log:");
|
|
3216
|
+
if (callLogIdx !== -1) head = head.slice(0, callLogIdx);
|
|
3217
|
+
const stackIdx = head.search(/\n\s+at /);
|
|
3218
|
+
if (stackIdx !== -1) head = head.slice(0, stackIdx);
|
|
3219
|
+
const lines = head.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3220
|
+
return lines.slice(0, 5).join("\n");
|
|
3221
|
+
}
|
|
3222
|
+
function extractLeafSelector(text) {
|
|
3223
|
+
const first = SELECTOR_FN_RE.exec(text);
|
|
3224
|
+
if (!first) return null;
|
|
3225
|
+
const nl = text.indexOf("\n", first.index);
|
|
3226
|
+
const region = text.slice(first.index, nl === -1 ? void 0 : nl);
|
|
3227
|
+
let depth = 0;
|
|
3228
|
+
let leafStart = -1;
|
|
3229
|
+
for (let i = 0; i < region.length; i++) {
|
|
3230
|
+
const ch = region[i];
|
|
3231
|
+
if (ch === "(") {
|
|
3232
|
+
depth++;
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
if (ch === ")") {
|
|
3236
|
+
if (depth > 0) depth--;
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
if (depth !== 0) continue;
|
|
3240
|
+
const prev = region[i - 1];
|
|
3241
|
+
if (prev && /\w/.test(prev)) continue;
|
|
3242
|
+
for (const method of LOCATOR_BUILDER_METHODS) {
|
|
3243
|
+
if (region.startsWith(method, i) && region[i + method.length] === "(") {
|
|
3244
|
+
leafStart = i;
|
|
3245
|
+
break;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (leafStart === -1) return null;
|
|
3250
|
+
depth = 0;
|
|
3251
|
+
for (let i = leafStart; i < region.length; i++) {
|
|
3252
|
+
const ch = region[i];
|
|
3253
|
+
if (ch === "(") {
|
|
3254
|
+
depth++;
|
|
3255
|
+
} else if (ch === ")") {
|
|
3256
|
+
depth--;
|
|
3257
|
+
if (depth === 0) return region.slice(leafStart, i + 1);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
return region.slice(leafStart, leafStart + 80);
|
|
3261
|
+
}
|
|
3262
|
+
function extractLocatorChain(text) {
|
|
3263
|
+
const first = SELECTOR_FN_RE.exec(text);
|
|
3264
|
+
if (!first) return null;
|
|
3265
|
+
const nl = text.indexOf("\n", first.index);
|
|
3266
|
+
const region = text.slice(first.index, nl === -1 ? void 0 : nl);
|
|
3267
|
+
let depth = 0;
|
|
3268
|
+
let end = -1;
|
|
3269
|
+
for (let i = 0; i < region.length; i++) {
|
|
3270
|
+
const ch = region[i];
|
|
3271
|
+
if (ch === "(") {
|
|
3272
|
+
depth++;
|
|
3273
|
+
} else if (ch === ")") {
|
|
3274
|
+
if (depth > 0) depth--;
|
|
3275
|
+
if (depth === 0) {
|
|
3276
|
+
end = i + 1;
|
|
3277
|
+
const link = /^\.(\w+)\(/.exec(region.slice(end));
|
|
3278
|
+
if (!link || !CHAIN_LINK_METHODS.has(link[1])) break;
|
|
3279
|
+
i = end + link[0].length - 2;
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
if (end === -1) return region.slice(0, 80);
|
|
3284
|
+
return region.slice(0, end);
|
|
3285
|
+
}
|
|
3286
|
+
function extractTopFrame(text) {
|
|
3287
|
+
const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
|
|
3288
|
+
let m;
|
|
3289
|
+
while ((m = frameRe.exec(text)) !== null) {
|
|
3290
|
+
const file = m[1].replace(/\\/g, "/");
|
|
3291
|
+
if (file.includes("node_modules") || file.startsWith("node:")) continue;
|
|
3292
|
+
return { file, line: Number(m[2]), column: Number(m[3]) };
|
|
3293
|
+
}
|
|
3294
|
+
return null;
|
|
3295
|
+
}
|
|
3296
|
+
var SUBJECTS = "locator|page|frame|frameLocator|elementHandle|mouse|keyboard|touchscreen|browserContext|browser|apiRequestContext|request|response|route|download|dialog|fileChooser|expect|worker|jsHandle|clock|tracing|video";
|
|
3297
|
+
var CALL_RE = new RegExp(`\\b(${SUBJECTS})\\.(\\w+): `);
|
|
3298
|
+
var ERROR_NAME_RE = /^((?:[A-Z]\w*)?(?:Error|Exception))\b:?/;
|
|
3299
|
+
var MATCHER_RE = /\bexpect(?:\.soft)?\((?:[^()]|\([^()]*\))*\)(\.not)?\.(to\w+)/;
|
|
3300
|
+
var MATCHER_SHORT_RE = /\bexpect(?:\.soft)?\.(to\w+)\b/;
|
|
3301
|
+
var TIMED_OUT_EXPECT_RE = /Timed out (\d+)ms waiting for expect\(/;
|
|
3302
|
+
var NAVIGATION_RE = /\b(?:page|frame)\.(?:goto|waitForURL|waitForNavigation|reload|goBack|goForward)\b|net::ERR_|NS_ERROR_|Navigation failed|navigating to "/i;
|
|
3303
|
+
var NETWORK_CODE_RE = /\b(net::ERR_[A-Z0-9_]+|NS_ERROR_[A-Z0-9_]+)\b/;
|
|
3304
|
+
var CRASH_RE = /Target page, context or browser has been closed|Target closed|browser has been closed|Browser closed|Page crashed|Navigation failed because page was closed/i;
|
|
3305
|
+
var TEST_TIMEOUT_RE = /\bTest timeout of (\d+)ms exceeded(?: while (?:running "(\w+)" hook|tearing down "(\w+)"))?/;
|
|
3306
|
+
var STRICT_RE = /strict mode violation: (.+?) resolved to (\d+) elements/;
|
|
3307
|
+
var URL_RE = /https?:\/\/[^\s'"`)]+/;
|
|
3308
|
+
var CALL_LOG_LINE_RE = /^\s*-\s+(?:(\d+) × )?(.*)$/;
|
|
3309
|
+
var RETRY_COUNT_LINE_RE = /^\s+\d+ × (.+)$/;
|
|
3310
|
+
function withoutStackFrames(text) {
|
|
3311
|
+
return text.split("\n").filter((line) => !/^\s+at /.test(line)).join("\n");
|
|
3312
|
+
}
|
|
3313
|
+
function callLogLines(text) {
|
|
3314
|
+
const start = text.indexOf("Call log:");
|
|
3315
|
+
const region = start === -1 ? text : text.slice(start + "Call log:".length);
|
|
3316
|
+
const lines = [];
|
|
3317
|
+
for (const line of region.split("\n")) {
|
|
3318
|
+
const bulleted = CALL_LOG_LINE_RE.exec(line);
|
|
3319
|
+
if (bulleted) {
|
|
3320
|
+
lines.push(bulleted[2].trim());
|
|
3321
|
+
continue;
|
|
3322
|
+
}
|
|
3323
|
+
const retry = RETRY_COUNT_LINE_RE.exec(line);
|
|
3324
|
+
if (retry) lines.push(retry[1].trim());
|
|
3325
|
+
}
|
|
3326
|
+
return lines;
|
|
3327
|
+
}
|
|
3328
|
+
function readCallLog(text) {
|
|
3329
|
+
const lines = callLogLines(text);
|
|
3330
|
+
const read = {
|
|
3331
|
+
state: "unknown",
|
|
3332
|
+
count: null,
|
|
3333
|
+
lastLine: null,
|
|
3334
|
+
stateLine: null,
|
|
3335
|
+
url: null,
|
|
3336
|
+
timeoutMs: null,
|
|
3337
|
+
matcher: null
|
|
3338
|
+
};
|
|
3339
|
+
if (lines.length === 0) return read;
|
|
3340
|
+
read.lastLine = lines[lines.length - 1];
|
|
3341
|
+
let sawWaiting = false;
|
|
3342
|
+
let waitingLine = null;
|
|
3343
|
+
for (const line of lines) {
|
|
3344
|
+
const before = read.state;
|
|
3345
|
+
const lower = line.toLowerCase();
|
|
3346
|
+
const count = /^locator resolved to (\d+) elements?/.exec(line);
|
|
3347
|
+
const expectLine = /^Expect "(\w+)" with timeout (\d+)ms/.exec(line);
|
|
3348
|
+
const navTo = /navigat(?:ing|ion|ed) to "([^"]+)"/.exec(line);
|
|
3349
|
+
if (expectLine) {
|
|
3350
|
+
read.matcher = expectLine[1];
|
|
3351
|
+
read.timeoutMs = Number(expectLine[2]);
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
if (navTo) {
|
|
3355
|
+
read.url ??= navTo[1];
|
|
3356
|
+
read.state = "navigating";
|
|
3357
|
+
continue;
|
|
3358
|
+
}
|
|
3359
|
+
if (/^waiting for (?:navigation|page to navigate)/.test(line)) {
|
|
3360
|
+
read.state = "navigating";
|
|
3361
|
+
continue;
|
|
3362
|
+
}
|
|
3363
|
+
if (line.startsWith("waiting for ") && SELECTOR_FN_RE.test(line)) {
|
|
3364
|
+
sawWaiting = true;
|
|
3365
|
+
waitingLine ??= line;
|
|
3366
|
+
continue;
|
|
3367
|
+
}
|
|
3368
|
+
if (count) {
|
|
3369
|
+
read.state = "resolved-count";
|
|
3370
|
+
read.count = Number(count[1]);
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3373
|
+
if (line.startsWith("locator resolved to hidden <") || line.startsWith('unexpected value "hidden"')) {
|
|
3374
|
+
read.state = "hidden";
|
|
3375
|
+
continue;
|
|
3376
|
+
}
|
|
3377
|
+
if (/^locator resolved to (?:visible )?</.test(line)) {
|
|
3378
|
+
read.state = "resolved";
|
|
3379
|
+
continue;
|
|
3380
|
+
}
|
|
3381
|
+
if (lower.startsWith("element is not visible")) read.state = "not-visible";
|
|
3382
|
+
else if (lower.startsWith("element is not enabled")) read.state = "not-enabled";
|
|
3383
|
+
else if (lower.startsWith("element is not editable")) read.state = "not-editable";
|
|
3384
|
+
else if (lower.startsWith("element is not stable")) read.state = "not-stable";
|
|
3385
|
+
else if (lower.startsWith("element is outside of the viewport")) read.state = "outside-viewport";
|
|
3386
|
+
else if (lower.startsWith("element is not attached") || lower.includes("element was detached"))
|
|
3387
|
+
read.state = "detached";
|
|
3388
|
+
else if (lower.includes("intercepts pointer events")) read.state = "intercepts-pointer";
|
|
3389
|
+
else if (lower.startsWith("element is visible, enabled and stable")) read.state = "resolved";
|
|
3390
|
+
else if (/^(?:attempting|performing) \w+ action/.test(lower) && (read.state === "unknown" || read.state === "not-found"))
|
|
3391
|
+
read.state = "resolved";
|
|
3392
|
+
if (read.state !== before || /^(?:locator resolved to|unexpected value|element is)/.test(lower)) {
|
|
3393
|
+
read.stateLine = line;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
if (read.state === "unknown" && sawWaiting) {
|
|
3397
|
+
read.state = "not-found";
|
|
3398
|
+
read.stateLine = waitingLine;
|
|
3399
|
+
}
|
|
3400
|
+
return read;
|
|
3401
|
+
}
|
|
3402
|
+
function headerValue(text, label) {
|
|
3403
|
+
const re = new RegExp(`^\\s*${label}(?: string| pattern| substring| value)?:[ \\t]*(.*)$`, "m");
|
|
3404
|
+
const m = re.exec(text);
|
|
3405
|
+
if (!m) return null;
|
|
3406
|
+
const value = m[1].trim();
|
|
3407
|
+
return value.length > 0 ? value : null;
|
|
3408
|
+
}
|
|
3409
|
+
function readLocator(text, strict) {
|
|
3410
|
+
const header = /^\s*Locator:[ \t]*(.+)$/m.exec(text);
|
|
3411
|
+
if (header) return header[1].trim();
|
|
3412
|
+
if (strict) return strict[1].trim();
|
|
3413
|
+
return extractLocatorChain(text);
|
|
3414
|
+
}
|
|
3415
|
+
function readTimeout(text, callLog) {
|
|
3416
|
+
const patterns = [
|
|
3417
|
+
/^\s*Timeout:[ \t]*(\d+)ms/m,
|
|
3418
|
+
/\bTimeout (\d+)ms exceeded/,
|
|
3419
|
+
TEST_TIMEOUT_RE,
|
|
3420
|
+
TIMED_OUT_EXPECT_RE,
|
|
3421
|
+
/\bTimed out (\d+)ms/
|
|
3422
|
+
];
|
|
3423
|
+
for (const re of patterns) {
|
|
3424
|
+
const m = re.exec(text);
|
|
3425
|
+
if (m) return Number(m[1]);
|
|
3426
|
+
}
|
|
3427
|
+
return callLog.timeoutMs;
|
|
3428
|
+
}
|
|
3429
|
+
function readUrl(text, callLog, received) {
|
|
3430
|
+
if (callLog.url) return callLog.url;
|
|
3431
|
+
const at = /\b(?:net::ERR_[A-Z0-9_]+|NS_ERROR_[A-Z0-9_]+) at (\S+)/.exec(text);
|
|
3432
|
+
if (at) return at[1];
|
|
3433
|
+
const head = extractMessageHead(text);
|
|
3434
|
+
const inHead = URL_RE.exec(head.replace(/^\s*Received.*$/gm, ""));
|
|
3435
|
+
if (inHead) return inHead[0];
|
|
3436
|
+
if (received) {
|
|
3437
|
+
const inReceived = URL_RE.exec(received);
|
|
3438
|
+
if (inReceived) return inReceived[0];
|
|
3439
|
+
}
|
|
3440
|
+
return null;
|
|
3441
|
+
}
|
|
3442
|
+
function parsePlaywrightError(raw, context) {
|
|
3443
|
+
const clean = stripAnsi(raw ?? "").replace(/\r\n?/g, "\n");
|
|
3444
|
+
const text = withoutStackFrames(clean);
|
|
3445
|
+
const messageHead = extractMessageHead(clean);
|
|
3446
|
+
const firstLine = messageHead.split("\n")[0] ?? "";
|
|
3447
|
+
const errorName = ERROR_NAME_RE.exec(firstLine)?.[1] ?? null;
|
|
3448
|
+
const strict = STRICT_RE.exec(text);
|
|
3449
|
+
const call = CALL_RE.exec(text);
|
|
3450
|
+
const callLog = readCallLog(text);
|
|
3451
|
+
const testTimeout = TEST_TIMEOUT_RE.exec(text);
|
|
3452
|
+
let subject2 = null;
|
|
3453
|
+
let action = null;
|
|
3454
|
+
let assertion = null;
|
|
3455
|
+
let negated = false;
|
|
3456
|
+
if (call) {
|
|
3457
|
+
subject2 = call[1];
|
|
3458
|
+
if (subject2 === "expect") assertion = call[2];
|
|
3459
|
+
else action = call[2];
|
|
3460
|
+
}
|
|
3461
|
+
const matcher = MATCHER_RE.exec(text);
|
|
3462
|
+
const matcherShort = MATCHER_SHORT_RE.exec(text);
|
|
3463
|
+
if (matcher) {
|
|
3464
|
+
assertion = matcher[2];
|
|
3465
|
+
negated = Boolean(matcher[1]);
|
|
3466
|
+
} else if (!assertion && matcherShort) {
|
|
3467
|
+
assertion = matcherShort[1];
|
|
3468
|
+
} else if (!assertion && callLog.matcher) {
|
|
3469
|
+
assertion = callLog.matcher;
|
|
3470
|
+
}
|
|
3471
|
+
if (/\bexpect\((?:[^()]|\([^()]*\))*\)\.not\./.test(text)) negated = true;
|
|
3472
|
+
const isAssertion = assertion !== null || /\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)/.test(text);
|
|
3473
|
+
const isNavigationFailure = NAVIGATION_RE.test(text);
|
|
3474
|
+
const networkErrorCode = NETWORK_CODE_RE.exec(text)?.[1] ?? null;
|
|
3475
|
+
let kind;
|
|
3476
|
+
if (strict) kind = "strict-mode";
|
|
3477
|
+
else if (testTimeout) kind = "test-timeout";
|
|
3478
|
+
else if (CRASH_RE.test(text)) kind = "crash";
|
|
3479
|
+
else if (isAssertion) {
|
|
3480
|
+
const retrying = /^\s*Timeout:[ \t]*\d+ms/m.test(text) || TIMED_OUT_EXPECT_RE.test(text) || callLog.matcher !== null;
|
|
3481
|
+
kind = retrying ? "assertion-timeout" : "assertion";
|
|
3482
|
+
} else if (isNavigationFailure) kind = "navigation";
|
|
3483
|
+
else if (/\bTimeout \d+ms exceeded/.test(text) || errorName === "TimeoutError") kind = "action-timeout";
|
|
3484
|
+
else kind = "unknown";
|
|
3485
|
+
const paramsLocator = typeof context?.stepParams?.locator === "string" ? context.stepParams.locator : null;
|
|
3486
|
+
const paramsUrl = typeof context?.stepParams?.url === "string" ? context.stepParams.url : null;
|
|
3487
|
+
const locator = readLocator(text, strict) ?? paramsLocator;
|
|
3488
|
+
const leafLocator = locator ? extractLeafSelector(locator) : null;
|
|
3489
|
+
const expected = headerValue(text, "Expected");
|
|
3490
|
+
const received = headerValue(text, "Received");
|
|
3491
|
+
const timeoutMs = readTimeout(text, callLog);
|
|
3492
|
+
const url = readUrl(text, callLog, received) ?? paramsUrl;
|
|
3493
|
+
const frame = extractTopFrame(clean);
|
|
3494
|
+
const resolvedCount = strict ? Number(strict[2]) : callLog.count;
|
|
3495
|
+
const lastState = strict ? "resolved-count" : callLog.state;
|
|
3496
|
+
const isLocatorResolutionFailure = kind === "strict-mode" || locator !== null && (lastState === "not-found" || lastState === "resolved-count" && resolvedCount === 0);
|
|
3497
|
+
return {
|
|
3498
|
+
kind,
|
|
3499
|
+
errorName,
|
|
3500
|
+
subject: subject2,
|
|
3501
|
+
action,
|
|
3502
|
+
assertion,
|
|
3503
|
+
negated,
|
|
3504
|
+
locator,
|
|
3505
|
+
leafLocator,
|
|
3506
|
+
expected,
|
|
3507
|
+
received,
|
|
3508
|
+
timeoutMs,
|
|
3509
|
+
url,
|
|
3510
|
+
networkErrorCode,
|
|
3511
|
+
timeoutPhase: testTimeout ? testTimeout[2] ?? testTimeout[3] ?? null : null,
|
|
3512
|
+
lastState,
|
|
3513
|
+
resolvedCount,
|
|
3514
|
+
lastCallLogLine: callLog.lastLine,
|
|
3515
|
+
lastStateLine: callLog.stateLine,
|
|
3516
|
+
messageHead,
|
|
3517
|
+
topFrame: frame ? `${frame.file}:${frame.line}` : null,
|
|
3518
|
+
isNavigationFailure,
|
|
3519
|
+
isLocatorResolutionFailure
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
// ../core/src/describe-failure.ts
|
|
3524
|
+
var HEADLINE_MAX_CHARS = 120;
|
|
3525
|
+
var VALUE_MAX_CHARS = 40;
|
|
3526
|
+
var SHORT_VALUE_MAX_CHARS = 20;
|
|
3527
|
+
var MASK_TOKEN_RE = /<(?:N|VALUE|URL|STR|UUID|HASH|EMAIL)>/g;
|
|
3528
|
+
var ACTION_VERBS = {
|
|
3529
|
+
click: "click",
|
|
3530
|
+
dblclick: "double-click",
|
|
3531
|
+
fill: "fill",
|
|
3532
|
+
type: "type",
|
|
3533
|
+
press: "press",
|
|
3534
|
+
pressSequentially: "type",
|
|
3535
|
+
check: "check",
|
|
3536
|
+
uncheck: "uncheck",
|
|
3537
|
+
hover: "hover",
|
|
3538
|
+
tap: "tap",
|
|
3539
|
+
focus: "focus",
|
|
3540
|
+
blur: "blur",
|
|
3541
|
+
clear: "clear",
|
|
3542
|
+
selectOption: "select",
|
|
3543
|
+
selectText: "select text",
|
|
3544
|
+
setInputFiles: "file upload",
|
|
3545
|
+
setChecked: "check",
|
|
3546
|
+
dragTo: "drag",
|
|
3547
|
+
dragAndDrop: "drag",
|
|
3548
|
+
scrollIntoViewIfNeeded: "scroll",
|
|
3549
|
+
screenshot: "screenshot",
|
|
3550
|
+
waitFor: "wait",
|
|
3551
|
+
waitForSelector: "waitForSelector",
|
|
3552
|
+
waitForLoadState: "waitForLoadState",
|
|
3553
|
+
waitForFunction: "waitForFunction",
|
|
3554
|
+
waitForResponse: "waitForResponse",
|
|
3555
|
+
waitForRequest: "waitForRequest",
|
|
3556
|
+
waitForEvent: "waitForEvent",
|
|
3557
|
+
waitForTimeout: "waitForTimeout",
|
|
3558
|
+
innerText: "read text",
|
|
3559
|
+
textContent: "read text",
|
|
3560
|
+
inputValue: "read value",
|
|
3561
|
+
getAttribute: "read attribute",
|
|
3562
|
+
isVisible: "visibility check",
|
|
3563
|
+
isEnabled: "enabled check",
|
|
3564
|
+
isChecked: "checked check",
|
|
3565
|
+
boundingBox: "measure",
|
|
3566
|
+
evaluate: "evaluate",
|
|
3567
|
+
goto: "navigation"
|
|
3568
|
+
};
|
|
3569
|
+
var ACTION_GERUNDS = {
|
|
3570
|
+
click: "clicking",
|
|
3571
|
+
dblclick: "double-clicking",
|
|
3572
|
+
fill: "filling",
|
|
3573
|
+
type: "typing into",
|
|
3574
|
+
press: "pressing a key on",
|
|
3575
|
+
pressSequentially: "typing into",
|
|
3576
|
+
check: "checking",
|
|
3577
|
+
uncheck: "unchecking",
|
|
3578
|
+
hover: "hovering",
|
|
3579
|
+
tap: "tapping",
|
|
3580
|
+
focus: "focusing",
|
|
3581
|
+
clear: "clearing",
|
|
3582
|
+
selectOption: "selecting an option in",
|
|
3583
|
+
setInputFiles: "uploading a file to",
|
|
3584
|
+
dragTo: "dragging",
|
|
3585
|
+
waitFor: "waiting for",
|
|
3586
|
+
waitForSelector: "waiting for",
|
|
3587
|
+
evaluate: "evaluating on"
|
|
3588
|
+
};
|
|
3589
|
+
var STATE_PHRASES = {
|
|
3590
|
+
"not-found": "was not found on the page",
|
|
3591
|
+
hidden: "never became visible",
|
|
3592
|
+
"not-visible": "never became visible",
|
|
3593
|
+
"not-enabled": "never became enabled",
|
|
3594
|
+
"not-editable": "never became editable",
|
|
3595
|
+
"not-stable": "never stopped moving",
|
|
3596
|
+
"outside-viewport": "stayed outside the viewport",
|
|
3597
|
+
"intercepts-pointer": "was covered by another element",
|
|
3598
|
+
detached: "was detached from the DOM",
|
|
3599
|
+
navigating: "was still navigating"
|
|
3600
|
+
};
|
|
3601
|
+
var STATE_MATCHERS = {
|
|
3602
|
+
toBeVisible: { met: "visible", unmet: "never became visible" },
|
|
3603
|
+
toBeHidden: { met: "hidden", unmet: "never became hidden" },
|
|
3604
|
+
toBeEnabled: { met: "enabled", unmet: "never became enabled" },
|
|
3605
|
+
toBeDisabled: { met: "disabled", unmet: "never became disabled" },
|
|
3606
|
+
toBeChecked: { met: "checked", unmet: "never became checked" },
|
|
3607
|
+
toBeEditable: { met: "editable", unmet: "never became editable" },
|
|
3608
|
+
toBeFocused: { met: "focused", unmet: "never received focus" },
|
|
3609
|
+
toBeAttached: { met: "attached", unmet: "never appeared in the DOM" },
|
|
3610
|
+
toBeDetached: { met: "detached", unmet: "never left the DOM" },
|
|
3611
|
+
toBeInViewport: { met: "in the viewport", unmet: "never entered the viewport" },
|
|
3612
|
+
toBeEmpty: { met: "empty", unmet: "never became empty" }
|
|
3613
|
+
};
|
|
3614
|
+
var VALUE_MATCHERS = {
|
|
3615
|
+
toHaveText: "text",
|
|
3616
|
+
toContainText: "text containing",
|
|
3617
|
+
toHaveValue: "value",
|
|
3618
|
+
toHaveValues: "values",
|
|
3619
|
+
toHaveAttribute: "attribute",
|
|
3620
|
+
toHaveClass: "class",
|
|
3621
|
+
toContainClass: "class",
|
|
3622
|
+
toHaveId: "id",
|
|
3623
|
+
toHaveCSS: "CSS",
|
|
3624
|
+
toHaveJSProperty: "property",
|
|
3625
|
+
toHaveAccessibleName: "accessible name",
|
|
3626
|
+
toHaveAccessibleDescription: "accessible description",
|
|
3627
|
+
toHaveRole: "role",
|
|
3628
|
+
toHaveURL: "URL",
|
|
3629
|
+
toHaveTitle: "title",
|
|
3630
|
+
toHaveScreenshot: "screenshot",
|
|
3631
|
+
toMatchAriaSnapshot: "ARIA snapshot"
|
|
3632
|
+
};
|
|
3633
|
+
var NETWORK_ERRORS = {
|
|
3634
|
+
ERR_CONNECTION_REFUSED: "Connection refused loading",
|
|
3635
|
+
ERR_CONNECTION_RESET: "Connection reset loading",
|
|
3636
|
+
ERR_CONNECTION_CLOSED: "Connection closed loading",
|
|
3637
|
+
ERR_CONNECTION_TIMED_OUT: "Connection timed out loading",
|
|
3638
|
+
ERR_TIMED_OUT: "Connection timed out loading",
|
|
3639
|
+
ERR_NAME_NOT_RESOLVED: "DNS lookup failed for",
|
|
3640
|
+
ERR_INTERNET_DISCONNECTED: "No network while loading",
|
|
3641
|
+
ERR_ADDRESS_UNREACHABLE: "Address unreachable loading",
|
|
3642
|
+
ERR_ABORTED: "Navigation aborted loading",
|
|
3643
|
+
ERR_EMPTY_RESPONSE: "Empty response loading",
|
|
3644
|
+
ERR_TOO_MANY_REDIRECTS: "Too many redirects loading",
|
|
3645
|
+
ERR_SSL_PROTOCOL_ERROR: "TLS error loading",
|
|
3646
|
+
ERR_CERT_AUTHORITY_INVALID: "Untrusted certificate loading",
|
|
3647
|
+
ERR_CERT_COMMON_NAME_INVALID: "Certificate name mismatch loading",
|
|
3648
|
+
ERR_CERT_DATE_INVALID: "Expired certificate loading",
|
|
3649
|
+
ERR_BLOCKED_BY_CLIENT: "Request blocked loading",
|
|
3650
|
+
ERR_FAILED: "Request failed loading",
|
|
3651
|
+
ERR_HTTP_RESPONSE_CODE_FAILURE: "HTTP error loading",
|
|
3652
|
+
NS_ERROR_CONNECTION_REFUSED: "Connection refused loading",
|
|
3653
|
+
NS_ERROR_UNKNOWN_HOST: "DNS lookup failed for",
|
|
3654
|
+
NS_ERROR_NET_TIMEOUT: "Connection timed out loading",
|
|
3655
|
+
NS_ERROR_OFFLINE: "No network while loading",
|
|
3656
|
+
NS_ERROR_ABORT: "Navigation aborted loading",
|
|
3657
|
+
NS_BINDING_ABORTED: "Navigation aborted loading"
|
|
3658
|
+
};
|
|
3659
|
+
function formatTimeout(ms) {
|
|
3660
|
+
if (ms < 1e3) return `${ms} ms`;
|
|
3661
|
+
const seconds = ms / 1e3;
|
|
3662
|
+
const rounded = Math.round(seconds * 10) / 10;
|
|
3663
|
+
return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded} s`;
|
|
3664
|
+
}
|
|
3665
|
+
function routeOf(url) {
|
|
3666
|
+
try {
|
|
3667
|
+
const parsed = new URL(url);
|
|
3668
|
+
return `${parsed.pathname}${parsed.search}` || "/";
|
|
3669
|
+
} catch {
|
|
3670
|
+
return url;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
function truncateValue(value, max) {
|
|
3674
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
3675
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
3676
|
+
}
|
|
3677
|
+
function countNoun(locator, count) {
|
|
3678
|
+
const role = locator ? /getByRole\(\s*['"]([a-z]+)['"]/.exec(locator)?.[1] : null;
|
|
3679
|
+
const noun = role ?? "element";
|
|
3680
|
+
if (count === 1) return noun;
|
|
3681
|
+
return noun.endsWith("s") ? noun : noun.endsWith("x") || noun.endsWith("ch") ? `${noun}es` : `${noun}s`;
|
|
3682
|
+
}
|
|
3683
|
+
function textOfGetByText(locator) {
|
|
3684
|
+
if (!locator) return null;
|
|
3685
|
+
const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1).)*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
|
|
3686
|
+
return m ? m[2] : null;
|
|
3687
|
+
}
|
|
3688
|
+
function receivedDisplay(parsed) {
|
|
3689
|
+
const received = parsed.received ?? "";
|
|
3690
|
+
if (parsed.assertion !== "toHaveURL") return received;
|
|
3691
|
+
const m = /^"?(https?:\/\/[^"\s]+)"?$/.exec(received);
|
|
3692
|
+
return m ? `"${routeOf(m[1])}"` : received;
|
|
3693
|
+
}
|
|
3694
|
+
function firstLineFallback(parsed) {
|
|
3695
|
+
const line = (parsed.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
|
|
3696
|
+
return truncateValue(line || "Unknown error", HEADLINE_MAX_CHARS);
|
|
3697
|
+
}
|
|
3698
|
+
var Line = class {
|
|
3699
|
+
constructor() {
|
|
3700
|
+
this.parts = [];
|
|
3701
|
+
}
|
|
3702
|
+
text(text) {
|
|
3703
|
+
if (!text) return this;
|
|
3704
|
+
const last = this.parts[this.parts.length - 1];
|
|
3705
|
+
if (last && last.kind === "text") last.text += text;
|
|
3706
|
+
else this.parts.push({ kind: "text", text });
|
|
3707
|
+
return this;
|
|
3708
|
+
}
|
|
3709
|
+
locator(text) {
|
|
3710
|
+
this.parts.push({ kind: "locator", text });
|
|
3711
|
+
return this;
|
|
3712
|
+
}
|
|
3713
|
+
value(text) {
|
|
3714
|
+
this.parts.push({ kind: "value", text });
|
|
3715
|
+
return this;
|
|
3716
|
+
}
|
|
3717
|
+
toString() {
|
|
3718
|
+
return this.parts.map((p) => p.text).join("");
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
function subject(line, locator, opts) {
|
|
3722
|
+
const text = textOfGetByText(locator);
|
|
3723
|
+
if (text !== null) return line.text("Text ").value(`"${truncateValue(text, opts.valueMax)}"`);
|
|
3724
|
+
return line.locator(opts.locator ?? locator ?? "");
|
|
3725
|
+
}
|
|
3726
|
+
function timeoutSuffix(timeoutMs) {
|
|
3727
|
+
return timeoutMs !== null ? ` after ${formatTimeout(timeoutMs)}` : "";
|
|
3728
|
+
}
|
|
3729
|
+
function timeoutParen(timeoutMs) {
|
|
3730
|
+
return timeoutMs !== null ? ` (${formatTimeout(timeoutMs)})` : "";
|
|
3731
|
+
}
|
|
3732
|
+
function buildActionTimeout(parsed, opts) {
|
|
3733
|
+
const line = new Line();
|
|
3734
|
+
const verb = ACTION_VERBS[parsed.action ?? ""] ?? parsed.action ?? "action";
|
|
3735
|
+
const state = STATE_PHRASES[parsed.lastState];
|
|
3736
|
+
if (parsed.locator && state) {
|
|
3737
|
+
return subject(line, parsed.locator, opts).text(` ${state} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed.timeoutMs));
|
|
3738
|
+
}
|
|
3739
|
+
if (parsed.locator && parsed.lastState === "resolved-count" && parsed.resolvedCount !== null) {
|
|
3740
|
+
const count = parsed.resolvedCount;
|
|
3741
|
+
return subject(line, parsed.locator, opts).text(` matched ${count === 0 ? "no" : count} ${countNoun(parsed.locator, count)} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed.timeoutMs));
|
|
3742
|
+
}
|
|
3743
|
+
if (parsed.locator) {
|
|
3744
|
+
return line.text(`${verb} on `).locator(opts.locator ?? parsed.locator).text(" timed out").text(timeoutSuffix(parsed.timeoutMs));
|
|
3745
|
+
}
|
|
3746
|
+
const call = parsed.subject && parsed.action ? `${parsed.subject}.${parsed.action}` : verb;
|
|
3747
|
+
return line.text(`${call} timed out`).text(timeoutSuffix(parsed.timeoutMs));
|
|
3748
|
+
}
|
|
3749
|
+
function buildAssertion(parsed, opts) {
|
|
3750
|
+
const line = new Line();
|
|
3751
|
+
const matcher = parsed.assertion ?? "expect";
|
|
3752
|
+
const notFound = parsed.lastState === "not-found" || parsed.lastState === "resolved-count" && parsed.resolvedCount === 0 || /element\(s\) not found/.test(parsed.received ?? "");
|
|
3753
|
+
const expected = parsed.expected ? truncateValue(parsed.expected, opts.valueMax) : null;
|
|
3754
|
+
const received = parsed.received ? truncateValue(receivedDisplay(parsed), opts.valueMax) : null;
|
|
3755
|
+
if (matcher === "toHaveCount" && parsed.locator) {
|
|
3756
|
+
const want = Number(parsed.expected);
|
|
3757
|
+
const got = notFound ? 0 : Number(parsed.received);
|
|
3758
|
+
const wantText = Number.isFinite(want) ? String(want) : expected ?? "?";
|
|
3759
|
+
const noun2 = countNoun(parsed.locator, Number.isFinite(want) ? want : 2);
|
|
3760
|
+
const gotText = Number.isFinite(got) ? got === 0 ? "none" : String(got) : received ?? "none";
|
|
3761
|
+
return line.text(`Expected ${wantText} ${noun2}, found ${gotText} \u2014 `).locator(opts.locator ?? parsed.locator).text(" toHaveCount");
|
|
3762
|
+
}
|
|
3763
|
+
const state = STATE_MATCHERS[matcher];
|
|
3764
|
+
if (state) {
|
|
3765
|
+
const unmet = parsed.negated ? `stayed ${state.met}` : state.unmet;
|
|
3766
|
+
if (!parsed.locator) return line.text(`Expected ${state.met}, page ${unmet}`).text(timeoutParen(parsed.timeoutMs));
|
|
3767
|
+
if (notFound && matcher !== "toBeVisible" && matcher !== "toBeAttached") {
|
|
3768
|
+
return subject(line, parsed.locator, opts).text(` was not found on the page \u2014 expected ${state.met}`).text(timeoutParen(parsed.timeoutMs));
|
|
3769
|
+
}
|
|
3770
|
+
return subject(line, parsed.locator, opts).text(` ${unmet}`).text(timeoutParen(parsed.timeoutMs));
|
|
3771
|
+
}
|
|
3772
|
+
const noun = VALUE_MATCHERS[matcher];
|
|
3773
|
+
if (noun) {
|
|
3774
|
+
if (parsed.locator && notFound) {
|
|
3775
|
+
return subject(line, parsed.locator, opts).text(" was not found on the page \u2014 expected ").text(`${noun} `).value(expected ?? "").text(timeoutParen(parsed.timeoutMs));
|
|
3776
|
+
}
|
|
3777
|
+
line.text(`Expected ${noun} `);
|
|
3778
|
+
if (expected) line.value(expected);
|
|
3779
|
+
else line.text("to match");
|
|
3780
|
+
if (received) line.text(", got ").value(received);
|
|
3781
|
+
if (parsed.locator)
|
|
3782
|
+
line.text(" \u2014 ").locator(opts.locator ?? parsed.locator).text(` ${matcher}`);
|
|
3783
|
+
else if (matcher === "toHaveURL" || matcher === "toHaveTitle") line.text(` \u2014 page ${matcher}`);
|
|
3784
|
+
return line;
|
|
3785
|
+
}
|
|
3786
|
+
if (matcher === "toPass") return line.text("expect.toPass never passed").text(timeoutParen(parsed.timeoutMs));
|
|
3787
|
+
if (expected || received) {
|
|
3788
|
+
line.text("Expected ");
|
|
3789
|
+
if (expected) line.value(expected);
|
|
3790
|
+
else line.text("a different value");
|
|
3791
|
+
if (received) line.text(", got ").value(received);
|
|
3792
|
+
line.text(` \u2014 ${matcher}`);
|
|
3793
|
+
if (parsed.locator) line.text(" on ").locator(opts.locator ?? parsed.locator);
|
|
3794
|
+
return line;
|
|
3795
|
+
}
|
|
3796
|
+
if (parsed.locator) {
|
|
3797
|
+
return subject(line, parsed.locator, opts).text(` failed ${matcher}`).text(timeoutParen(parsed.timeoutMs));
|
|
3798
|
+
}
|
|
3799
|
+
return line.text(`${matcher} assertion failed`);
|
|
3800
|
+
}
|
|
3801
|
+
function buildStrictMode(parsed, opts) {
|
|
3802
|
+
const line = new Line();
|
|
3803
|
+
const count = parsed.resolvedCount ?? 0;
|
|
3804
|
+
if (!parsed.locator) return line.text(`Locator matched ${count} elements \u2014 strict mode`);
|
|
3805
|
+
return line.locator(opts.locator ?? parsed.locator).text(` matched ${count} ${countNoun(parsed.locator, count)} \u2014 strict mode`);
|
|
3806
|
+
}
|
|
3807
|
+
function buildNavigation(parsed, opts) {
|
|
3808
|
+
const line = new Line();
|
|
3809
|
+
const code = parsed.networkErrorCode?.replace(/^net::/, "") ?? null;
|
|
3810
|
+
if (code) {
|
|
3811
|
+
const phrase = NETWORK_ERRORS[code] ?? `${code.replace(/^(?:ERR|NS_ERROR)_/, "").replace(/_/g, " ").toLowerCase().replace(/^\w/, (c) => c.toUpperCase())} loading`;
|
|
3812
|
+
line.text(phrase);
|
|
3813
|
+
if (parsed.url) line.text(" ").value(truncateValue(parsed.url, opts.valueMax * 2));
|
|
3814
|
+
return line;
|
|
3815
|
+
}
|
|
3816
|
+
const route = parsed.url ? routeOf(parsed.url) : null;
|
|
3817
|
+
const timedOut = parsed.timeoutMs !== null || parsed.errorName === "TimeoutError";
|
|
3818
|
+
if (parsed.action === "waitForURL" || parsed.action === "waitForNavigation") {
|
|
3819
|
+
line.text("Never navigated to ");
|
|
3820
|
+
if (route) line.value(truncateValue(route, opts.valueMax));
|
|
3821
|
+
else line.text("the expected URL");
|
|
3822
|
+
return line.text(` \u2014 ${parsed.action} timed out`).text(timeoutSuffix(parsed.timeoutMs));
|
|
3823
|
+
}
|
|
3824
|
+
line.text("Navigation");
|
|
3825
|
+
if (route) line.text(" to ").value(truncateValue(route, opts.valueMax));
|
|
3826
|
+
if (timedOut) return line.text(" timed out").text(timeoutSuffix(parsed.timeoutMs));
|
|
3827
|
+
return line.text(" failed");
|
|
3828
|
+
}
|
|
3829
|
+
function buildTestTimeout(parsed, opts, ctx) {
|
|
3830
|
+
const line = new Line().text("Test timed out").text(timeoutSuffix(parsed.timeoutMs));
|
|
3831
|
+
if (parsed.timeoutPhase) {
|
|
3832
|
+
const phase = parsed.timeoutPhase;
|
|
3833
|
+
const isHook = /^(?:before|after)(?:Each|All)$/.test(phase);
|
|
3834
|
+
return line.text(isHook ? ` in the "${phase}" hook` : ` while tearing down "${phase}"`);
|
|
3835
|
+
}
|
|
3836
|
+
if (parsed.isNavigationFailure && parsed.url) {
|
|
3837
|
+
return line.text(" while navigating to ").value(truncateValue(routeOf(parsed.url), opts.valueMax));
|
|
3838
|
+
}
|
|
3839
|
+
if (parsed.assertion && parsed.locator) {
|
|
3840
|
+
const state = STATE_MATCHERS[parsed.assertion];
|
|
3841
|
+
line.text(state ? ` while waiting for ` : " while expecting ");
|
|
3842
|
+
subject(line, parsed.locator, opts);
|
|
3843
|
+
return line.text(state ? ` to be ${state.met}` : ` ${parsed.assertion}`);
|
|
3844
|
+
}
|
|
3845
|
+
if (parsed.action && parsed.locator) {
|
|
3846
|
+
const gerund = ACTION_GERUNDS[parsed.action] ?? `${ACTION_VERBS[parsed.action] ?? parsed.action} on`;
|
|
3847
|
+
line.text(` while ${gerund} `);
|
|
3848
|
+
return subject(line, parsed.locator, opts);
|
|
3849
|
+
}
|
|
3850
|
+
if (parsed.subject && parsed.action) return line.text(` during ${parsed.subject}.${parsed.action}`);
|
|
3851
|
+
const step = ctx?.lastStepTitle?.trim();
|
|
3852
|
+
if (step) return line.text(" while ").value(`"${truncateValue(step, opts.valueMax * 1.5)}"`);
|
|
3853
|
+
return line;
|
|
3854
|
+
}
|
|
3855
|
+
function buildCrash(parsed, opts) {
|
|
3856
|
+
const line = new Line();
|
|
3857
|
+
const what = /Page crashed/i.test(parsed.messageHead) ? "Page crashed" : "Page or browser closed";
|
|
3858
|
+
if (parsed.action) {
|
|
3859
|
+
const verb = ACTION_VERBS[parsed.action] ?? parsed.action;
|
|
3860
|
+
line.text(`${what} during ${verb}`);
|
|
3861
|
+
if (parsed.locator) line.text(" on ").locator(opts.locator ?? parsed.locator);
|
|
3862
|
+
return line;
|
|
3863
|
+
}
|
|
3864
|
+
if (parsed.assertion && parsed.locator) {
|
|
3865
|
+
return line.text(`${what} while expecting `).locator(opts.locator ?? parsed.locator).text(` ${parsed.assertion}`);
|
|
3866
|
+
}
|
|
3867
|
+
return line.text(what);
|
|
3868
|
+
}
|
|
3869
|
+
function build(parsed, opts, ctx) {
|
|
3870
|
+
switch (parsed.kind) {
|
|
3871
|
+
case "action-timeout":
|
|
3872
|
+
return buildActionTimeout(parsed, opts);
|
|
3873
|
+
case "assertion":
|
|
3874
|
+
case "assertion-timeout":
|
|
3875
|
+
return buildAssertion(parsed, opts);
|
|
3876
|
+
case "strict-mode":
|
|
3877
|
+
return buildStrictMode(parsed, opts);
|
|
3878
|
+
case "navigation":
|
|
3879
|
+
return buildNavigation(parsed, opts);
|
|
3880
|
+
case "test-timeout":
|
|
3881
|
+
return buildTestTimeout(parsed, opts, ctx);
|
|
3882
|
+
case "crash":
|
|
3883
|
+
return buildCrash(parsed, opts);
|
|
3884
|
+
default:
|
|
3885
|
+
return new Line().text(firstLineFallback(parsed));
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
function detailOf(parsed, headline) {
|
|
3889
|
+
switch (parsed.kind) {
|
|
3890
|
+
case "test-timeout":
|
|
3891
|
+
case "action-timeout": {
|
|
3892
|
+
const state = parsed.lastStateLine;
|
|
3893
|
+
if (!state || parsed.lastState === "not-found" || headline.includes(state)) return null;
|
|
3894
|
+
return truncateValue(state, HEADLINE_MAX_CHARS);
|
|
3895
|
+
}
|
|
3896
|
+
case "assertion":
|
|
3897
|
+
case "assertion-timeout": {
|
|
3898
|
+
const received = parsed.received;
|
|
3899
|
+
if (received && !/element\(s\) not found/.test(received)) {
|
|
3900
|
+
const shown = truncateValue(receivedDisplay(parsed), SHORT_VALUE_MAX_CHARS).slice(0, 8);
|
|
3901
|
+
if (!headline.includes(shown)) return truncateValue(`Received: ${received}`, HEADLINE_MAX_CHARS);
|
|
3902
|
+
}
|
|
3903
|
+
const state = parsed.lastStateLine;
|
|
3904
|
+
if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
|
|
3905
|
+
return truncateValue(state, HEADLINE_MAX_CHARS);
|
|
3906
|
+
}
|
|
3907
|
+
case "navigation":
|
|
3908
|
+
return parsed.networkErrorCode ?? parsed.lastStateLine;
|
|
3909
|
+
case "strict-mode":
|
|
3910
|
+
case "crash":
|
|
3911
|
+
return null;
|
|
3912
|
+
default:
|
|
3913
|
+
return null;
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
function describeFailure(parsed, ctx) {
|
|
3917
|
+
const attempts = [
|
|
3918
|
+
{ locator: parsed.locator, valueMax: VALUE_MAX_CHARS },
|
|
3919
|
+
{ locator: parsed.leafLocator ?? parsed.locator, valueMax: VALUE_MAX_CHARS },
|
|
3920
|
+
{ locator: parsed.leafLocator ?? parsed.locator, valueMax: SHORT_VALUE_MAX_CHARS }
|
|
3921
|
+
];
|
|
3922
|
+
let line = build(parsed, attempts[0], ctx);
|
|
3923
|
+
for (const opts of attempts.slice(1)) {
|
|
3924
|
+
if (line.toString().length <= HEADLINE_MAX_CHARS) break;
|
|
3925
|
+
line = build(parsed, opts, ctx);
|
|
3926
|
+
}
|
|
3927
|
+
let parts = line.parts.map((p) => ({ ...p, text: p.text.replace(MASK_TOKEN_RE, "\u2026") }));
|
|
3928
|
+
let headline = parts.map((p) => p.text).join("");
|
|
3929
|
+
if (headline.length > HEADLINE_MAX_CHARS) {
|
|
3930
|
+
headline = `${headline.slice(0, HEADLINE_MAX_CHARS - 1)}\u2026`;
|
|
3931
|
+
parts = clipParts(parts, HEADLINE_MAX_CHARS - 1);
|
|
3932
|
+
parts.push({ kind: "text", text: "\u2026" });
|
|
3933
|
+
}
|
|
3934
|
+
if (!headline.trim()) {
|
|
3935
|
+
headline = firstLineFallback(parsed);
|
|
3936
|
+
parts = [{ kind: "text", text: headline }];
|
|
3937
|
+
}
|
|
3938
|
+
return { headline, detail: detailOf(parsed, headline), parts };
|
|
3939
|
+
}
|
|
3940
|
+
function clipParts(parts, max) {
|
|
3941
|
+
const out = [];
|
|
3942
|
+
let used = 0;
|
|
3943
|
+
for (const part of parts) {
|
|
3944
|
+
if (used >= max) break;
|
|
3945
|
+
const text = part.text.slice(0, max - used);
|
|
3946
|
+
if (text) out.push({ kind: part.kind, text });
|
|
3947
|
+
used += text.length;
|
|
3948
|
+
}
|
|
3949
|
+
return out;
|
|
3950
|
+
}
|
|
3951
|
+
function describeFailureText(raw, ctx) {
|
|
3952
|
+
if (!raw || !raw.trim()) return null;
|
|
3953
|
+
return describeFailure(parsePlaywrightError(raw, { stepParams: ctx?.stepParams }), ctx);
|
|
3954
|
+
}
|
|
3955
|
+
function lastStepTitle(steps) {
|
|
3956
|
+
if (!steps || steps.length === 0) return null;
|
|
3957
|
+
const failed = steps.find((s) => s.failed);
|
|
3958
|
+
return (failed ?? steps[steps.length - 1])?.title ?? null;
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
// src/internal/support/failure-links.ts
|
|
3962
|
+
function failureHeadline(error, steps) {
|
|
3963
|
+
return describeFailureText(error, { lastStepTitle: lastStepTitle(steps) })?.headline ?? null;
|
|
3964
|
+
}
|
|
3965
|
+
function caseLocateUrl(serverUrl, runId, test) {
|
|
3966
|
+
const params = [
|
|
3967
|
+
`file=${encodeURIComponent(test.file)}`,
|
|
3968
|
+
`title=${encodeURIComponent(test.title)}`,
|
|
3969
|
+
`retry=${test.retry}`
|
|
3970
|
+
];
|
|
3971
|
+
if (test.browser) params.push(`browser=${encodeURIComponent(test.browser)}`);
|
|
3972
|
+
return `${serverUrl.replace(/\/+$/, "")}/test-runs/${runId}/locate?${params.join("&")}`;
|
|
3973
|
+
}
|
|
3974
|
+
function formatFailureLine(link) {
|
|
3975
|
+
const headline = link.headline ? ` \u2014 ${link.headline}` : "";
|
|
3976
|
+
return `\u2717 ${link.title}${headline} \u2192 ${link.url}`;
|
|
3977
|
+
}
|
|
3978
|
+
var FailureLinks = class {
|
|
3979
|
+
constructor(serverUrl, logger) {
|
|
3980
|
+
this.serverUrl = serverUrl;
|
|
3981
|
+
this.logger = logger;
|
|
3982
|
+
this.failures = [];
|
|
3983
|
+
this.printed = 0;
|
|
3984
|
+
}
|
|
3985
|
+
/** Number of failed tests recorded so far. */
|
|
3986
|
+
get count() {
|
|
3987
|
+
return this.failures.length;
|
|
3988
|
+
}
|
|
3989
|
+
add(test) {
|
|
3990
|
+
this.failures.push(test);
|
|
3991
|
+
}
|
|
3992
|
+
/** Every recorded failure with its link under `runId`. */
|
|
3993
|
+
resolve(runId) {
|
|
3994
|
+
return this.failures.map((test) => ({ ...test, url: caseLocateUrl(this.serverUrl, runId, test) }));
|
|
3995
|
+
}
|
|
3996
|
+
/** Print the lines that have not been printed yet. */
|
|
3997
|
+
printPending(runId) {
|
|
3998
|
+
const links = this.resolve(runId);
|
|
3999
|
+
for (const link of links.slice(this.printed)) this.logger.info(formatFailureLine(link));
|
|
4000
|
+
this.printed = links.length;
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
|
|
2608
4004
|
// src/public/reporter.ts
|
|
2609
4005
|
function testLocation(test) {
|
|
2610
|
-
|
|
2611
|
-
|
|
4006
|
+
return `${testFile(test)}:${test.location.line}:${test.location.column}`;
|
|
4007
|
+
}
|
|
4008
|
+
function testFile(test) {
|
|
4009
|
+
return path15.relative(process.cwd(), test.location.file).split(path15.sep).join("/");
|
|
2612
4010
|
}
|
|
2613
4011
|
var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
2614
4012
|
constructor(rawOptions = {}) {
|
|
@@ -2640,6 +4038,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2640
4038
|
this.setupSteps = [];
|
|
2641
4039
|
this.options = resolveOptions(rawOptions);
|
|
2642
4040
|
this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
|
|
4041
|
+
this.listMode = isListMode();
|
|
2643
4042
|
this.viaDesktopApp = usedDesktopDiscovery();
|
|
2644
4043
|
this.runLabel = this.options.runLabel || detectCiRunLabel();
|
|
2645
4044
|
this.instanceId = computeInstanceId(this.options.projectName, this.runLabel);
|
|
@@ -2650,6 +4049,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2650
4049
|
this.uploader = new Uploader(this.httpClient, this.fileHandler, logger);
|
|
2651
4050
|
this.recovery = new CrashRecovery(this.options.projectName, logger);
|
|
2652
4051
|
this.metadataCollector = new MetadataCollector(logger);
|
|
4052
|
+
this.failureLinks = new FailureLinks(this.httpClient.baseUrl, logger);
|
|
2653
4053
|
const streamBuffer = new StreamBuffer(this.options.projectName);
|
|
2654
4054
|
streamBuffer.clearStale();
|
|
2655
4055
|
if (this.options.streaming) {
|
|
@@ -2663,7 +4063,14 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2663
4063
|
logger
|
|
2664
4064
|
);
|
|
2665
4065
|
}
|
|
2666
|
-
this.submitter = new RunSubmitter(
|
|
4066
|
+
this.submitter = new RunSubmitter(
|
|
4067
|
+
this.httpClient,
|
|
4068
|
+
this.uploader,
|
|
4069
|
+
this.recovery,
|
|
4070
|
+
this.streamManager,
|
|
4071
|
+
logger,
|
|
4072
|
+
this.failureLinks
|
|
4073
|
+
);
|
|
2667
4074
|
}
|
|
2668
4075
|
static {
|
|
2669
4076
|
this.wrapConfig = wrapConfig;
|
|
@@ -2673,6 +4080,10 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2673
4080
|
}
|
|
2674
4081
|
/** Playwright reporter hook: called once at the start of the test run */
|
|
2675
4082
|
onBegin(config, suite) {
|
|
4083
|
+
if (this.listMode) {
|
|
4084
|
+
this.logger.debug("List mode (--list) detected \u2014 no run registered or report uploaded.");
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
2676
4087
|
if (!this.enabled) {
|
|
2677
4088
|
this.logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
2678
4089
|
return;
|
|
@@ -2686,6 +4097,12 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2686
4097
|
this.logger.info(
|
|
2687
4098
|
`Starting test run for project: ${this.options.projectName} (Playwright v${this.playwrightVersion})`
|
|
2688
4099
|
);
|
|
4100
|
+
const defaulted = process.env[PIWI_DEFAULTED_CAPTURE_ENV];
|
|
4101
|
+
if (defaulted) {
|
|
4102
|
+
this.logger.info(
|
|
4103
|
+
`Defaulted Playwright ${defaulted} for failure evidence (set defaultCapture: false to opt out).`
|
|
4104
|
+
);
|
|
4105
|
+
}
|
|
2689
4106
|
const rawConfig = config;
|
|
2690
4107
|
const grepRe = rawConfig.grep instanceof RegExp ? rawConfig.grep : void 0;
|
|
2691
4108
|
const grepInvertRe = rawConfig.grepInvert instanceof RegExp ? rawConfig.grepInvert : void 0;
|
|
@@ -2756,6 +4173,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2756
4173
|
const event = {
|
|
2757
4174
|
type: "step-begin",
|
|
2758
4175
|
title: step.title,
|
|
4176
|
+
subtitle: typeof step.subtitle === "string" && step.subtitle.length > 0 ? step.subtitle : null,
|
|
2759
4177
|
location: step.location ? `${step.location.file}:${step.location.line}:${step.location.column}` : "unknown",
|
|
2760
4178
|
stepCategory: cat,
|
|
2761
4179
|
parentTitle: test?.title || null,
|
|
@@ -2774,6 +4192,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2774
4192
|
const event = {
|
|
2775
4193
|
type: "step-end",
|
|
2776
4194
|
title: step.title,
|
|
4195
|
+
subtitle: typeof step.subtitle === "string" && step.subtitle.length > 0 ? step.subtitle : null,
|
|
2777
4196
|
location: step.location ? `${step.location.file}:${step.location.line}:${step.location.column}` : "unknown",
|
|
2778
4197
|
status: step.error ? "failed" : "passed",
|
|
2779
4198
|
duration: step.duration || 0,
|
|
@@ -2803,6 +4222,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2803
4222
|
const annotations = mergeAnnotations(test, result);
|
|
2804
4223
|
const status = classifyStatus(result.status, annotations);
|
|
2805
4224
|
const tags = collectTestTags(test);
|
|
4225
|
+
const locks = collectTestLocks(test);
|
|
2806
4226
|
const attempts = this.attemptsByTest.get(test.id) ?? [];
|
|
2807
4227
|
attempts.push({
|
|
2808
4228
|
retry: result.retry,
|
|
@@ -2832,6 +4252,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2832
4252
|
suiteConfig,
|
|
2833
4253
|
testAnnotations: annotations.length ? annotations : null,
|
|
2834
4254
|
tags: tags.length ? tags : null,
|
|
4255
|
+
locks: locks.length ? locks : null,
|
|
2835
4256
|
testMeta: collectTestMetadata(annotations),
|
|
2836
4257
|
// An annotation-less skip reclassified to `didnotrun` is a serial-group
|
|
2837
4258
|
// cascade: an earlier test failed and Playwright skipped the rest.
|
|
@@ -2880,6 +4301,18 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2880
4301
|
}
|
|
2881
4302
|
this.testCases.push(testCase);
|
|
2882
4303
|
if (status === "didnotrun") linkBlockedTests(this.testCases);
|
|
4304
|
+
const isFailure = status === "failed" || status === "timedOut";
|
|
4305
|
+
if (isFailure && result.retry >= (test.retries ?? 0)) {
|
|
4306
|
+
this.failureLinks.add({
|
|
4307
|
+
title: test.title,
|
|
4308
|
+
file: testFile(test),
|
|
4309
|
+
retry: result.retry,
|
|
4310
|
+
browser: typeof testCase.browser?.projectName === "string" ? testCase.browser.projectName : null,
|
|
4311
|
+
headline: failureHeadline(testCase.error, testCase.performanceMetrics?.steps)
|
|
4312
|
+
});
|
|
4313
|
+
}
|
|
4314
|
+
const liveRunId = this.streamManager?.runId;
|
|
4315
|
+
if (liveRunId != null) this.failureLinks.printPending(liveRunId);
|
|
2883
4316
|
if (this.streamManager) {
|
|
2884
4317
|
this.streamManager.queueEvent(toWireTestCase(testCase));
|
|
2885
4318
|
if (this.options.liveFileUploads) this.streamManager.scheduleLiveUpload(testCase);
|
|
@@ -2899,6 +4332,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2899
4332
|
const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
|
|
2900
4333
|
const declaredAnnotations = test.annotations ?? [];
|
|
2901
4334
|
const tags = collectTestTags(test);
|
|
4335
|
+
const locks = collectTestLocks(test);
|
|
2902
4336
|
const testCase = {
|
|
2903
4337
|
type: "complete",
|
|
2904
4338
|
title: test.title,
|
|
@@ -2917,6 +4351,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2917
4351
|
suiteConfig,
|
|
2918
4352
|
testAnnotations: declaredAnnotations.length ? declaredAnnotations : null,
|
|
2919
4353
|
tags: tags.length ? tags : null,
|
|
4354
|
+
locks: locks.length ? locks : null,
|
|
2920
4355
|
testMeta: collectTestMetadata(declaredAnnotations),
|
|
2921
4356
|
didNotRunReason: reason
|
|
2922
4357
|
};
|
|
@@ -2930,34 +4365,38 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
2930
4365
|
}
|
|
2931
4366
|
/** Playwright reporter hook: called when the full test run finishes */
|
|
2932
4367
|
async onEnd(result) {
|
|
2933
|
-
if (!this.enabled) return;
|
|
4368
|
+
if (this.listMode || !this.enabled) return;
|
|
2934
4369
|
const unrunReason = resolveUnrunReason(result?.status, {
|
|
2935
4370
|
maxFailures: this.maxFailures,
|
|
2936
4371
|
failures: this.failedTests + this.timedOutTests
|
|
2937
4372
|
});
|
|
2938
4373
|
this.materializeUnrunTests(unrunReason);
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
4374
|
+
try {
|
|
4375
|
+
await this.submitter.submit(
|
|
4376
|
+
{
|
|
4377
|
+
options: this.options,
|
|
4378
|
+
testCases: this.testCases,
|
|
4379
|
+
startTime: this.startTime,
|
|
4380
|
+
playwrightVersion: this.playwrightVersion,
|
|
4381
|
+
reporterVersion: this.reporterVersion,
|
|
4382
|
+
totalTests: this.totalTests,
|
|
4383
|
+
passedTests: this.passedTests,
|
|
4384
|
+
failedTests: this.failedTests,
|
|
4385
|
+
skippedTests: this.skippedTests,
|
|
4386
|
+
timedOutTests: this.timedOutTests,
|
|
4387
|
+
didNotRunTests: this.didNotRunTests,
|
|
4388
|
+
metadata: this.metadata,
|
|
4389
|
+
instanceId: this.instanceId,
|
|
4390
|
+
shardInfo: this.shardInfo,
|
|
4391
|
+
setupSteps: this.setupSteps,
|
|
4392
|
+
isFullRun: this.isFullRun,
|
|
4393
|
+
filterDetails: this.filterDetails
|
|
4394
|
+
},
|
|
4395
|
+
result
|
|
4396
|
+
);
|
|
4397
|
+
} finally {
|
|
4398
|
+
this.fileHandler.cleanupBodyAttachments();
|
|
4399
|
+
}
|
|
2961
4400
|
}
|
|
2962
4401
|
};
|
|
2963
4402
|
|
|
@@ -3165,15 +4604,24 @@ function probeElementAttrs(el, arg) {
|
|
|
3165
4604
|
let index = -1;
|
|
3166
4605
|
let levelCount = 0;
|
|
3167
4606
|
let roleNameCount = 0;
|
|
4607
|
+
let visibleRoleNameCount = 0;
|
|
3168
4608
|
for (let i = 0; i < nodes.length; i++) {
|
|
3169
4609
|
const n = nodes[i];
|
|
3170
4610
|
if (roleOf(n) !== targetRole) continue;
|
|
3171
4611
|
if (n === el) index = roleCountAll;
|
|
3172
4612
|
roleCountAll++;
|
|
3173
4613
|
if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
|
|
3174
|
-
if (targetName != null && nameOf(n) === targetName)
|
|
4614
|
+
if (targetName != null && nameOf(n) === targetName) {
|
|
4615
|
+
roleNameCount++;
|
|
4616
|
+
const node = n;
|
|
4617
|
+
const box = typeof node.getBoundingClientRect === "function" ? node.getBoundingClientRect() : null;
|
|
4618
|
+
if (node.offsetParent != null || !!box && box.width > 0 && box.height > 0) visibleRoleNameCount++;
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
if (targetName != null) {
|
|
4622
|
+
selectorCounts.roleName = roleNameCount;
|
|
4623
|
+
selectorCounts.visibleRoleName = visibleRoleNameCount;
|
|
3175
4624
|
}
|
|
3176
|
-
if (targetName != null) selectorCounts.roleName = roleNameCount;
|
|
3177
4625
|
if (index !== -1) {
|
|
3178
4626
|
rolePosition = {
|
|
3179
4627
|
role: targetRole,
|
|
@@ -4384,7 +5832,7 @@ function mergeCandidates(base, extra) {
|
|
|
4384
5832
|
}
|
|
4385
5833
|
|
|
4386
5834
|
// src/internal/capture/locator-healing.ts
|
|
4387
|
-
var
|
|
5835
|
+
var path16 = __toESM(require("path"));
|
|
4388
5836
|
|
|
4389
5837
|
// ../core/src/locator-fingerprint.ts
|
|
4390
5838
|
var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
|
|
@@ -4508,18 +5956,6 @@ function freshLocatorsFromCandidate(c) {
|
|
|
4508
5956
|
return out;
|
|
4509
5957
|
}
|
|
4510
5958
|
|
|
4511
|
-
// ../core/src/locator-methods.ts
|
|
4512
|
-
var LOCATOR_BUILDER_METHODS = [
|
|
4513
|
-
"getByRole",
|
|
4514
|
-
"getByTestId",
|
|
4515
|
-
"getByText",
|
|
4516
|
-
"getByLabel",
|
|
4517
|
-
"getByPlaceholder",
|
|
4518
|
-
"getByAltText",
|
|
4519
|
-
"getByTitle",
|
|
4520
|
-
"locator"
|
|
4521
|
-
];
|
|
4522
|
-
|
|
4523
5959
|
// src/internal/capture/locator-healing.ts
|
|
4524
5960
|
function dedupeSnapshotsByLocation(snaps) {
|
|
4525
5961
|
const lastWithElement = /* @__PURE__ */ new Map();
|
|
@@ -4540,6 +5976,7 @@ var CHAIN_METHODS = [
|
|
|
4540
5976
|
"nth",
|
|
4541
5977
|
"last",
|
|
4542
5978
|
"filter",
|
|
5979
|
+
"visible",
|
|
4543
5980
|
"and",
|
|
4544
5981
|
"or",
|
|
4545
5982
|
"locator",
|
|
@@ -4765,10 +6202,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
4765
6202
|
}
|
|
4766
6203
|
let rel = file;
|
|
4767
6204
|
try {
|
|
4768
|
-
rel =
|
|
6205
|
+
rel = path16.relative(process.cwd(), file);
|
|
4769
6206
|
} catch {
|
|
4770
6207
|
}
|
|
4771
|
-
rel = rel.split(
|
|
6208
|
+
rel = rel.split(path16.sep).join("/");
|
|
4772
6209
|
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
4773
6210
|
return `${rel}:${m[3]}:${m[4]}`;
|
|
4774
6211
|
}
|
|
@@ -4811,8 +6248,8 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
4811
6248
|
}
|
|
4812
6249
|
|
|
4813
6250
|
// src/internal/capture/pick-on-failure.ts
|
|
4814
|
-
var
|
|
4815
|
-
var
|
|
6251
|
+
var path17 = __toESM(require("path"));
|
|
6252
|
+
var ANSI_RE2 = /\[[0-9;]*m/g;
|
|
4816
6253
|
function endOfString(s, start) {
|
|
4817
6254
|
const q = s[start];
|
|
4818
6255
|
for (let i = start + 1; i < s.length; i++) {
|
|
@@ -4899,13 +6336,13 @@ function deriveFailedLocator(testInfo) {
|
|
|
4899
6336
|
const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
|
|
4900
6337
|
for (const err of errors) {
|
|
4901
6338
|
const text = `${err.message ?? ""}
|
|
4902
|
-
${err.stack ?? ""}`.replace(
|
|
6339
|
+
${err.stack ?? ""}`.replace(ANSI_RE2, "");
|
|
4903
6340
|
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
4904
6341
|
if (!line) continue;
|
|
4905
6342
|
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
4906
6343
|
if (!parsed) continue;
|
|
4907
6344
|
const loc = err.location;
|
|
4908
|
-
const location = loc ? `${
|
|
6345
|
+
const location = loc ? `${path17.relative(process.cwd(), loc.file).split(path17.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
4909
6346
|
return { method: parsed.method, args: parsed.args, location };
|
|
4910
6347
|
}
|
|
4911
6348
|
return null;
|
|
@@ -5087,6 +6524,7 @@ function createSink() {
|
|
|
5087
6524
|
return {
|
|
5088
6525
|
networkRequests: [],
|
|
5089
6526
|
consoleEntries: [],
|
|
6527
|
+
dialogs: [],
|
|
5090
6528
|
pendingHandlers: [],
|
|
5091
6529
|
capturedLocators: [],
|
|
5092
6530
|
capturePromises: [],
|
|
@@ -5097,6 +6535,7 @@ function createSink() {
|
|
|
5097
6535
|
stashedWebVitals: null,
|
|
5098
6536
|
stashedPageState: null,
|
|
5099
6537
|
stashedAria: null,
|
|
6538
|
+
stashedAriaJson: null,
|
|
5100
6539
|
pickOffered: false,
|
|
5101
6540
|
userPick: null
|
|
5102
6541
|
};
|
|
@@ -5285,9 +6724,17 @@ async function stashPageState(sink, closing) {
|
|
|
5285
6724
|
if (pageState) sink.stashedPageState = pageState;
|
|
5286
6725
|
}
|
|
5287
6726
|
const status = sink.testInfo?.status;
|
|
5288
|
-
|
|
5289
|
-
const
|
|
6727
|
+
const sampleAria = async () => {
|
|
6728
|
+
const root = page.locator(":root");
|
|
6729
|
+
const aria = await ariaSnapshotBestEffort(root, 1e3);
|
|
5290
6730
|
if (aria) sink.stashedAria = aria;
|
|
6731
|
+
const ariaJson = await ariaSnapshotJSONBestEffort(root, 1e3);
|
|
6732
|
+
if (ariaJson) sink.stashedAriaJson = ariaJson;
|
|
6733
|
+
};
|
|
6734
|
+
if (status === "failed" || status === "timedOut" || status === "interrupted") {
|
|
6735
|
+
await sampleAria();
|
|
6736
|
+
} else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
|
|
6737
|
+
await sampleAria();
|
|
5291
6738
|
}
|
|
5292
6739
|
}
|
|
5293
6740
|
async function maybeOpenPicker(sink, closing) {
|
|
@@ -5318,6 +6765,7 @@ var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
|
|
|
5318
6765
|
var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
|
|
5319
6766
|
var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
|
|
5320
6767
|
var ACTION_METHOD_SET = new Set(ACTION_METHODS);
|
|
6768
|
+
var LOCATOR_METHOD_SET = new Set(LOCATOR_METHODS);
|
|
5321
6769
|
var CAPTURED_ATTRS_ARG = {
|
|
5322
6770
|
keep: [...CAPTURED_ATTRIBUTES],
|
|
5323
6771
|
tagRoles: TAG_TO_ROLE,
|
|
@@ -5370,6 +6818,16 @@ async function ariaSnapshotBestEffort(target, timeout) {
|
|
|
5370
6818
|
}
|
|
5371
6819
|
}
|
|
5372
6820
|
}
|
|
6821
|
+
async function ariaSnapshotJSONBestEffort(target, timeout) {
|
|
6822
|
+
const fn = target.ariaSnapshotJSON;
|
|
6823
|
+
if (typeof fn !== "function") return null;
|
|
6824
|
+
try {
|
|
6825
|
+
const tree = await fn.call(target, timeout != null ? { timeout } : {});
|
|
6826
|
+
return tree == null ? null : JSON.stringify(tree);
|
|
6827
|
+
} catch {
|
|
6828
|
+
return null;
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
5373
6831
|
function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
5374
6832
|
const probe = probeElement(page, target);
|
|
5375
6833
|
const settledProbe = probe.then(
|
|
@@ -5504,6 +6962,20 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
5504
6962
|
}
|
|
5505
6963
|
});
|
|
5506
6964
|
}
|
|
6965
|
+
function wrapFrameLocator(page, frameLocator) {
|
|
6966
|
+
return new Proxy(frameLocator, {
|
|
6967
|
+
get(target, prop) {
|
|
6968
|
+
const original = Reflect.get(target, prop);
|
|
6969
|
+
if (typeof original !== "function") return original;
|
|
6970
|
+
const fn = original;
|
|
6971
|
+
if (!LOCATOR_METHOD_SET.has(prop)) return original;
|
|
6972
|
+
return (...args) => {
|
|
6973
|
+
if (currentSink) currentSink.lastActivePage = page;
|
|
6974
|
+
return wrapLocator(page, fn.apply(target, args), String(prop), args);
|
|
6975
|
+
};
|
|
6976
|
+
}
|
|
6977
|
+
});
|
|
6978
|
+
}
|
|
5507
6979
|
function instrumentPage(page) {
|
|
5508
6980
|
if (!page || INSTRUMENTED_PAGES.has(page)) return;
|
|
5509
6981
|
INSTRUMENTED_PAGES.add(page);
|
|
@@ -5531,6 +7003,13 @@ function instrumentPage(page) {
|
|
|
5531
7003
|
return wrapLocator(page, original(...args), method, args);
|
|
5532
7004
|
};
|
|
5533
7005
|
}
|
|
7006
|
+
const originalFrameLocator = typeof page.frameLocator === "function" ? page.frameLocator.bind(page) : null;
|
|
7007
|
+
if (originalFrameLocator) {
|
|
7008
|
+
page.frameLocator = (...args) => {
|
|
7009
|
+
const frame = originalFrameLocator(...args);
|
|
7010
|
+
return args.length === 0 ? wrapFrameLocator(page, frame) : frame;
|
|
7011
|
+
};
|
|
7012
|
+
}
|
|
5534
7013
|
if (typeof page.on === "function") {
|
|
5535
7014
|
page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
|
|
5536
7015
|
}
|
|
@@ -5549,6 +7028,24 @@ function instrumentPage(page) {
|
|
|
5549
7028
|
});
|
|
5550
7029
|
}
|
|
5551
7030
|
});
|
|
7031
|
+
if (typeof page.on === "function") {
|
|
7032
|
+
try {
|
|
7033
|
+
page.on("dialogclosed", (dialog) => {
|
|
7034
|
+
const sink = currentSink;
|
|
7035
|
+
if (!sink) return;
|
|
7036
|
+
try {
|
|
7037
|
+
sink.dialogs.push({
|
|
7038
|
+
type: typeof dialog.type === "function" ? dialog.type() : null,
|
|
7039
|
+
message: typeof dialog.message === "function" ? dialog.message() : null,
|
|
7040
|
+
defaultValue: typeof dialog.defaultValue === "function" ? dialog.defaultValue() || null : null,
|
|
7041
|
+
closedAt: Date.now()
|
|
7042
|
+
});
|
|
7043
|
+
} catch {
|
|
7044
|
+
}
|
|
7045
|
+
});
|
|
7046
|
+
} catch {
|
|
7047
|
+
}
|
|
7048
|
+
}
|
|
5552
7049
|
page.on("requestfinished", (request) => {
|
|
5553
7050
|
const sink = currentSink;
|
|
5554
7051
|
if (!sink) return;
|
|
@@ -5675,6 +7172,13 @@ async function flushSink(sink, testInfo) {
|
|
|
5675
7172
|
contentType: "text/plain",
|
|
5676
7173
|
body: snapshot
|
|
5677
7174
|
});
|
|
7175
|
+
const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
|
|
7176
|
+
if (snapshotJson) {
|
|
7177
|
+
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
7178
|
+
contentType: "application/json",
|
|
7179
|
+
body: snapshotJson
|
|
7180
|
+
});
|
|
7181
|
+
}
|
|
5678
7182
|
const failed = sink.failedLocators[sink.failedLocators.length - 1];
|
|
5679
7183
|
const suggestion = failed ? suggestLocatorsFromAria(failed, snapshot) : null;
|
|
5680
7184
|
if (suggestion) {
|
|
@@ -5691,6 +7195,25 @@ async function flushSink(sink, testInfo) {
|
|
|
5691
7195
|
} catch {
|
|
5692
7196
|
}
|
|
5693
7197
|
}
|
|
7198
|
+
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
7199
|
+
try {
|
|
7200
|
+
const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
|
|
7201
|
+
if (snapshot) {
|
|
7202
|
+
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
7203
|
+
contentType: "text/plain",
|
|
7204
|
+
body: snapshot
|
|
7205
|
+
});
|
|
7206
|
+
const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
|
|
7207
|
+
if (snapshotJson) {
|
|
7208
|
+
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
7209
|
+
contentType: "application/json",
|
|
7210
|
+
body: snapshotJson
|
|
7211
|
+
});
|
|
7212
|
+
}
|
|
7213
|
+
}
|
|
7214
|
+
} catch {
|
|
7215
|
+
}
|
|
7216
|
+
}
|
|
5694
7217
|
if (sink.userPick) {
|
|
5695
7218
|
const pick = sink.userPick;
|
|
5696
7219
|
testInfo.annotations.push({
|
|
@@ -5714,6 +7237,12 @@ async function flushSink(sink, testInfo) {
|
|
|
5714
7237
|
body: Buffer.from(JSON.stringify(sink.consoleEntries))
|
|
5715
7238
|
});
|
|
5716
7239
|
}
|
|
7240
|
+
if (sink.dialogs.length > 0) {
|
|
7241
|
+
await testInfo.attach(ATTACHMENT_NAMES.dialogs, {
|
|
7242
|
+
contentType: "application/json",
|
|
7243
|
+
body: Buffer.from(JSON.stringify(sink.dialogs))
|
|
7244
|
+
});
|
|
7245
|
+
}
|
|
5717
7246
|
if (sink.networkRequests.length > 0) {
|
|
5718
7247
|
await testInfo.attach(ATTACHMENT_NAMES.network, {
|
|
5719
7248
|
contentType: "application/json",
|
|
@@ -5777,15 +7306,15 @@ function extendPiwiFixtures(test) {
|
|
|
5777
7306
|
}
|
|
5778
7307
|
|
|
5779
7308
|
// src/internal/ai/ai-fixtures.ts
|
|
5780
|
-
var
|
|
5781
|
-
var
|
|
7309
|
+
var fs16 = __toESM(require("fs"));
|
|
7310
|
+
var path20 = __toESM(require("path"));
|
|
5782
7311
|
var import_test = require("@playwright/test");
|
|
5783
7312
|
|
|
5784
7313
|
// src/internal/ai/artifact.ts
|
|
5785
|
-
var
|
|
5786
|
-
var
|
|
7314
|
+
var fs15 = __toESM(require("fs"));
|
|
7315
|
+
var path18 = __toESM(require("path"));
|
|
5787
7316
|
var ARTIFACT_VERSION = 1;
|
|
5788
|
-
var
|
|
7317
|
+
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
5789
7318
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
5790
7319
|
var POSTCONDITION_ASSERTS = /* @__PURE__ */ new Set([
|
|
5791
7320
|
"visible",
|
|
@@ -5818,7 +7347,7 @@ function validateStructuredLocator(value, where) {
|
|
|
5818
7347
|
assert(value !== null && typeof value === "object", `${where}: locator must be an object`);
|
|
5819
7348
|
const loc = value;
|
|
5820
7349
|
assert(typeof loc.method === "string", `${where}: locator.method must be a string`);
|
|
5821
|
-
assert(
|
|
7350
|
+
assert(LOCATOR_METHOD_SET2.has(loc.method), `${where}: locator method "${String(loc.method)}" is not allowlisted`);
|
|
5822
7351
|
assert(Array.isArray(loc.args), `${where}: locator.args must be an array`);
|
|
5823
7352
|
if (loc.chain !== void 0) {
|
|
5824
7353
|
assert(Array.isArray(loc.chain), `${where}: locator.chain must be an array`);
|
|
@@ -5876,7 +7405,7 @@ function parseEntry(text) {
|
|
|
5876
7405
|
function readEntry(file) {
|
|
5877
7406
|
let text;
|
|
5878
7407
|
try {
|
|
5879
|
-
text =
|
|
7408
|
+
text = fs15.readFileSync(file, "utf8");
|
|
5880
7409
|
} catch (error) {
|
|
5881
7410
|
if (error.code === "ENOENT") return null;
|
|
5882
7411
|
throw error;
|
|
@@ -5886,28 +7415,28 @@ function readEntry(file) {
|
|
|
5886
7415
|
function writeEntry(file, entry) {
|
|
5887
7416
|
const canonical = serializeEntry(entry);
|
|
5888
7417
|
return withEntryLock(file, () => {
|
|
5889
|
-
if (
|
|
5890
|
-
|
|
7418
|
+
if (fs15.existsSync(file) && fs15.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
7419
|
+
fs15.mkdirSync(path18.dirname(file), { recursive: true });
|
|
5891
7420
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
5892
|
-
|
|
5893
|
-
|
|
7421
|
+
fs15.writeFileSync(tmp, canonical);
|
|
7422
|
+
fs15.renameSync(tmp, file);
|
|
5894
7423
|
return { written: true };
|
|
5895
7424
|
});
|
|
5896
7425
|
}
|
|
5897
7426
|
function withEntryLock(file, fn) {
|
|
5898
7427
|
const lock = `${file}.lock`;
|
|
5899
|
-
|
|
7428
|
+
fs15.mkdirSync(path18.dirname(file), { recursive: true });
|
|
5900
7429
|
const deadline = Date.now() + 5e3;
|
|
5901
7430
|
for (; ; ) {
|
|
5902
7431
|
try {
|
|
5903
|
-
const fd =
|
|
5904
|
-
|
|
7432
|
+
const fd = fs15.openSync(lock, "wx");
|
|
7433
|
+
fs15.closeSync(fd);
|
|
5905
7434
|
break;
|
|
5906
7435
|
} catch (error) {
|
|
5907
7436
|
if (error.code !== "EEXIST") throw error;
|
|
5908
7437
|
if (Date.now() > deadline) {
|
|
5909
7438
|
try {
|
|
5910
|
-
|
|
7439
|
+
fs15.unlinkSync(lock);
|
|
5911
7440
|
} catch {
|
|
5912
7441
|
}
|
|
5913
7442
|
}
|
|
@@ -5917,7 +7446,7 @@ function withEntryLock(file, fn) {
|
|
|
5917
7446
|
return fn();
|
|
5918
7447
|
} finally {
|
|
5919
7448
|
try {
|
|
5920
|
-
|
|
7449
|
+
fs15.unlinkSync(lock);
|
|
5921
7450
|
} catch {
|
|
5922
7451
|
}
|
|
5923
7452
|
}
|
|
@@ -5925,7 +7454,7 @@ function withEntryLock(file, fn) {
|
|
|
5925
7454
|
|
|
5926
7455
|
// src/internal/ai/keys.ts
|
|
5927
7456
|
var crypto3 = __toESM(require("crypto"));
|
|
5928
|
-
var
|
|
7457
|
+
var path19 = __toESM(require("path"));
|
|
5929
7458
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
5930
7459
|
function normalizeTemplate(template) {
|
|
5931
7460
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -5939,13 +7468,13 @@ function slug(text, maxLength = 40) {
|
|
|
5939
7468
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
5940
7469
|
}
|
|
5941
7470
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
5942
|
-
return
|
|
7471
|
+
return path19.join(path19.dirname(specFile), dir, path19.basename(specFile));
|
|
5943
7472
|
}
|
|
5944
7473
|
function entryPath(params) {
|
|
5945
7474
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
5946
7475
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
5947
7476
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
5948
|
-
return
|
|
7477
|
+
return path19.join(entryDir(params.specFile, params.dir), name);
|
|
5949
7478
|
}
|
|
5950
7479
|
function parseLocation(location) {
|
|
5951
7480
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -6036,7 +7565,7 @@ function isParametric(template, compiledText) {
|
|
|
6036
7565
|
}
|
|
6037
7566
|
|
|
6038
7567
|
// src/internal/ai/interpreter.ts
|
|
6039
|
-
var
|
|
7568
|
+
var LOCATOR_METHOD_SET3 = new Set(LOCATOR_METHODS);
|
|
6040
7569
|
var ACTION_METHOD_SET3 = new Set(ACTION_METHODS);
|
|
6041
7570
|
var StepDriftError = class extends Error {
|
|
6042
7571
|
constructor(step, fingerprint) {
|
|
@@ -6074,7 +7603,7 @@ function describePostcondition(post) {
|
|
|
6074
7603
|
return post.assert === "url" ? `url \u2192 ${post.url ?? ""}` : `${describeLocator(post.locator)} ${post.assert}`;
|
|
6075
7604
|
}
|
|
6076
7605
|
function buildLocator(root, structured, params = {}) {
|
|
6077
|
-
if (!
|
|
7606
|
+
if (!LOCATOR_METHOD_SET3.has(structured.method)) {
|
|
6078
7607
|
throw new Error(`piwi AI: locator method "${structured.method}" is not allowlisted`);
|
|
6079
7608
|
}
|
|
6080
7609
|
const args = substituteArgs(structured.args, params);
|
|
@@ -6399,7 +7928,7 @@ function testIdentity(testInfo) {
|
|
|
6399
7928
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
6400
7929
|
}
|
|
6401
7930
|
function relativeToCwd(file) {
|
|
6402
|
-
return
|
|
7931
|
+
return path20.relative(process.cwd(), file).split(path20.sep).join("/");
|
|
6403
7932
|
}
|
|
6404
7933
|
function recordIntents(intents, entry) {
|
|
6405
7934
|
const add = (locator, kind) => {
|
|
@@ -6426,7 +7955,7 @@ function createAiApi(page, testInfo, config, used, intents) {
|
|
|
6426
7955
|
const readSource = () => {
|
|
6427
7956
|
if (source === null) {
|
|
6428
7957
|
try {
|
|
6429
|
-
source =
|
|
7958
|
+
source = fs16.readFileSync(testInfo.file, "utf8");
|
|
6430
7959
|
} catch {
|
|
6431
7960
|
source = "";
|
|
6432
7961
|
}
|