@piwitests/reporter 0.55.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1392 -1377
- package/dist/global-setup-module.js +32 -22
- package/dist/index.d.ts +9 -0
- package/dist/index.js +555 -347
- package/dist/internal/capture/capture-fixtures.d.ts +2 -1
- package/dist/internal/capture/capture-fixtures.js +1035 -980
- package/dist/internal/capture/locator-healing.js +70 -26
- package/dist/internal/capture/pick-on-failure.js +84 -54
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
45
45
|
// src/public/reporter.ts
|
|
46
46
|
var fs24 = __toESM(require("fs"));
|
|
47
47
|
var os9 = __toESM(require("os"));
|
|
48
|
-
var
|
|
48
|
+
var path24 = __toESM(require("path"));
|
|
49
49
|
|
|
50
50
|
// src/internal/config/desktop.ts
|
|
51
51
|
var fs = __toESM(require("fs"));
|
|
@@ -363,11 +363,15 @@ var HttpClient = class {
|
|
|
363
363
|
* @param logger Prefixed logger for verbose diagnostics.
|
|
364
364
|
* @param timeout Socket inactivity timeout in ms (default 30s), so a hung
|
|
365
365
|
* server fails fast rather than stalling the reporter.
|
|
366
|
+
* @param uploadTimeout Socket inactivity timeout of a multipart upload in ms
|
|
367
|
+
* (default 10 min): the dashboard answers one only once it
|
|
368
|
+
* has stored every report and trace in it.
|
|
366
369
|
*/
|
|
367
|
-
constructor(serverUrl, logger = new Logger(), timeout = 3e4) {
|
|
370
|
+
constructor(serverUrl, logger = new Logger(), timeout = 3e4, uploadTimeout = 10 * 60 * 1e3) {
|
|
368
371
|
this.serverUrl = serverUrl;
|
|
369
372
|
this.logger = logger;
|
|
370
373
|
this.timeout = timeout;
|
|
374
|
+
this.uploadTimeout = uploadTimeout;
|
|
371
375
|
/** Requests in flight, so `close` can abort them. */
|
|
372
376
|
this.inFlight = /* @__PURE__ */ new Set();
|
|
373
377
|
/** Set by `close`: every later request fails with this reason. */
|
|
@@ -515,7 +519,7 @@ var HttpClient = class {
|
|
|
515
519
|
/** Send a multipart form-data POST request. Used for report and trace uploads. */
|
|
516
520
|
async postFormData(pathname, form, auth) {
|
|
517
521
|
const headers = form.getHeaders();
|
|
518
|
-
const res = await this.request("POST", pathname, { headers, form, auth });
|
|
522
|
+
const res = await this.request("POST", pathname, { headers, form, auth, timeout: this.uploadTimeout });
|
|
519
523
|
if (res.status < 200 || res.status >= 300) {
|
|
520
524
|
this.logger.debugError(`Response: ${res.text}`);
|
|
521
525
|
const detail = parseErrorMessage(res.text);
|
|
@@ -568,8 +572,9 @@ var HttpClient = class {
|
|
|
568
572
|
this.inFlight.add(req);
|
|
569
573
|
req.on("close", () => this.inFlight.delete(req));
|
|
570
574
|
req.on("error", reject);
|
|
571
|
-
|
|
572
|
-
|
|
575
|
+
const timeout = opts.timeout ?? this.timeout;
|
|
576
|
+
req.setTimeout(timeout, () => {
|
|
577
|
+
req.destroy(new Error(`Request to ${pathname} timed out after ${timeout}ms`));
|
|
573
578
|
});
|
|
574
579
|
if (opts.form) {
|
|
575
580
|
opts.form.on("error", (error) => req.destroy(error));
|
|
@@ -663,13 +668,18 @@ function browserProjectName(browser) {
|
|
|
663
668
|
}
|
|
664
669
|
return "";
|
|
665
670
|
}
|
|
671
|
+
function isFailingStatus(status) {
|
|
672
|
+
return status === "failed" || status === "timedOut" || status === "timedout" || status === "interrupted";
|
|
673
|
+
}
|
|
666
674
|
function computeDistinctRunCounts(testCases) {
|
|
667
675
|
const final = /* @__PURE__ */ new Map();
|
|
668
676
|
for (const tc of testCases) {
|
|
669
|
-
const key =
|
|
677
|
+
const key = tc.testId ?? [tc.title, tc.location, (tc.suitePath ?? []).join(""), browserProjectName(tc.browser)].join("\0");
|
|
678
|
+
const status = tc.status ?? "";
|
|
670
679
|
const retries = tc.retries ?? 0;
|
|
671
680
|
const prev = final.get(key);
|
|
672
|
-
|
|
681
|
+
const decides = !prev || retries > prev.retries || retries === prev.retries && (isFailingStatus(status) || !isFailingStatus(prev.status));
|
|
682
|
+
if (decides) final.set(key, { status, retries });
|
|
673
683
|
}
|
|
674
684
|
const counts = {
|
|
675
685
|
totalTests: final.size,
|
|
@@ -677,14 +687,11 @@ function computeDistinctRunCounts(testCases) {
|
|
|
677
687
|
failedTests: 0,
|
|
678
688
|
timedOutTests: 0,
|
|
679
689
|
skippedTests: 0,
|
|
680
|
-
didNotRunTests: 0
|
|
681
|
-
flakyTests: 0
|
|
690
|
+
didNotRunTests: 0
|
|
682
691
|
};
|
|
683
|
-
for (const { status
|
|
684
|
-
if (status === "passed")
|
|
685
|
-
|
|
686
|
-
if (retries > 0) counts.flakyTests++;
|
|
687
|
-
} else if (status === "failed") counts.failedTests++;
|
|
692
|
+
for (const { status } of final.values()) {
|
|
693
|
+
if (status === "passed") counts.passedTests++;
|
|
694
|
+
else if (status === "failed") counts.failedTests++;
|
|
688
695
|
else if (status === "timedOut" || status === "timedout") counts.timedOutTests++;
|
|
689
696
|
else if (status === "skipped") counts.skippedTests++;
|
|
690
697
|
else if (status === "didnotrun") counts.didNotRunTests++;
|
|
@@ -719,6 +726,7 @@ function serializeRun(payload, opts) {
|
|
|
719
726
|
filterDetails: payload.filterDetails ?? null
|
|
720
727
|
};
|
|
721
728
|
if (payload.resourceReport) body.resourceReport = payload.resourceReport;
|
|
729
|
+
if (payload.deliveryKey) body.deliveryKey = payload.deliveryKey;
|
|
722
730
|
if (opts.includeTestCases) {
|
|
723
731
|
body.testCases = payload.testCases.map((tc) => toWireTestCase(tc));
|
|
724
732
|
}
|
|
@@ -990,6 +998,9 @@ var StreamBuffer = class {
|
|
|
990
998
|
};
|
|
991
999
|
|
|
992
1000
|
// src/internal/streaming/crash-recovery.ts
|
|
1001
|
+
var crypto2 = __toESM(require("crypto"));
|
|
1002
|
+
var fs4 = __toESM(require("fs"));
|
|
1003
|
+
var os4 = __toESM(require("os"));
|
|
993
1004
|
var path4 = __toESM(require("path"));
|
|
994
1005
|
|
|
995
1006
|
// src/internal/support/errors.ts
|
|
@@ -999,45 +1010,79 @@ function errorMessage(error) {
|
|
|
999
1010
|
}
|
|
1000
1011
|
|
|
1001
1012
|
// src/internal/streaming/crash-recovery.ts
|
|
1002
|
-
|
|
1003
|
-
|
|
1013
|
+
function recoveryServer(serverUrl) {
|
|
1014
|
+
return serverUrl.replace(/\/+$/, "");
|
|
1015
|
+
}
|
|
1016
|
+
function recoveryFilePath(serverUrl, projectName) {
|
|
1017
|
+
const key = crypto2.createHash("sha256").update(`${recoveryServer(serverUrl)}
|
|
1018
|
+
${projectName}`).digest("hex").slice(0, 16);
|
|
1019
|
+
return path4.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
|
|
1020
|
+
}
|
|
1021
|
+
function refusesRun(error) {
|
|
1022
|
+
return error instanceof HttpError && [400, 409, 422].includes(error.status);
|
|
1023
|
+
}
|
|
1004
1024
|
var CrashRecovery = class {
|
|
1005
1025
|
/**
|
|
1006
|
-
* @param
|
|
1026
|
+
* @param serverUrl The dashboard the run is for: the file is kept for it and replayed to it only.
|
|
1027
|
+
* @param projectName With `serverUrl`, the key of the file.
|
|
1007
1028
|
* @param logger Prefixed logger.
|
|
1008
1029
|
*/
|
|
1009
|
-
constructor(projectName, logger = new Logger()) {
|
|
1030
|
+
constructor(serverUrl, projectName, logger = new Logger()) {
|
|
1010
1031
|
this.logger = logger;
|
|
1011
|
-
this.
|
|
1032
|
+
this.serverUrl = recoveryServer(serverUrl);
|
|
1033
|
+
this.filePath = recoveryFilePath(serverUrl, projectName);
|
|
1012
1034
|
}
|
|
1013
|
-
/**
|
|
1014
|
-
save(
|
|
1035
|
+
/** Write the payload for a later retry, owner-only, replacing the file in one step. */
|
|
1036
|
+
save(payload) {
|
|
1037
|
+
const file = { v: 1, serverUrl: this.serverUrl, payload };
|
|
1038
|
+
const temp = `${this.filePath}.${process.pid}.${crypto2.randomBytes(6).toString("hex")}.tmp`;
|
|
1015
1039
|
try {
|
|
1016
|
-
fs4.writeFileSync(
|
|
1040
|
+
fs4.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
|
|
1041
|
+
fs4.renameSync(temp, this.filePath);
|
|
1017
1042
|
this.logger.info("Saved recovery data for later upload");
|
|
1018
1043
|
} catch (error) {
|
|
1044
|
+
fs4.rmSync(temp, { force: true });
|
|
1019
1045
|
this.logger.error(`Failed to save recovery data: ${errorMessage(error)}`);
|
|
1020
1046
|
}
|
|
1021
1047
|
}
|
|
1022
|
-
/**
|
|
1048
|
+
/**
|
|
1049
|
+
* The saved payload, or null when there is none, or when the file is not a
|
|
1050
|
+
* regular file this user owns, or names another dashboard.
|
|
1051
|
+
*/
|
|
1023
1052
|
load() {
|
|
1053
|
+
let fd;
|
|
1024
1054
|
try {
|
|
1025
|
-
|
|
1055
|
+
fd = fs4.openSync(this.filePath, fs4.constants.O_RDONLY | (fs4.constants.O_NOFOLLOW ?? 0));
|
|
1026
1056
|
} catch {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
try {
|
|
1060
|
+
const stat = fs4.fstatSync(fd);
|
|
1061
|
+
if (!stat.isFile() || typeof process.getuid === "function" && stat.uid !== process.getuid()) return null;
|
|
1062
|
+
const file = JSON.parse(fs4.readFileSync(fd, "utf8"));
|
|
1063
|
+
if (file?.v !== 1 || file.serverUrl !== this.serverUrl) return null;
|
|
1064
|
+
return file.payload && typeof file.payload === "object" ? file.payload : null;
|
|
1065
|
+
} catch {
|
|
1066
|
+
return null;
|
|
1067
|
+
} finally {
|
|
1068
|
+
fs4.closeSync(fd);
|
|
1027
1069
|
}
|
|
1028
|
-
return null;
|
|
1029
1070
|
}
|
|
1030
1071
|
/** Delete the recovery file from disk */
|
|
1031
1072
|
clear() {
|
|
1032
1073
|
try {
|
|
1033
|
-
|
|
1074
|
+
fs4.rmSync(this.filePath, { force: true });
|
|
1034
1075
|
} catch {
|
|
1035
1076
|
}
|
|
1036
1077
|
}
|
|
1037
|
-
/**
|
|
1078
|
+
/**
|
|
1079
|
+
* Submit a previously saved payload to the dashboard it was saved for. The
|
|
1080
|
+
* file is deleted once the dashboard took the run, or refused it for good;
|
|
1081
|
+
* anything else keeps it for the next run.
|
|
1082
|
+
*/
|
|
1038
1083
|
async tryUpload(httpClient, auth) {
|
|
1039
1084
|
const data = this.load();
|
|
1040
|
-
if (!data) return;
|
|
1085
|
+
if (!data || recoveryServer(httpClient.baseUrl) !== this.serverUrl) return;
|
|
1041
1086
|
this.logger.info("Found saved test data from a previous run, uploading...");
|
|
1042
1087
|
delete data.instanceId;
|
|
1043
1088
|
const metadata = data.metadata && typeof data.metadata === "object" ? data.metadata : {};
|
|
@@ -1047,6 +1092,11 @@ var CrashRecovery = class {
|
|
|
1047
1092
|
this.logger.info("Successfully uploaded saved test data");
|
|
1048
1093
|
this.clear();
|
|
1049
1094
|
} catch (error) {
|
|
1095
|
+
if (refusesRun(error)) {
|
|
1096
|
+
this.logger.warn(`The dashboard refused the saved test data, which is deleted: ${errorMessage(error)}`);
|
|
1097
|
+
this.clear();
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1050
1100
|
this.logger.warn(`Could not upload saved test data: ${errorMessage(error)}`);
|
|
1051
1101
|
}
|
|
1052
1102
|
}
|
|
@@ -1056,7 +1106,7 @@ var CrashRecovery = class {
|
|
|
1056
1106
|
var fs6 = __toESM(require("fs"));
|
|
1057
1107
|
var os5 = __toESM(require("os"));
|
|
1058
1108
|
var path6 = __toESM(require("path"));
|
|
1059
|
-
var
|
|
1109
|
+
var crypto3 = __toESM(require("crypto"));
|
|
1060
1110
|
|
|
1061
1111
|
// src/internal/files/compression.ts
|
|
1062
1112
|
var fs5 = __toESM(require("fs"));
|
|
@@ -1355,7 +1405,7 @@ var FileHandler = class {
|
|
|
1355
1405
|
if (fs6.existsSync(tp)) lastPath = tp;
|
|
1356
1406
|
}
|
|
1357
1407
|
if (!lastPath) return null;
|
|
1358
|
-
const hash =
|
|
1408
|
+
const hash = crypto3.createHash("sha256");
|
|
1359
1409
|
await new Promise((resolve9, reject) => {
|
|
1360
1410
|
fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve9).on("error", reject);
|
|
1361
1411
|
});
|
|
@@ -1387,6 +1437,31 @@ function formatMiB(bytes) {
|
|
|
1387
1437
|
|
|
1388
1438
|
// src/internal/collect/metadata-collector.ts
|
|
1389
1439
|
var import_node_child_process = require("child_process");
|
|
1440
|
+
|
|
1441
|
+
// src/internal/support/test-paths.ts
|
|
1442
|
+
var path7 = __toESM(require("path"));
|
|
1443
|
+
function configDirFromConfig(config) {
|
|
1444
|
+
if (config?.configFile) return path7.dirname(config.configFile);
|
|
1445
|
+
if (config?.rootDir) return config.rootDir;
|
|
1446
|
+
return process.cwd();
|
|
1447
|
+
}
|
|
1448
|
+
function relativeTestPath(file, base) {
|
|
1449
|
+
return path7.relative(base, file).split(path7.sep).join("/");
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/internal/collect/metadata-collector.ts
|
|
1453
|
+
function withoutCredentials(remoteUrl) {
|
|
1454
|
+
let url;
|
|
1455
|
+
try {
|
|
1456
|
+
url = new URL(remoteUrl);
|
|
1457
|
+
} catch {
|
|
1458
|
+
return remoteUrl;
|
|
1459
|
+
}
|
|
1460
|
+
if (!url.username && !url.password) return remoteUrl;
|
|
1461
|
+
url.username = "";
|
|
1462
|
+
url.password = "";
|
|
1463
|
+
return url.toString();
|
|
1464
|
+
}
|
|
1390
1465
|
var DETACHED_HEAD = "HEAD";
|
|
1391
1466
|
function resolveScmBranch(env, gitBranch) {
|
|
1392
1467
|
const pick = (...names) => {
|
|
@@ -1454,7 +1529,7 @@ var MetadataCollector = class {
|
|
|
1454
1529
|
/** Collect all available metadata from the environment, config, and suite */
|
|
1455
1530
|
collect(config, suite, options) {
|
|
1456
1531
|
const metadata = {
|
|
1457
|
-
workingDir:
|
|
1532
|
+
workingDir: configDirFromConfig(config),
|
|
1458
1533
|
[RUN_ORIGIN_METADATA_KEY]: resolveRunOrigin(process.env, (message) => this.logger.warn(message))
|
|
1459
1534
|
};
|
|
1460
1535
|
if (options.projectDescription) metadata.projectDescription = options.projectDescription;
|
|
@@ -1572,7 +1647,7 @@ var MetadataCollector = class {
|
|
|
1572
1647
|
scm.author = (0, import_node_child_process.execSync)('git log -1 --pretty=format:"%an"', execOpts).trim();
|
|
1573
1648
|
scm.commitMessage = (0, import_node_child_process.execSync)('git log -1 --pretty=format:"%s"', execOpts).trim();
|
|
1574
1649
|
try {
|
|
1575
|
-
scm.remoteUrl = (0, import_node_child_process.execSync)("git config --get remote.origin.url", execOpts).trim();
|
|
1650
|
+
scm.remoteUrl = withoutCredentials((0, import_node_child_process.execSync)("git config --get remote.origin.url", execOpts).trim());
|
|
1576
1651
|
} catch {
|
|
1577
1652
|
}
|
|
1578
1653
|
} catch (error) {
|
|
@@ -1825,11 +1900,11 @@ function createLimiter(maxConcurrent) {
|
|
|
1825
1900
|
}
|
|
1826
1901
|
|
|
1827
1902
|
// src/internal/support/setup-file.ts
|
|
1828
|
-
var
|
|
1903
|
+
var path8 = __toESM(require("path"));
|
|
1829
1904
|
var os6 = __toESM(require("os"));
|
|
1830
1905
|
var fs7 = __toESM(require("fs"));
|
|
1831
1906
|
function getSetupFilePath(projectName) {
|
|
1832
|
-
return
|
|
1907
|
+
return path8.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
|
|
1833
1908
|
}
|
|
1834
1909
|
function readSetupInfo(projectName) {
|
|
1835
1910
|
const setupFile = getSetupFilePath(projectName);
|
|
@@ -2678,6 +2753,10 @@ function tryParseLocatorChain(expr) {
|
|
|
2678
2753
|
return null;
|
|
2679
2754
|
}
|
|
2680
2755
|
}
|
|
2756
|
+
function quoteJsString(value) {
|
|
2757
|
+
const json = JSON.stringify(value).slice(1, -1);
|
|
2758
|
+
return `'${json.replace(/\\"/g, '"').replace(/'/g, "\\'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
|
|
2759
|
+
}
|
|
2681
2760
|
function parseLeafLocatorCall(expr) {
|
|
2682
2761
|
const chain = tryParseLocatorChain(expr);
|
|
2683
2762
|
if (!chain) return null;
|
|
@@ -2744,11 +2823,20 @@ function extractMessageHead(text) {
|
|
|
2744
2823
|
let head = text;
|
|
2745
2824
|
const callLogIdx = head.indexOf("\nCall log:");
|
|
2746
2825
|
if (callLogIdx !== -1) head = head.slice(0, callLogIdx);
|
|
2747
|
-
const stackIdx = head
|
|
2826
|
+
const stackIdx = stackStart(head);
|
|
2748
2827
|
if (stackIdx !== -1) head = head.slice(0, stackIdx);
|
|
2749
2828
|
const lines = head.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2750
2829
|
return lines.slice(0, 5).join("\n");
|
|
2751
2830
|
}
|
|
2831
|
+
function stackStart(text) {
|
|
2832
|
+
const runs = /\s+/g;
|
|
2833
|
+
for (let run = runs.exec(text); run; run = runs.exec(text)) {
|
|
2834
|
+
if (!text.startsWith("at ", runs.lastIndex)) continue;
|
|
2835
|
+
const lineBreak = run[0].indexOf("\n");
|
|
2836
|
+
if (lineBreak !== -1 && lineBreak < run[0].length - 1) return run.index + lineBreak;
|
|
2837
|
+
}
|
|
2838
|
+
return -1;
|
|
2839
|
+
}
|
|
2752
2840
|
function extractSelector(text) {
|
|
2753
2841
|
const match = SELECTOR_FN_RE.exec(text);
|
|
2754
2842
|
if (!match) return null;
|
|
@@ -2869,7 +2957,7 @@ function readChain(region) {
|
|
|
2869
2957
|
return link && CHAIN_LINK_METHODS.has(link[1]) ? null : scanned;
|
|
2870
2958
|
}
|
|
2871
2959
|
function extractStackFrames(text) {
|
|
2872
|
-
const frameRe =
|
|
2960
|
+
const frameRe = /^[ \t]+at (?:.*? \()?([^()\s][^()\n]*?):(\d+):(\d+)\)?[ \t]*$/gm;
|
|
2873
2961
|
const frames = [];
|
|
2874
2962
|
let m;
|
|
2875
2963
|
while ((m = frameRe.exec(text)) !== null) {
|
|
@@ -2992,21 +3080,25 @@ function readCallLog(text) {
|
|
|
2992
3080
|
return read;
|
|
2993
3081
|
}
|
|
2994
3082
|
function headerValue(text, label) {
|
|
2995
|
-
const re = new RegExp(
|
|
3083
|
+
const re = new RegExp(`^[ \\t]*${label}(?: string| pattern| substring| value)?:[ \\t]*(.*)$`, "m");
|
|
2996
3084
|
const m = re.exec(text);
|
|
2997
3085
|
if (!m) return null;
|
|
2998
3086
|
const value = m[1].trim();
|
|
2999
3087
|
return value.length > 0 ? value : null;
|
|
3000
3088
|
}
|
|
3089
|
+
function readLocatorHeader(text) {
|
|
3090
|
+
const header = /^[ \t]*Locator:[ \t]*(.+)$/m.exec(text);
|
|
3091
|
+
return header ? header[1].trim() : null;
|
|
3092
|
+
}
|
|
3001
3093
|
function readLocator(text, strict) {
|
|
3002
|
-
const header =
|
|
3003
|
-
if (header) return header
|
|
3094
|
+
const header = readLocatorHeader(text);
|
|
3095
|
+
if (header) return header;
|
|
3004
3096
|
if (strict) return strict[1].trim();
|
|
3005
3097
|
return extractLocatorChain(text);
|
|
3006
3098
|
}
|
|
3007
3099
|
function readTimeout(text, callLog) {
|
|
3008
3100
|
const patterns = [
|
|
3009
|
-
|
|
3101
|
+
/^[ \t]*Timeout:[ \t]*(\d+)ms/m,
|
|
3010
3102
|
/\bTimeout (\d+)ms exceeded/,
|
|
3011
3103
|
TEST_TIMEOUT_RE,
|
|
3012
3104
|
TIMED_OUT_EXPECT_RE,
|
|
@@ -3023,7 +3115,7 @@ function readUrl(text, callLog, received) {
|
|
|
3023
3115
|
const at = /\b(?:net::ERR_[A-Z0-9_]+|NS_ERROR_[A-Z0-9_]+) at (\S+)/.exec(text);
|
|
3024
3116
|
if (at) return at[1];
|
|
3025
3117
|
const head = extractMessageHead(text);
|
|
3026
|
-
const inHead = URL_RE.exec(head.replace(
|
|
3118
|
+
const inHead = URL_RE.exec(head.replace(/^[ \t]*Received.*$/gm, ""));
|
|
3027
3119
|
if (inHead) return inHead[0];
|
|
3028
3120
|
if (received) {
|
|
3029
3121
|
const inReceived = URL_RE.exec(received);
|
|
@@ -3039,8 +3131,9 @@ function readCustomMessage(messageHead) {
|
|
|
3039
3131
|
if (!first || MATCHER_LINE_RE.test(first)) return null;
|
|
3040
3132
|
return /^(?:expect[.(]|Timed out\b)/.test(lines[1]) ? first : null;
|
|
3041
3133
|
}
|
|
3134
|
+
var MAX_PARSED_ERROR_CHARS = 64 * 1024;
|
|
3042
3135
|
function parsePlaywrightError(raw, context) {
|
|
3043
|
-
const clean = stripAnsi(raw ?? "").replace(/\r\n?/g, "\n");
|
|
3136
|
+
const clean = stripAnsi((raw ?? "").slice(0, MAX_PARSED_ERROR_CHARS)).replace(/\r\n?/g, "\n");
|
|
3044
3137
|
const text = withoutStackFrames(clean);
|
|
3045
3138
|
const messageHead = extractMessageHead(clean);
|
|
3046
3139
|
const firstLine2 = messageHead.split("\n")[0] ?? "";
|
|
@@ -3077,7 +3170,7 @@ function parsePlaywrightError(raw, context) {
|
|
|
3077
3170
|
else if (testTimeout) kind = "test-timeout";
|
|
3078
3171
|
else if (CRASH_RE.test(text)) kind = "crash";
|
|
3079
3172
|
else if (isAssertion) {
|
|
3080
|
-
const retrying =
|
|
3173
|
+
const retrying = /^[ \t]*Timeout:[ \t]*\d+ms/m.test(text) || TIMED_OUT_EXPECT_RE.test(text) || callLog.matcher !== null;
|
|
3081
3174
|
kind = retrying ? "assertion-timeout" : "assertion";
|
|
3082
3175
|
} else if (isNavigationFailure) kind = "navigation";
|
|
3083
3176
|
else if (/\bTimeout \d+ms exceeded/.test(text) || errorName === "TimeoutError") kind = "action-timeout";
|
|
@@ -3128,9 +3221,25 @@ var LONG_HEX_RE = /\b[0-9a-f]{32,}\b/gi;
|
|
|
3128
3221
|
function maskTokenLike(text) {
|
|
3129
3222
|
return text.replace(DATA_URI_RE, "data:[masked]").replace(JWT_RE, "[masked-token]").replace(LONG_HEX_RE, "[masked-hex]");
|
|
3130
3223
|
}
|
|
3224
|
+
var CREDENTIAL_PARAM_RE = /token|code|secret|password|passwd|pwd|key|session|auth|sig|credential|assertion|nonce|otp|jwt/i;
|
|
3225
|
+
function maskUrlFragment(hash) {
|
|
3226
|
+
const body = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
3227
|
+
const query = body.indexOf("?");
|
|
3228
|
+
const route = query >= 0 ? body.slice(0, query) : body.includes("=") ? "" : body;
|
|
3229
|
+
const params = query >= 0 ? body.slice(query + 1) : body.includes("=") ? body : null;
|
|
3230
|
+
const masked = params === null ? null : params.split("&").map((param) => {
|
|
3231
|
+
const eq = param.indexOf("=");
|
|
3232
|
+
if (eq < 0) return maskTokenLike(param);
|
|
3233
|
+
const name = param.slice(0, eq);
|
|
3234
|
+
const value = param.slice(eq + 1);
|
|
3235
|
+
return `${name}=${value && CREDENTIAL_PARAM_RE.test(name) ? "[masked]" : maskTokenLike(value)}`;
|
|
3236
|
+
}).join("&");
|
|
3237
|
+
const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
|
|
3238
|
+
return hash.startsWith("#") ? `#${out}` : out;
|
|
3239
|
+
}
|
|
3131
3240
|
|
|
3132
3241
|
// ../core/src/locator-break.ts
|
|
3133
|
-
var filePath = (
|
|
3242
|
+
var filePath = (path32) => path32.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
3134
3243
|
function sameFilePath(a, b) {
|
|
3135
3244
|
const x = filePath(a);
|
|
3136
3245
|
const y = filePath(b);
|
|
@@ -3559,17 +3668,17 @@ function extractWaitEvents(steps, insideWait = false) {
|
|
|
3559
3668
|
|
|
3560
3669
|
// src/internal/support/reporter-version.ts
|
|
3561
3670
|
var fs9 = __toESM(require("fs"));
|
|
3562
|
-
var
|
|
3671
|
+
var path9 = __toESM(require("path"));
|
|
3563
3672
|
var cachedVersion = null;
|
|
3564
3673
|
var CANDIDATE_OFFSETS = ["..", "../../.."];
|
|
3565
3674
|
function findOwnPackageJson(fromDir) {
|
|
3566
3675
|
for (const offset of CANDIDATE_OFFSETS) {
|
|
3567
3676
|
try {
|
|
3568
|
-
const pkgPath =
|
|
3677
|
+
const pkgPath = path9.resolve(fromDir, offset, "package.json");
|
|
3569
3678
|
const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
|
|
3570
3679
|
const { name, version } = pkg;
|
|
3571
3680
|
if (name === "@piwitests/reporter" && typeof version === "string")
|
|
3572
|
-
return { version, root:
|
|
3681
|
+
return { version, root: path9.dirname(pkgPath) };
|
|
3573
3682
|
} catch {
|
|
3574
3683
|
}
|
|
3575
3684
|
}
|
|
@@ -3583,7 +3692,7 @@ function getReporterVersion() {
|
|
|
3583
3692
|
|
|
3584
3693
|
// src/internal/support/source-snippet.ts
|
|
3585
3694
|
var fs10 = __toESM(require("fs"));
|
|
3586
|
-
var
|
|
3695
|
+
var path10 = __toESM(require("path"));
|
|
3587
3696
|
function readSourceSnippet(file, declLine, context, failingLine) {
|
|
3588
3697
|
try {
|
|
3589
3698
|
const content = fs10.readFileSync(file, "utf-8");
|
|
@@ -3605,6 +3714,10 @@ function readSourceSnippet(file, declLine, context, failingLine) {
|
|
|
3605
3714
|
return null;
|
|
3606
3715
|
}
|
|
3607
3716
|
}
|
|
3717
|
+
var MAX_SCANNED_ERROR_CHARS = 64 * 1024;
|
|
3718
|
+
function stackFrames(errorText) {
|
|
3719
|
+
return extractStackFrames(errorText.slice(0, MAX_SCANNED_ERROR_CHARS));
|
|
3720
|
+
}
|
|
3608
3721
|
function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
|
|
3609
3722
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
3610
3723
|
const context = opts.context ?? 8;
|
|
@@ -3618,23 +3731,20 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
|
|
|
3618
3731
|
picked.push({ absFile, line });
|
|
3619
3732
|
};
|
|
3620
3733
|
if (errorText) {
|
|
3621
|
-
const
|
|
3622
|
-
|
|
3623
|
-
while ((m = stackRe.exec(errorText)) !== null && picked.length < maxFrames) {
|
|
3734
|
+
for (const frame of stackFrames(errorText)) {
|
|
3735
|
+
if (picked.length >= maxFrames) break;
|
|
3624
3736
|
let abs;
|
|
3625
3737
|
try {
|
|
3626
|
-
abs =
|
|
3738
|
+
abs = path10.resolve(frame.file);
|
|
3627
3739
|
} catch {
|
|
3628
3740
|
continue;
|
|
3629
3741
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
if (
|
|
3633
|
-
const line = parseInt(m[2], 10);
|
|
3634
|
-
if (!isNaN(line)) add(abs, line);
|
|
3742
|
+
const rel = path10.relative(projectRoot, abs);
|
|
3743
|
+
if (rel.startsWith("..") || path10.isAbsolute(rel)) continue;
|
|
3744
|
+
if (!isNaN(frame.line)) add(abs, frame.line);
|
|
3635
3745
|
}
|
|
3636
3746
|
}
|
|
3637
|
-
const absTest =
|
|
3747
|
+
const absTest = path10.resolve(testFile2);
|
|
3638
3748
|
if (!picked.some((p) => p.absFile === absTest)) {
|
|
3639
3749
|
add(absTest, extractFailingLine(errorText, testFile2, declLine));
|
|
3640
3750
|
}
|
|
@@ -3642,23 +3752,17 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
|
|
|
3642
3752
|
for (const p of picked.slice(0, maxFrames)) {
|
|
3643
3753
|
const snippet = readSourceSnippet(p.absFile, p.line, context, p.line);
|
|
3644
3754
|
if (snippet) {
|
|
3645
|
-
frames.push({ file:
|
|
3755
|
+
frames.push({ file: path10.relative(projectRoot, p.absFile) || path10.basename(p.absFile), line: p.line, snippet });
|
|
3646
3756
|
}
|
|
3647
3757
|
}
|
|
3648
3758
|
return frames;
|
|
3649
3759
|
}
|
|
3650
3760
|
function extractFailingLine(errorText, testFile2, declarationLine) {
|
|
3651
3761
|
if (!errorText) return declarationLine;
|
|
3652
|
-
const expectedFile =
|
|
3653
|
-
const
|
|
3654
|
-
let m;
|
|
3655
|
-
while ((m = stackRe.exec(errorText)) !== null) {
|
|
3762
|
+
const expectedFile = path10.resolve(testFile2);
|
|
3763
|
+
for (const frame of stackFrames(errorText)) {
|
|
3656
3764
|
try {
|
|
3657
|
-
|
|
3658
|
-
if (frameFile === expectedFile) {
|
|
3659
|
-
const line = parseInt(m[2], 10);
|
|
3660
|
-
if (!isNaN(line)) return line;
|
|
3661
|
-
}
|
|
3765
|
+
if (path10.resolve(frame.file) === expectedFile && !isNaN(frame.line)) return frame.line;
|
|
3662
3766
|
} catch {
|
|
3663
3767
|
}
|
|
3664
3768
|
}
|
|
@@ -3776,18 +3880,18 @@ function isListMode(argv = process.argv) {
|
|
|
3776
3880
|
}
|
|
3777
3881
|
|
|
3778
3882
|
// src/public/global-setup.ts
|
|
3779
|
-
var
|
|
3883
|
+
var path14 = __toESM(require("path"));
|
|
3780
3884
|
var fs14 = __toESM(require("fs"));
|
|
3781
3885
|
|
|
3782
3886
|
// src/internal/support/aria-sampling.ts
|
|
3783
|
-
var
|
|
3887
|
+
var path11 = __toESM(require("path"));
|
|
3784
3888
|
var os7 = __toESM(require("os"));
|
|
3785
3889
|
var fs11 = __toESM(require("fs"));
|
|
3786
3890
|
function ariaSampleIdentity(filePath2, title) {
|
|
3787
3891
|
return `${filePath2}\0${title}`;
|
|
3788
3892
|
}
|
|
3789
3893
|
function getAriaSampleFilePath(projectName) {
|
|
3790
|
-
return
|
|
3894
|
+
return path11.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
|
|
3791
3895
|
}
|
|
3792
3896
|
function writeAriaSampleFile(projectName, identities) {
|
|
3793
3897
|
try {
|
|
@@ -3817,15 +3921,14 @@ function loadAriaSampleSet(projectName) {
|
|
|
3817
3921
|
cachedSets.set(projectName, set);
|
|
3818
3922
|
return set;
|
|
3819
3923
|
}
|
|
3820
|
-
function relativeTestFile(file) {
|
|
3821
|
-
return path10.relative(process.cwd(), file).split(path10.sep).join("/");
|
|
3822
|
-
}
|
|
3823
3924
|
function isDueForAriaSample(testInfo) {
|
|
3824
3925
|
const projectName = process.env.PIWI_PROJECT_NAME;
|
|
3825
3926
|
if (!projectName) return false;
|
|
3826
3927
|
const set = loadAriaSampleSet(projectName);
|
|
3827
3928
|
if (!set || set.size === 0) return false;
|
|
3828
|
-
return set.has(
|
|
3929
|
+
return set.has(
|
|
3930
|
+
ariaSampleIdentity(relativeTestPath(testInfo.file, configDirFromConfig(testInfo.config)), testInfo.title)
|
|
3931
|
+
);
|
|
3829
3932
|
}
|
|
3830
3933
|
|
|
3831
3934
|
// src/internal/support/base-url-check.ts
|
|
@@ -3962,7 +4065,7 @@ async function checkBaseUrls(config, options = {}) {
|
|
|
3962
4065
|
|
|
3963
4066
|
// src/internal/manifest/collect.ts
|
|
3964
4067
|
var fs12 = __toESM(require("fs"));
|
|
3965
|
-
var
|
|
4068
|
+
var path12 = __toESM(require("path"));
|
|
3966
4069
|
|
|
3967
4070
|
// src/internal/transport/backend-request.ts
|
|
3968
4071
|
var http2 = __toESM(require("http"));
|
|
@@ -4028,7 +4131,7 @@ function hasInstrumentationHeader(headers) {
|
|
|
4028
4131
|
return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
|
|
4029
4132
|
}
|
|
4030
4133
|
function readCommittedManifest(configDir) {
|
|
4031
|
-
const file =
|
|
4134
|
+
const file = path12.join(configDir, COMMITTED_MANIFEST_FILE);
|
|
4032
4135
|
try {
|
|
4033
4136
|
if (!fs12.existsSync(file)) return null;
|
|
4034
4137
|
return parseManifestJson(fs12.readFileSync(file, "utf8"));
|
|
@@ -4036,11 +4139,6 @@ function readCommittedManifest(configDir) {
|
|
|
4036
4139
|
return null;
|
|
4037
4140
|
}
|
|
4038
4141
|
}
|
|
4039
|
-
function configDirFromConfig(config) {
|
|
4040
|
-
if (config?.configFile) return path11.dirname(config.configFile);
|
|
4041
|
-
if (config?.rootDir) return config.rootDir;
|
|
4042
|
-
return process.cwd();
|
|
4043
|
-
}
|
|
4044
4142
|
var INSTRUMENTATION_FETCH_TIMEOUT_MS = 5e3;
|
|
4045
4143
|
var MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
|
|
4046
4144
|
function backendManifestFetch(insecure) {
|
|
@@ -4062,8 +4160,8 @@ function backendManifestFetch(insecure) {
|
|
|
4062
4160
|
function instrumentationManifestUrls(baseUrl) {
|
|
4063
4161
|
const base = new URL(baseUrl);
|
|
4064
4162
|
const root = new URL(INSTRUMENTATION_MANIFEST_PATH, base).toString();
|
|
4065
|
-
const
|
|
4066
|
-
return
|
|
4163
|
+
const path32 = base.pathname.replace(/\/+$/, "");
|
|
4164
|
+
return path32 ? [new URL(`${path32}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
|
|
4067
4165
|
}
|
|
4068
4166
|
async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
|
|
4069
4167
|
try {
|
|
@@ -4201,13 +4299,13 @@ function normalizeFunctionRanges(ranges) {
|
|
|
4201
4299
|
}
|
|
4202
4300
|
function normalizeCoveragePath(raw) {
|
|
4203
4301
|
if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
|
|
4204
|
-
const
|
|
4205
|
-
if (!
|
|
4206
|
-
if (/(^|\/)node_modules\//.test(
|
|
4207
|
-
return
|
|
4302
|
+
const path32 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4303
|
+
if (!path32 || path32.startsWith("../") || path32.startsWith("/") || /^[A-Za-z]:\//.test(path32)) return null;
|
|
4304
|
+
if (/(^|\/)node_modules\//.test(path32)) return null;
|
|
4305
|
+
return path32;
|
|
4208
4306
|
}
|
|
4209
|
-
function fileEntry(
|
|
4210
|
-
const file = { path:
|
|
4307
|
+
function fileEntry(path32, runtime, blob, functions = [], lines = []) {
|
|
4308
|
+
const file = { path: path32, runtime };
|
|
4211
4309
|
if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
|
|
4212
4310
|
if (functions.length > 0) file.functions = functions;
|
|
4213
4311
|
if (lines.length > 0) file.lines = lines;
|
|
@@ -4226,11 +4324,11 @@ function sanitizeCoverage(raw) {
|
|
|
4226
4324
|
const files = [];
|
|
4227
4325
|
for (const item of raw.files) {
|
|
4228
4326
|
if (!isRecord(item) || !isRuntime(item.runtime)) continue;
|
|
4229
|
-
const
|
|
4230
|
-
if (
|
|
4327
|
+
const path32 = normalizeCoveragePath(item.path);
|
|
4328
|
+
if (path32 === null) continue;
|
|
4231
4329
|
const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
|
|
4232
4330
|
files.push(
|
|
4233
|
-
fileEntry(
|
|
4331
|
+
fileEntry(path32, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
|
|
4234
4332
|
);
|
|
4235
4333
|
}
|
|
4236
4334
|
const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
|
|
@@ -4294,9 +4392,9 @@ function reduceCoverage(capture, granularity) {
|
|
|
4294
4392
|
}
|
|
4295
4393
|
|
|
4296
4394
|
// src/internal/collect/source-checksums.ts
|
|
4297
|
-
var
|
|
4395
|
+
var crypto4 = __toESM(require("crypto"));
|
|
4298
4396
|
var fs13 = __toESM(require("fs"));
|
|
4299
|
-
var
|
|
4397
|
+
var path13 = __toESM(require("path"));
|
|
4300
4398
|
var CHECKSUM = /^(sha1|sha256):[0-9a-f]{40,64}$/;
|
|
4301
4399
|
function checksumsOf(raw) {
|
|
4302
4400
|
if (!Array.isArray(raw)) return [];
|
|
@@ -4312,7 +4410,7 @@ function contentChecksums(content) {
|
|
|
4312
4410
|
const out = /* @__PURE__ */ new Set();
|
|
4313
4411
|
for (const form of lineEndingForms(content)) {
|
|
4314
4412
|
for (const algorithm of ["sha1", "sha256"]) {
|
|
4315
|
-
out.add(`${algorithm}:${
|
|
4413
|
+
out.add(`${algorithm}:${crypto4.createHash(algorithm).update(form).digest("hex")}`);
|
|
4316
4414
|
}
|
|
4317
4415
|
}
|
|
4318
4416
|
return out;
|
|
@@ -4361,9 +4459,9 @@ var SourceChecksums = class {
|
|
|
4361
4459
|
}
|
|
4362
4460
|
async read(relativePath2) {
|
|
4363
4461
|
try {
|
|
4364
|
-
const root =
|
|
4365
|
-
const file =
|
|
4366
|
-
if (!file.startsWith(root +
|
|
4462
|
+
const root = path13.resolve(this.repoRoot());
|
|
4463
|
+
const file = path13.resolve(root, relativePath2);
|
|
4464
|
+
if (!file.startsWith(root + path13.sep)) return null;
|
|
4367
4465
|
return contentChecksums(await fs13.promises.readFile(file));
|
|
4368
4466
|
} catch {
|
|
4369
4467
|
return null;
|
|
@@ -4451,6 +4549,30 @@ function runFilesOf(answer) {
|
|
|
4451
4549
|
}
|
|
4452
4550
|
return files;
|
|
4453
4551
|
}
|
|
4552
|
+
function instanceOf(answer) {
|
|
4553
|
+
const instance = isRecord2(answer) ? answer.instance : null;
|
|
4554
|
+
if (!isRecord2(instance) || typeof instance.id !== "string" || !REQUEST_ID.test(instance.id)) return null;
|
|
4555
|
+
const startedAt = typeof instance.startedAt === "string" && !Number.isNaN(Date.parse(instance.startedAt)) ? new Date(instance.startedAt).toISOString() : null;
|
|
4556
|
+
return { id: instance.id, startedAt };
|
|
4557
|
+
}
|
|
4558
|
+
function processKeyOf(id) {
|
|
4559
|
+
const dot = id.lastIndexOf(".");
|
|
4560
|
+
return dot > 0 ? id.slice(0, dot) : null;
|
|
4561
|
+
}
|
|
4562
|
+
function withTestCoverage(files, tests) {
|
|
4563
|
+
if (!tests || tests.files.length === 0) return files;
|
|
4564
|
+
const byKey = new Map(files.map((file) => [`${file.runtime}\0${file.path}`, { ...file }]));
|
|
4565
|
+
for (const ran of tests.files) {
|
|
4566
|
+
const key = `${ran.runtime}\0${ran.path}`;
|
|
4567
|
+
const file = byKey.get(key) ?? { path: ran.path, runtime: ran.runtime };
|
|
4568
|
+
const functions = normalizeFunctionRanges([...file.functions ?? [], ...ran.functions ?? []]);
|
|
4569
|
+
const lines = normalizeLineRanges([...file.lines ?? [], ...ran.lines ?? []]);
|
|
4570
|
+
if (functions.length > 0) file.functions = functions;
|
|
4571
|
+
if (lines.length > 0) file.lines = lines;
|
|
4572
|
+
byKey.set(key, file);
|
|
4573
|
+
}
|
|
4574
|
+
return [...byKey.values()];
|
|
4575
|
+
}
|
|
4454
4576
|
function isCollectAnswer(answer) {
|
|
4455
4577
|
return isRecord2(answer) && answer.v === 1 && Array.isArray(answer.groups);
|
|
4456
4578
|
}
|
|
@@ -4502,11 +4624,16 @@ var ServerCoverageCollector = class {
|
|
|
4502
4624
|
}
|
|
4503
4625
|
/**
|
|
4504
4626
|
* The run-level coverage of an origin, with each server file's executable
|
|
4505
|
-
* lines at `lines` granularity
|
|
4627
|
+
* lines at `lines` granularity, and the lines and functions the tests'
|
|
4628
|
+
* coverage named added to it. The gap says why part of it is missing: the
|
|
4629
|
+
* backend restarted during the run (its answer names another process than
|
|
4630
|
+
* the run's first request ids), or it did not answer with run-level coverage.
|
|
4506
4631
|
*/
|
|
4507
4632
|
async fetchRun(origin) {
|
|
4508
4633
|
const state = this.stateOf(origin);
|
|
4509
4634
|
const query = `?granularity=${encodeURIComponent(this.options.granularity)}`;
|
|
4635
|
+
let answer = null;
|
|
4636
|
+
let failure = null;
|
|
4510
4637
|
for (const base of this.pathBases(origin)) {
|
|
4511
4638
|
const outcome = await this.call(
|
|
4512
4639
|
`${origin}${base}${RUN_PATH}${query}`,
|
|
@@ -4514,21 +4641,47 @@ var ServerCoverageCollector = class {
|
|
|
4514
4641
|
this.limits.runTimeoutMs
|
|
4515
4642
|
);
|
|
4516
4643
|
if (outcome.kind === "failed") {
|
|
4517
|
-
|
|
4518
|
-
|
|
4644
|
+
failure = outcome.reason;
|
|
4645
|
+
break;
|
|
4519
4646
|
}
|
|
4520
4647
|
const answered = outcome.ok ? runFilesOf(outcome.json) : null;
|
|
4521
4648
|
if (answered) {
|
|
4522
4649
|
state.base = base;
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
fileChecksums(isRecord2(outcome.json) ? outcome.json.files : null)
|
|
4526
|
-
);
|
|
4527
|
-
return this.options.blobs ? this.options.blobs.withBlobs(files.files) : files.files;
|
|
4650
|
+
answer = { files: answered, json: outcome.json };
|
|
4651
|
+
break;
|
|
4528
4652
|
}
|
|
4529
4653
|
}
|
|
4530
|
-
this.
|
|
4531
|
-
|
|
4654
|
+
const gap = answer ? this.restartGap(origin, state, instanceOf(answer.json)) : { origin, reason: "unreachable" };
|
|
4655
|
+
if (!answer) failure ??= this.noEndpoint(origin, RUN_PATH);
|
|
4656
|
+
if (gap && !this.closer.signal.aborted) this.logger.warn(this.gapMessage(gap, failure));
|
|
4657
|
+
const checksums = fileChecksums(answer?.json.files ?? null, new Map(state.testChecksums));
|
|
4658
|
+
const files = await this.verifyChecksums(withTestCoverage(answer?.files ?? [], state.testCoverage), checksums);
|
|
4659
|
+
let out = files.files;
|
|
4660
|
+
try {
|
|
4661
|
+
if (this.options.blobs) out = await this.options.blobs.withBlobs(out);
|
|
4662
|
+
} catch {
|
|
4663
|
+
}
|
|
4664
|
+
return { files: out, gap };
|
|
4665
|
+
}
|
|
4666
|
+
/**
|
|
4667
|
+
* The gap of an origin whose run-level coverage `instance` answered: a
|
|
4668
|
+
* restart when it, or a collection answer, names another process than the
|
|
4669
|
+
* run's first request ids; null when the backend names no process.
|
|
4670
|
+
*/
|
|
4671
|
+
restartGap(origin, state, instance) {
|
|
4672
|
+
if (!instance) return null;
|
|
4673
|
+
const keys = [...state.processKeys];
|
|
4674
|
+
const others = [...state.instances.values()].filter((known) => known.id !== instance.id);
|
|
4675
|
+
if (keys.every((key) => key === instance.id) && others.length === 0) return null;
|
|
4676
|
+
const at = keys[0] === instance.id ? others[others.length - 1]?.startedAt ?? null : instance.startedAt;
|
|
4677
|
+
return { origin, reason: "restarted", ...at ? { at } : {} };
|
|
4678
|
+
}
|
|
4679
|
+
gapMessage(gap, failure) {
|
|
4680
|
+
if (gap.reason === "unreachable") {
|
|
4681
|
+
return `Could not fetch the run's server coverage from ${gap.origin} (${failure}): the run's server coverage is marked incomplete, and keeps the lines the tests' coverage named.`;
|
|
4682
|
+
}
|
|
4683
|
+
const when = gap.at ? ` at about ${gap.at}` : "";
|
|
4684
|
+
return `The backend at ${gap.origin} restarted during the run${when}: the run's server coverage is marked incomplete. It keeps the lines the tests' coverage named; what the earlier process ran outside a request is lost.`;
|
|
4532
4685
|
}
|
|
4533
4686
|
/** Abort every call in flight and fail every later one; the tests still waiting resolve incomplete. */
|
|
4534
4687
|
close() {
|
|
@@ -4550,7 +4703,11 @@ var ServerCoverageCollector = class {
|
|
|
4550
4703
|
timer: null,
|
|
4551
4704
|
base: null,
|
|
4552
4705
|
limit: createLimiter(this.limits.concurrency),
|
|
4553
|
-
warned: false
|
|
4706
|
+
warned: false,
|
|
4707
|
+
processKeys: /* @__PURE__ */ new Set(),
|
|
4708
|
+
instances: /* @__PURE__ */ new Map(),
|
|
4709
|
+
testCoverage: null,
|
|
4710
|
+
testChecksums: /* @__PURE__ */ new Map()
|
|
4554
4711
|
};
|
|
4555
4712
|
this.originStates.set(origin, state);
|
|
4556
4713
|
}
|
|
@@ -4620,6 +4777,13 @@ var ServerCoverageCollector = class {
|
|
|
4620
4777
|
}
|
|
4621
4778
|
/** Send one call and hand each group's answer to its test. */
|
|
4622
4779
|
async send(origin, groups) {
|
|
4780
|
+
const state = this.stateOf(origin);
|
|
4781
|
+
for (const group of groups) {
|
|
4782
|
+
for (const id of group.ids) {
|
|
4783
|
+
const key = processKeyOf(id);
|
|
4784
|
+
if (key !== null) state.processKeys.add(key);
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4623
4787
|
let answer = null;
|
|
4624
4788
|
try {
|
|
4625
4789
|
const body = JSON.stringify({
|
|
@@ -4630,6 +4794,8 @@ var ServerCoverageCollector = class {
|
|
|
4630
4794
|
} catch (error) {
|
|
4631
4795
|
this.logger.debug(`Server coverage collection from ${origin} failed: ${errorMessage(error)}`);
|
|
4632
4796
|
}
|
|
4797
|
+
const instance = instanceOf(answer);
|
|
4798
|
+
if (instance && !state.instances.has(instance.id)) state.instances.set(instance.id, instance);
|
|
4633
4799
|
const answered = /* @__PURE__ */ new Map();
|
|
4634
4800
|
for (const group of answer?.groups ?? []) {
|
|
4635
4801
|
if (isRecord2(group) && typeof group.key === "string") answered.set(group.key, group);
|
|
@@ -4641,9 +4807,11 @@ var ServerCoverageCollector = class {
|
|
|
4641
4807
|
} else {
|
|
4642
4808
|
if (Array.isArray(result.missing) && result.missing.length > 0) group.entry.incomplete = true;
|
|
4643
4809
|
fileChecksums(result.files, group.entry.checksums);
|
|
4810
|
+
fileChecksums(result.files, state.testChecksums);
|
|
4644
4811
|
const capture = sanitizeCoverage({ v: 1, files: result.files });
|
|
4645
4812
|
if (capture) {
|
|
4646
4813
|
group.entry.captures.push(capture);
|
|
4814
|
+
state.testCoverage = mergeCoverage(state.testCoverage, capture);
|
|
4647
4815
|
if (capture.incomplete) group.entry.incomplete = true;
|
|
4648
4816
|
}
|
|
4649
4817
|
}
|
|
@@ -4771,14 +4939,14 @@ var ServerCoverageCollector = class {
|
|
|
4771
4939
|
// src/public/global-setup.ts
|
|
4772
4940
|
function createGlobalSetup(options, userSetup) {
|
|
4773
4941
|
return async function globalSetupFn(config) {
|
|
4774
|
-
const piwiReporterPath =
|
|
4942
|
+
const piwiReporterPath = path14.resolve(__dirname, "./index.js");
|
|
4775
4943
|
let inlineReporterOptions = {};
|
|
4776
4944
|
if (Array.isArray(config?.reporter)) {
|
|
4777
4945
|
for (const r of config.reporter) {
|
|
4778
4946
|
if (!Array.isArray(r) || typeof r[0] !== "string") continue;
|
|
4779
4947
|
const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
|
|
4780
4948
|
try {
|
|
4781
|
-
return
|
|
4949
|
+
return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
4782
4950
|
} catch {
|
|
4783
4951
|
return false;
|
|
4784
4952
|
}
|
|
@@ -4814,7 +4982,7 @@ function createGlobalSetup(options, userSetup) {
|
|
|
4814
4982
|
if (!Array.isArray(r) || typeof r[0] !== "string") return false;
|
|
4815
4983
|
if (r[0].toLowerCase().includes("piwi")) return true;
|
|
4816
4984
|
try {
|
|
4817
|
-
return
|
|
4985
|
+
return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
|
|
4818
4986
|
} catch {
|
|
4819
4987
|
return false;
|
|
4820
4988
|
}
|
|
@@ -4898,12 +5066,11 @@ function createGlobalSetup(options, userSetup) {
|
|
|
4898
5066
|
}
|
|
4899
5067
|
|
|
4900
5068
|
// src/public/config-wrapper.ts
|
|
4901
|
-
var
|
|
5069
|
+
var path15 = __toESM(require("path"));
|
|
4902
5070
|
var fs17 = __toESM(require("fs"));
|
|
4903
5071
|
|
|
4904
5072
|
// src/internal/probe/mode.ts
|
|
4905
5073
|
var fs15 = __toESM(require("fs"));
|
|
4906
|
-
var path14 = __toESM(require("path"));
|
|
4907
5074
|
|
|
4908
5075
|
// src/internal/probe/plan.ts
|
|
4909
5076
|
function probeRunMetadata(base = {}) {
|
|
@@ -4915,8 +5082,8 @@ function parseRouteKey(routeKey2) {
|
|
|
4915
5082
|
return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
|
|
4916
5083
|
}
|
|
4917
5084
|
function patternToRegex(pattern) {
|
|
4918
|
-
const
|
|
4919
|
-
const escaped =
|
|
5085
|
+
const path32 = pattern.split("?")[0] ?? pattern;
|
|
5086
|
+
const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
4920
5087
|
return new RegExp(`^${escaped}$`);
|
|
4921
5088
|
}
|
|
4922
5089
|
function suitePathEqual(a, b) {
|
|
@@ -4939,13 +5106,13 @@ function matchProbeItem(plan, test) {
|
|
|
4939
5106
|
function requestMatchesRoute(method, url, routeKey2) {
|
|
4940
5107
|
const { method: rm, pattern } = parseRouteKey(routeKey2);
|
|
4941
5108
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
4942
|
-
let
|
|
5109
|
+
let path32;
|
|
4943
5110
|
try {
|
|
4944
|
-
|
|
5111
|
+
path32 = new URL(url).pathname;
|
|
4945
5112
|
} catch {
|
|
4946
|
-
|
|
5113
|
+
path32 = url.split("?")[0] ?? url;
|
|
4947
5114
|
}
|
|
4948
|
-
return patternToRegex(pattern).test(
|
|
5115
|
+
return patternToRegex(pattern).test(path32);
|
|
4949
5116
|
}
|
|
4950
5117
|
function createProbeState() {
|
|
4951
5118
|
return { navigated: false, matches: 0 };
|
|
@@ -4967,10 +5134,10 @@ function isProbeMode() {
|
|
|
4967
5134
|
function loadPlan() {
|
|
4968
5135
|
if (planCache !== void 0) return planCache;
|
|
4969
5136
|
planCache = null;
|
|
4970
|
-
const
|
|
4971
|
-
if (
|
|
5137
|
+
const path32 = process.env[PIWI_PROBE_ENV.plan];
|
|
5138
|
+
if (path32) {
|
|
4972
5139
|
try {
|
|
4973
|
-
planCache = JSON.parse(fs15.readFileSync(
|
|
5140
|
+
planCache = JSON.parse(fs15.readFileSync(path32, "utf8"));
|
|
4974
5141
|
} catch {
|
|
4975
5142
|
planCache = null;
|
|
4976
5143
|
}
|
|
@@ -4980,7 +5147,7 @@ function loadPlan() {
|
|
|
4980
5147
|
function probeItemForTest(test) {
|
|
4981
5148
|
const plan = loadPlan();
|
|
4982
5149
|
if (!plan) return null;
|
|
4983
|
-
const filePath2 = test.file ?
|
|
5150
|
+
const filePath2 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
|
|
4984
5151
|
const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
|
|
4985
5152
|
return matchProbeItem(plan, { title: test.title, filePath: filePath2, suitePath });
|
|
4986
5153
|
}
|
|
@@ -4994,10 +5161,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
4994
5161
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
4995
5162
|
}
|
|
4996
5163
|
function recordProbeOutcome(line) {
|
|
4997
|
-
const
|
|
4998
|
-
if (!
|
|
5164
|
+
const path32 = process.env[PIWI_PROBE_ENV.results];
|
|
5165
|
+
if (!path32) return;
|
|
4999
5166
|
try {
|
|
5000
|
-
fs15.appendFileSync(
|
|
5167
|
+
fs15.appendFileSync(path32, `${JSON.stringify(line)}
|
|
5001
5168
|
`);
|
|
5002
5169
|
} catch {
|
|
5003
5170
|
}
|
|
@@ -5005,7 +5172,6 @@ function recordProbeOutcome(line) {
|
|
|
5005
5172
|
|
|
5006
5173
|
// src/internal/flake/mode.ts
|
|
5007
5174
|
var fs16 = __toESM(require("fs"));
|
|
5008
|
-
var path15 = __toESM(require("path"));
|
|
5009
5175
|
|
|
5010
5176
|
// ../core/src/error-signature.ts
|
|
5011
5177
|
var UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
@@ -5525,12 +5691,12 @@ function flakeRunMetadata(stamp, base = {}) {
|
|
|
5525
5691
|
[RUN_ORIGIN_METADATA_KEY]: ref === void 0 ? { kind: "flake-lab" } : { kind: "flake-lab", ref }
|
|
5526
5692
|
};
|
|
5527
5693
|
}
|
|
5528
|
-
function
|
|
5529
|
-
return file ?
|
|
5694
|
+
function relativeTestFile(file, base = process.cwd()) {
|
|
5695
|
+
return file ? relativeTestPath(file, base) : null;
|
|
5530
5696
|
}
|
|
5531
5697
|
function flakeRoleForTest(plan, test) {
|
|
5532
5698
|
const running = {
|
|
5533
|
-
file:
|
|
5699
|
+
file: relativeTestFile(test.file, test.base),
|
|
5534
5700
|
title: test.title,
|
|
5535
5701
|
suite: test.titlePath ? test.titlePath.slice(1, -1) : void 0
|
|
5536
5702
|
};
|
|
@@ -5646,7 +5812,7 @@ function flakeResultLine(plan, attempt) {
|
|
|
5646
5812
|
experimentId: plan.experimentId,
|
|
5647
5813
|
armId: plan.arm.id,
|
|
5648
5814
|
role: attempt.role,
|
|
5649
|
-
file:
|
|
5815
|
+
file: relativeTestFile(attempt.file, attempt.base),
|
|
5650
5816
|
title: attempt.title,
|
|
5651
5817
|
project: attempt.project,
|
|
5652
5818
|
browserName: attempt.browserName,
|
|
@@ -5737,9 +5903,9 @@ function injectReporter(reporter, piwiOptions) {
|
|
|
5737
5903
|
}
|
|
5738
5904
|
function resolveSetupModule() {
|
|
5739
5905
|
const candidates = [
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5906
|
+
path15.join(__dirname, "global-setup-module.js"),
|
|
5907
|
+
path15.join(__dirname, "..", "global-setup-module.js"),
|
|
5908
|
+
path15.join(__dirname, "..", "global-setup-module.ts")
|
|
5743
5909
|
];
|
|
5744
5910
|
return candidates.find((candidate) => fs17.existsSync(candidate)) ?? candidates[0];
|
|
5745
5911
|
}
|
|
@@ -5949,9 +6115,6 @@ function collectTestMetadata(annotations) {
|
|
|
5949
6115
|
return parseTestMetadata(annotations);
|
|
5950
6116
|
}
|
|
5951
6117
|
|
|
5952
|
-
// src/internal/collect/error-text.ts
|
|
5953
|
-
var path17 = __toESM(require("path"));
|
|
5954
|
-
|
|
5955
6118
|
// ../core/src/error-text.ts
|
|
5956
6119
|
function joinErrorMessages(errors) {
|
|
5957
6120
|
if (!errors || errors.length === 0) return null;
|
|
@@ -5968,24 +6131,26 @@ function joinErrorMessages(errors) {
|
|
|
5968
6131
|
return parts.join("\n---\n") || null;
|
|
5969
6132
|
}
|
|
5970
6133
|
function appendErrorLocation(text, location) {
|
|
5971
|
-
if (!location?.file || /\n\
|
|
6134
|
+
if (!location?.file || /\n[ \t]+at\s/.test(text)) return text;
|
|
5972
6135
|
return `${text}
|
|
5973
6136
|
at ${location.file}:${location.line}:${location.column}`;
|
|
5974
6137
|
}
|
|
5975
6138
|
|
|
5976
6139
|
// src/internal/collect/error-text.ts
|
|
5977
|
-
function buildErrorText(result) {
|
|
6140
|
+
function buildErrorText(result, base = process.cwd()) {
|
|
5978
6141
|
const text = joinErrorMessages(result.errors);
|
|
5979
6142
|
if (!text) return null;
|
|
5980
6143
|
const loc = result.error?.location;
|
|
5981
6144
|
if (!loc?.file) return text;
|
|
5982
|
-
|
|
5983
|
-
return appendErrorLocation(text, { file: rel, line: loc.line, column: loc.column });
|
|
6145
|
+
return appendErrorLocation(text, { file: relativeTestPath(loc.file, base), line: loc.line, column: loc.column });
|
|
5984
6146
|
}
|
|
5985
6147
|
|
|
6148
|
+
// src/internal/submit/run-submitter.ts
|
|
6149
|
+
var import_node_crypto2 = require("crypto");
|
|
6150
|
+
|
|
5986
6151
|
// src/internal/support/ci-output.ts
|
|
5987
6152
|
var fs18 = __toESM(require("fs"));
|
|
5988
|
-
var
|
|
6153
|
+
var path16 = __toESM(require("path"));
|
|
5989
6154
|
var SUMMARY_MAX_FAILURES = 20;
|
|
5990
6155
|
function emitRunOutputs(output, logger, outputFile, env = process.env) {
|
|
5991
6156
|
logger.info(`View run: ${output.runUrl}`);
|
|
@@ -5995,7 +6160,7 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
|
|
|
5995
6160
|
}
|
|
5996
6161
|
function writeOutputFile(file, output, logger) {
|
|
5997
6162
|
try {
|
|
5998
|
-
const dir =
|
|
6163
|
+
const dir = path16.dirname(file);
|
|
5999
6164
|
if (dir && dir !== ".") fs18.mkdirSync(dir, { recursive: true });
|
|
6000
6165
|
fs18.writeFileSync(
|
|
6001
6166
|
file,
|
|
@@ -6107,8 +6272,8 @@ function ciBuildUrlFromMetadata(metadata) {
|
|
|
6107
6272
|
|
|
6108
6273
|
// src/internal/submit/run-submitter.ts
|
|
6109
6274
|
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
6110
|
-
function markSubmitFallback(run,
|
|
6111
|
-
run.metadata.ingestHealth = { submitFallback: { path:
|
|
6275
|
+
function markSubmitFallback(run, path32, reason) {
|
|
6276
|
+
run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
|
|
6112
6277
|
}
|
|
6113
6278
|
var RunSubmitter = class {
|
|
6114
6279
|
/**
|
|
@@ -6126,6 +6291,8 @@ var RunSubmitter = class {
|
|
|
6126
6291
|
this.streamManager = streamManager;
|
|
6127
6292
|
this.logger = logger;
|
|
6128
6293
|
this.failureLinks = failureLinks;
|
|
6294
|
+
/** The key every batch delivery of this run carries, so the dashboard stores the run once. */
|
|
6295
|
+
this.deliveryKey = (0, import_node_crypto2.randomUUID)();
|
|
6129
6296
|
}
|
|
6130
6297
|
/**
|
|
6131
6298
|
* Run the fallback ladder for a completed test run, within the
|
|
@@ -6181,8 +6348,9 @@ var RunSubmitter = class {
|
|
|
6181
6348
|
auth = sm?.auth ?? await this.httpClient.resolveAuth(run.options);
|
|
6182
6349
|
} catch (error) {
|
|
6183
6350
|
this.logger.error(`Authentication failed: ${errorMessage(error)}`);
|
|
6351
|
+
this.logRecoverySaved();
|
|
6184
6352
|
this.saveRecovery(this.buildRunPayload(run, overallStatus, duration));
|
|
6185
|
-
|
|
6353
|
+
return null;
|
|
6186
6354
|
}
|
|
6187
6355
|
if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
|
|
6188
6356
|
let outcome = { done: false, output: null };
|
|
@@ -6256,7 +6424,8 @@ var RunSubmitter = class {
|
|
|
6256
6424
|
shardTotal: run.shardInfo?.total,
|
|
6257
6425
|
isFullRun: run.isFullRun,
|
|
6258
6426
|
filterDetails: run.filterDetails,
|
|
6259
|
-
resourceReport: run.resourceReport ?? null
|
|
6427
|
+
resourceReport: run.resourceReport ?? null,
|
|
6428
|
+
deliveryKey: this.deliveryKey
|
|
6260
6429
|
};
|
|
6261
6430
|
}
|
|
6262
6431
|
async tryFinishStreaming(run, overallStatus, duration, auth) {
|
|
@@ -6322,11 +6491,7 @@ var RunSubmitter = class {
|
|
|
6322
6491
|
this.dropLocalCopies();
|
|
6323
6492
|
return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
|
|
6324
6493
|
} catch (error) {
|
|
6325
|
-
if (error instanceof HttpError && error.status === 401 && !auth)
|
|
6326
|
-
this.logAuthRequired(run.options.serverUrl);
|
|
6327
|
-
this.saveRecovery(payload);
|
|
6328
|
-
throw error;
|
|
6329
|
-
}
|
|
6494
|
+
if (error instanceof HttpError && error.status === 401 && !auth) return this.stopForMissingAuth(run, payload);
|
|
6330
6495
|
this.logger.warn(`Failed to upload with files: ${errorMessage(error)}`);
|
|
6331
6496
|
this.logger.info("Falling back to JSON upload...");
|
|
6332
6497
|
return { done: false, output: null };
|
|
@@ -6339,22 +6504,20 @@ var RunSubmitter = class {
|
|
|
6339
6504
|
this.dropLocalCopies();
|
|
6340
6505
|
return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
|
|
6341
6506
|
} catch (error) {
|
|
6342
|
-
if (error instanceof HttpError && error.status === 401 && !auth)
|
|
6343
|
-
this.logAuthRequired(run.options.serverUrl);
|
|
6344
|
-
this.saveRecovery(payload);
|
|
6345
|
-
throw error;
|
|
6346
|
-
}
|
|
6507
|
+
if (error instanceof HttpError && error.status === 401 && !auth) return this.stopForMissingAuth(run, payload);
|
|
6347
6508
|
this.logger.error(`All upload methods failed: ${errorMessage(error)}`);
|
|
6348
|
-
this.
|
|
6349
|
-
`
|
|
6509
|
+
this.logRecoverySaved(
|
|
6510
|
+
`If this keeps happening, check that serverUrl (${run.options.serverUrl ?? "not set"}) is correct and reachable.`
|
|
6350
6511
|
);
|
|
6351
6512
|
this.saveRecovery(payload);
|
|
6352
6513
|
return { done: true, output: null };
|
|
6353
6514
|
}
|
|
6354
6515
|
}
|
|
6355
|
-
/**
|
|
6516
|
+
/**
|
|
6517
|
+
* The run reached the server: drop its buffered live events. A recovery file
|
|
6518
|
+
* on disk is an earlier run's, deleted only once its own replay succeeds.
|
|
6519
|
+
*/
|
|
6356
6520
|
dropLocalCopies() {
|
|
6357
|
-
this.recovery.clear();
|
|
6358
6521
|
this.streamManager?.discardBuffered();
|
|
6359
6522
|
}
|
|
6360
6523
|
/**
|
|
@@ -6364,22 +6527,35 @@ var RunSubmitter = class {
|
|
|
6364
6527
|
saveRecovery(payload) {
|
|
6365
6528
|
this.recovery.save(serializeRun(payload, { includeTestCases: true }));
|
|
6366
6529
|
}
|
|
6367
|
-
/**
|
|
6368
|
-
|
|
6530
|
+
/**
|
|
6531
|
+
* The dashboard asked for a credential the reporter has none of: no other
|
|
6532
|
+
* rung can succeed, so the ladder stops with the run saved for the next one.
|
|
6533
|
+
* The test run's own result stays what Playwright made it.
|
|
6534
|
+
*/
|
|
6535
|
+
stopForMissingAuth(run, payload) {
|
|
6369
6536
|
this.logger.error(
|
|
6370
|
-
`Authentication is required by ${serverUrl ?? "the dashboard"} but no credentials were configured. Create an API key (Settings \u2192 Users on the dashboard) and set the reporter's \`apiKey\` option or the PIWI_API_KEY environment variable.`
|
|
6537
|
+
`Authentication is required by ${run.options.serverUrl ?? "the dashboard"} but no credentials were configured. Create an API key (Settings \u2192 Users on the dashboard) and set the reporter's \`apiKey\` option or the PIWI_API_KEY environment variable.`
|
|
6538
|
+
);
|
|
6539
|
+
this.logRecoverySaved();
|
|
6540
|
+
this.saveRecovery(payload);
|
|
6541
|
+
return { done: true, output: null };
|
|
6542
|
+
}
|
|
6543
|
+
/** Say where the run went when the dashboard did not take it, with an optional hint. */
|
|
6544
|
+
logRecoverySaved(hint) {
|
|
6545
|
+
this.logger.info(
|
|
6546
|
+
`Saved a local recovery copy \u2014 it will be uploaded automatically on your next test run.${hint ? ` ${hint}` : ""}`
|
|
6371
6547
|
);
|
|
6372
6548
|
}
|
|
6373
6549
|
};
|
|
6374
6550
|
|
|
6375
6551
|
// src/internal/capture/resource-ledger.ts
|
|
6376
6552
|
var fs20 = __toESM(require("fs"));
|
|
6377
|
-
var
|
|
6553
|
+
var path19 = __toESM(require("path"));
|
|
6378
6554
|
var import_node_module2 = require("module");
|
|
6379
6555
|
|
|
6380
6556
|
// src/internal/capture/quiet-capture.ts
|
|
6381
6557
|
var import_node_module = require("module");
|
|
6382
|
-
var
|
|
6558
|
+
var path17 = __toESM(require("path"));
|
|
6383
6559
|
var BOXED_PREFIX_EXPORTS = [
|
|
6384
6560
|
[
|
|
6385
6561
|
"playwright-core/lib/coreBundle",
|
|
@@ -6416,7 +6592,7 @@ function resolveBoxing() {
|
|
|
6416
6592
|
if (typeof set !== "function") continue;
|
|
6417
6593
|
return {
|
|
6418
6594
|
set,
|
|
6419
|
-
prefixes: [
|
|
6595
|
+
prefixes: [path17.dirname(playwrightPackageJson), path17.join(root, "dist") + path17.sep]
|
|
6420
6596
|
};
|
|
6421
6597
|
}
|
|
6422
6598
|
} catch {
|
|
@@ -6449,7 +6625,7 @@ var import_node_perf_hooks = require("perf_hooks");
|
|
|
6449
6625
|
|
|
6450
6626
|
// src/internal/support/system-readers.ts
|
|
6451
6627
|
var fs19 = __toESM(require("fs"));
|
|
6452
|
-
var
|
|
6628
|
+
var path18 = __toESM(require("path"));
|
|
6453
6629
|
var CLK_TCK = 100;
|
|
6454
6630
|
function readText(file) {
|
|
6455
6631
|
try {
|
|
@@ -6587,7 +6763,7 @@ var ProcFs = class {
|
|
|
6587
6763
|
}
|
|
6588
6764
|
/** Whether this looks like a Linux `/proc` at all. */
|
|
6589
6765
|
available() {
|
|
6590
|
-
return fs19.existsSync(
|
|
6766
|
+
return fs19.existsSync(path18.join(this.root, "stat"));
|
|
6591
6767
|
}
|
|
6592
6768
|
/** Every process, keyed by pid. */
|
|
6593
6769
|
table() {
|
|
@@ -6600,32 +6776,32 @@ var ProcFs = class {
|
|
|
6600
6776
|
}
|
|
6601
6777
|
for (const name of names) {
|
|
6602
6778
|
if (!/^\d+$/.test(name)) continue;
|
|
6603
|
-
const text = readText(
|
|
6779
|
+
const text = readText(path18.join(this.root, name, "stat"));
|
|
6604
6780
|
const stat = text ? parseProcStat(Number(name), text) : null;
|
|
6605
6781
|
if (stat) out.set(stat.pid, stat);
|
|
6606
6782
|
}
|
|
6607
6783
|
return out;
|
|
6608
6784
|
}
|
|
6609
6785
|
stat(pid) {
|
|
6610
|
-
const text = readText(
|
|
6786
|
+
const text = readText(path18.join(this.root, String(pid), "stat"));
|
|
6611
6787
|
return text ? parseProcStat(pid, text) : null;
|
|
6612
6788
|
}
|
|
6613
6789
|
runWaitMs(pid) {
|
|
6614
|
-
const text = readText(
|
|
6790
|
+
const text = readText(path18.join(this.root, String(pid), "schedstat"));
|
|
6615
6791
|
return text ? parseSchedstat(text) : null;
|
|
6616
6792
|
}
|
|
6617
6793
|
cmdline(pid) {
|
|
6618
|
-
return (readText(
|
|
6794
|
+
return (readText(path18.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
|
|
6619
6795
|
}
|
|
6620
6796
|
/** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
|
|
6621
6797
|
statusKb(pid, key) {
|
|
6622
|
-
const text = readText(
|
|
6798
|
+
const text = readText(path18.join(this.root, String(pid), "status"));
|
|
6623
6799
|
return text ? parseKbField(text, key) : null;
|
|
6624
6800
|
}
|
|
6625
6801
|
/** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
|
|
6626
6802
|
async pssKb(pid) {
|
|
6627
6803
|
try {
|
|
6628
|
-
return parseKbField(await fs19.promises.readFile(
|
|
6804
|
+
return parseKbField(await fs19.promises.readFile(path18.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
|
|
6629
6805
|
} catch {
|
|
6630
6806
|
return null;
|
|
6631
6807
|
}
|
|
@@ -6633,29 +6809,29 @@ var ProcFs = class {
|
|
|
6633
6809
|
/** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
|
|
6634
6810
|
resetPeakRss(pid) {
|
|
6635
6811
|
try {
|
|
6636
|
-
fs19.writeFileSync(
|
|
6812
|
+
fs19.writeFileSync(path18.join(this.root, String(pid), "clear_refs"), "5");
|
|
6637
6813
|
return true;
|
|
6638
6814
|
} catch {
|
|
6639
6815
|
return false;
|
|
6640
6816
|
}
|
|
6641
6817
|
}
|
|
6642
6818
|
cpuTicks() {
|
|
6643
|
-
const text = readText(
|
|
6819
|
+
const text = readText(path18.join(this.root, "stat"));
|
|
6644
6820
|
return text ? parseCpuTicks(text) : null;
|
|
6645
6821
|
}
|
|
6646
6822
|
/** Total stall time of a resource's pressure file, in µs. */
|
|
6647
6823
|
pressureUs(resource) {
|
|
6648
|
-
const text = readText(
|
|
6824
|
+
const text = readText(path18.join(this.root, "pressure", resource));
|
|
6649
6825
|
return text ? parsePsiTotal(text) : null;
|
|
6650
6826
|
}
|
|
6651
6827
|
memAvailableKb() {
|
|
6652
|
-
const text = readText(
|
|
6828
|
+
const text = readText(path18.join(this.root, "meminfo"));
|
|
6653
6829
|
return text ? parseKbField(text, "MemAvailable") : null;
|
|
6654
6830
|
}
|
|
6655
6831
|
/** Open file descriptors of a process. */
|
|
6656
6832
|
fdCount(pid) {
|
|
6657
6833
|
try {
|
|
6658
|
-
return fs19.readdirSync(
|
|
6834
|
+
return fs19.readdirSync(path18.join(this.root, String(pid), "fd")).length;
|
|
6659
6835
|
} catch {
|
|
6660
6836
|
return null;
|
|
6661
6837
|
}
|
|
@@ -6676,7 +6852,7 @@ var CgroupFs = class {
|
|
|
6676
6852
|
}
|
|
6677
6853
|
/** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
|
|
6678
6854
|
dirs() {
|
|
6679
|
-
const text = readText(
|
|
6855
|
+
const text = readText(path18.join(this.procRoot, String(this.pid), "cgroup"));
|
|
6680
6856
|
if (!text) return null;
|
|
6681
6857
|
const v1 = /* @__PURE__ */ new Map();
|
|
6682
6858
|
let v2 = null;
|
|
@@ -6687,21 +6863,21 @@ var CgroupFs = class {
|
|
|
6687
6863
|
else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
|
|
6688
6864
|
}
|
|
6689
6865
|
const pick = (mount, sub) => {
|
|
6690
|
-
const nested =
|
|
6866
|
+
const nested = path18.join(mount, sub);
|
|
6691
6867
|
if (fs19.existsSync(nested)) return nested;
|
|
6692
6868
|
return fs19.existsSync(mount) ? mount : null;
|
|
6693
6869
|
};
|
|
6694
6870
|
if (v1.has("memory") || v1.has("cpu")) {
|
|
6695
6871
|
const memoryPath = v1.get("memory");
|
|
6696
6872
|
const cpuPath = v1.get("cpu");
|
|
6697
|
-
const cpuMount = ["cpu,cpuacct", "cpu"].map((name) =>
|
|
6873
|
+
const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path18.join(this.cgroupRoot, name)).find((dir) => fs19.existsSync(dir));
|
|
6698
6874
|
return {
|
|
6699
6875
|
version: 1,
|
|
6700
|
-
memory: memoryPath !== void 0 ? pick(
|
|
6876
|
+
memory: memoryPath !== void 0 ? pick(path18.join(this.cgroupRoot, "memory"), memoryPath) : null,
|
|
6701
6877
|
cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
|
|
6702
6878
|
};
|
|
6703
6879
|
}
|
|
6704
|
-
if (v2 !== null && fs19.existsSync(
|
|
6880
|
+
if (v2 !== null && fs19.existsSync(path18.join(this.cgroupRoot, "cgroup.controllers"))) {
|
|
6705
6881
|
const dir = pick(this.cgroupRoot, v2);
|
|
6706
6882
|
return { version: 2, memory: dir, cpu: dir };
|
|
6707
6883
|
}
|
|
@@ -6710,7 +6886,7 @@ var CgroupFs = class {
|
|
|
6710
6886
|
read() {
|
|
6711
6887
|
const dirs = this.dirs();
|
|
6712
6888
|
if (!dirs) return null;
|
|
6713
|
-
const file = (dir, name) => dir ? readText(
|
|
6889
|
+
const file = (dir, name) => dir ? readText(path18.join(dir, name)) : null;
|
|
6714
6890
|
if (dirs.version === 2) {
|
|
6715
6891
|
const max = file(dirs.memory, "memory.max");
|
|
6716
6892
|
const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
|
|
@@ -6896,10 +7072,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
|
|
|
6896
7072
|
var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
|
|
6897
7073
|
var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
|
|
6898
7074
|
function relativePath(file) {
|
|
6899
|
-
return
|
|
7075
|
+
return path19.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path19.sep).join("/");
|
|
6900
7076
|
}
|
|
6901
7077
|
function isLibraryFile(file) {
|
|
6902
|
-
return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${
|
|
7078
|
+
return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path19.sep}node_modules${path19.sep}`) || file.includes("/node_modules/") || file === __filename;
|
|
6903
7079
|
}
|
|
6904
7080
|
function userSite(stack) {
|
|
6905
7081
|
for (const line of (stack ?? "").split("\n").slice(1)) {
|
|
@@ -6947,7 +7123,7 @@ function playwrightTestInfoSource() {
|
|
|
6947
7123
|
try {
|
|
6948
7124
|
const packageJson = resolvePlaywrightPackageJson();
|
|
6949
7125
|
if (!packageJson) return () => null;
|
|
6950
|
-
const globals = (0, import_node_module2.createRequire)(packageJson)(
|
|
7126
|
+
const globals = (0, import_node_module2.createRequire)(packageJson)(path19.join(path19.dirname(packageJson), "lib", "globals.js"));
|
|
6951
7127
|
const read = globals.currentTestInfo;
|
|
6952
7128
|
if (typeof read !== "function") return () => null;
|
|
6953
7129
|
return () => {
|
|
@@ -7436,7 +7612,7 @@ async function recordResourceCensus(testInfo) {
|
|
|
7436
7612
|
}
|
|
7437
7613
|
|
|
7438
7614
|
// src/internal/collect/resource-verdicts.ts
|
|
7439
|
-
var
|
|
7615
|
+
var path20 = __toESM(require("path"));
|
|
7440
7616
|
var OPEN_STEP_TITLES = {
|
|
7441
7617
|
"Create context": "context",
|
|
7442
7618
|
"Launch persistent context": "context",
|
|
@@ -7468,7 +7644,7 @@ function userFixturesOf(steps) {
|
|
|
7468
7644
|
const walk = (list) => {
|
|
7469
7645
|
for (const step of list) {
|
|
7470
7646
|
const file = step.location?.file;
|
|
7471
|
-
if (step.category === "fixture" && file && !file.includes(`${
|
|
7647
|
+
if (step.category === "fixture" && file && !file.includes(`${path20.sep}node_modules${path20.sep}`)) {
|
|
7472
7648
|
const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
|
|
7473
7649
|
if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
|
|
7474
7650
|
}
|
|
@@ -7501,7 +7677,7 @@ function tallyLifecycleSteps(file, steps) {
|
|
|
7501
7677
|
return tally;
|
|
7502
7678
|
}
|
|
7503
7679
|
function relativeFile(file) {
|
|
7504
|
-
return
|
|
7680
|
+
return path20.relative(process.cwd(), file).split(path20.sep).join("/");
|
|
7505
7681
|
}
|
|
7506
7682
|
function buildResourceReport(input) {
|
|
7507
7683
|
const findings = [];
|
|
@@ -7861,7 +8037,7 @@ function formatResourceSummary(report, leakCheck, maxLines = 10) {
|
|
|
7861
8037
|
// src/internal/collect/process-sampler.ts
|
|
7862
8038
|
var fs21 = __toESM(require("fs"));
|
|
7863
8039
|
var os8 = __toESM(require("os"));
|
|
7864
|
-
var
|
|
8040
|
+
var path21 = __toESM(require("path"));
|
|
7865
8041
|
var import_node_child_process2 = require("child_process");
|
|
7866
8042
|
var WALK_FILE_CAP = 1e4;
|
|
7867
8043
|
var SERIES_CAP = 3600;
|
|
@@ -7926,13 +8102,13 @@ function defaultWindowsQuery() {
|
|
|
7926
8102
|
});
|
|
7927
8103
|
}
|
|
7928
8104
|
function freeBytes(dir) {
|
|
7929
|
-
let current =
|
|
8105
|
+
let current = path21.resolve(dir);
|
|
7930
8106
|
for (; ; ) {
|
|
7931
8107
|
try {
|
|
7932
8108
|
const stats = fs21.statfsSync(current);
|
|
7933
8109
|
return stats.bavail * stats.bsize;
|
|
7934
8110
|
} catch {
|
|
7935
|
-
const parent =
|
|
8111
|
+
const parent = path21.dirname(current);
|
|
7936
8112
|
if (parent === current) return null;
|
|
7937
8113
|
current = parent;
|
|
7938
8114
|
}
|
|
@@ -7948,7 +8124,7 @@ async function dirBytes(dir, budget) {
|
|
|
7948
8124
|
}
|
|
7949
8125
|
for (const entry of entries) {
|
|
7950
8126
|
if (budget.files <= 0) return bytes;
|
|
7951
|
-
const full =
|
|
8127
|
+
const full = path21.join(dir, entry.name);
|
|
7952
8128
|
if (entry.isDirectory()) bytes += await dirBytes(full, budget);
|
|
7953
8129
|
else if (entry.isFile()) {
|
|
7954
8130
|
budget.files--;
|
|
@@ -7993,7 +8169,7 @@ var RunSampler = class {
|
|
|
7993
8169
|
this.platform = options.platform ?? process.platform;
|
|
7994
8170
|
this.proc = new ProcFs(options.procRoot);
|
|
7995
8171
|
this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
|
|
7996
|
-
this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) =>
|
|
8172
|
+
this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path21.resolve(dir)))];
|
|
7997
8173
|
this.tmpDir = options.tmpDir ?? os8.tmpdir();
|
|
7998
8174
|
this.intervalMs = options.intervalMs ?? 1e3;
|
|
7999
8175
|
this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
|
|
@@ -8195,7 +8371,7 @@ var RunSampler = class {
|
|
|
8195
8371
|
} catch {
|
|
8196
8372
|
return;
|
|
8197
8373
|
}
|
|
8198
|
-
this.noteTree(list, (proc) => roleOf(
|
|
8374
|
+
this.noteTree(list, (proc) => roleOf(path21.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
|
|
8199
8375
|
}
|
|
8200
8376
|
async sampleWindowsTree(at) {
|
|
8201
8377
|
let list;
|
|
@@ -8251,7 +8427,7 @@ var RunSampler = class {
|
|
|
8251
8427
|
let bytes = 0;
|
|
8252
8428
|
for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
|
|
8253
8429
|
for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
|
|
8254
|
-
bytes += await dirBytes(
|
|
8430
|
+
bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
|
|
8255
8431
|
}
|
|
8256
8432
|
if (budget.files <= 0) this.peakInUseIsLowerBound = true;
|
|
8257
8433
|
this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
|
|
@@ -8268,7 +8444,7 @@ var RunSampler = class {
|
|
|
8268
8444
|
for (const name of names) {
|
|
8269
8445
|
if (!PLAYWRIGHT_TEMP.test(name)) continue;
|
|
8270
8446
|
try {
|
|
8271
|
-
if (keep((await fs21.promises.stat(
|
|
8447
|
+
if (keep((await fs21.promises.stat(path21.join(this.tmpDir, name))).mtimeMs)) out.push(name);
|
|
8272
8448
|
} catch {
|
|
8273
8449
|
}
|
|
8274
8450
|
}
|
|
@@ -8279,7 +8455,7 @@ var RunSampler = class {
|
|
|
8279
8455
|
const budget = { files: WALK_FILE_CAP };
|
|
8280
8456
|
let bytes = 0;
|
|
8281
8457
|
for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
|
|
8282
|
-
bytes += await dirBytes(
|
|
8458
|
+
bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
|
|
8283
8459
|
}
|
|
8284
8460
|
return bytes;
|
|
8285
8461
|
} catch {
|
|
@@ -8672,7 +8848,7 @@ function resourceTimeline(samples, censuses) {
|
|
|
8672
8848
|
let startedAt = hasSamples ? samples.startedAt : Infinity;
|
|
8673
8849
|
for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
|
|
8674
8850
|
const shift = hasSamples ? samples.startedAt - startedAt : 0;
|
|
8675
|
-
const
|
|
8851
|
+
const relative11 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
|
|
8676
8852
|
const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
|
|
8677
8853
|
const perWorker = Math.max(
|
|
8678
8854
|
60,
|
|
@@ -8680,17 +8856,17 @@ function resourceTimeline(samples, censuses) {
|
|
|
8680
8856
|
);
|
|
8681
8857
|
return {
|
|
8682
8858
|
startedAt,
|
|
8683
|
-
cpuPct: hasSamples ? downsamplePoints(
|
|
8684
|
-
memoryBytes: hasSamples ? downsamplePoints(
|
|
8859
|
+
cpuPct: hasSamples ? downsamplePoints(relative11(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
|
|
8860
|
+
memoryBytes: hasSamples ? downsamplePoints(relative11(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
|
|
8685
8861
|
pages: workers.map(({ worker, events }) => {
|
|
8686
|
-
const points =
|
|
8862
|
+
const points = relative11(events, -startedAt);
|
|
8687
8863
|
const last = points.pop();
|
|
8688
8864
|
return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
|
|
8689
8865
|
}),
|
|
8690
8866
|
workers: trees.map((tree) => ({
|
|
8691
8867
|
worker: tree.worker,
|
|
8692
|
-
cpuCores: downsamplePoints(
|
|
8693
|
-
memoryBytes: downsamplePoints(
|
|
8868
|
+
cpuCores: downsamplePoints(relative11(tree.cpuCores, shift), perWorker, "mean"),
|
|
8869
|
+
memoryBytes: downsamplePoints(relative11(tree.memoryBytes, shift), perWorker, "max")
|
|
8694
8870
|
}))
|
|
8695
8871
|
};
|
|
8696
8872
|
}
|
|
@@ -9252,7 +9428,7 @@ function browserFunctionsInWorker() {
|
|
|
9252
9428
|
function browserFunctionCoverage(files) {
|
|
9253
9429
|
return sanitizeCoverage({
|
|
9254
9430
|
v: 1,
|
|
9255
|
-
files: [...files].map(([
|
|
9431
|
+
files: [...files].map(([path32, functions]) => ({ path: path32, runtime: "browser", functions }))
|
|
9256
9432
|
});
|
|
9257
9433
|
}
|
|
9258
9434
|
function newKnownFunctions(known, sent) {
|
|
@@ -9265,19 +9441,19 @@ function newKnownFunctions(known, sent) {
|
|
|
9265
9441
|
budget = MAX_COVERAGE_RANGES;
|
|
9266
9442
|
};
|
|
9267
9443
|
for (const [raw, ranges] of known) {
|
|
9268
|
-
const
|
|
9269
|
-
if (
|
|
9270
|
-
let named = sent.get(
|
|
9444
|
+
const path32 = normalizeCoveragePath(raw);
|
|
9445
|
+
if (path32 === null) continue;
|
|
9446
|
+
let named = sent.get(path32);
|
|
9271
9447
|
let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
|
|
9272
9448
|
if (fresh.length === 0) continue;
|
|
9273
|
-
if (!named) sent.set(
|
|
9449
|
+
if (!named) sent.set(path32, named = /* @__PURE__ */ new Set());
|
|
9274
9450
|
for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
|
|
9275
9451
|
while (fresh.length > 0) {
|
|
9276
9452
|
if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
|
|
9277
9453
|
const part = fresh.slice(0, budget);
|
|
9278
9454
|
fresh = fresh.slice(part.length);
|
|
9279
9455
|
budget -= part.length;
|
|
9280
|
-
files.push({ path:
|
|
9456
|
+
files.push({ path: path32, allFunctions: part });
|
|
9281
9457
|
}
|
|
9282
9458
|
}
|
|
9283
9459
|
close();
|
|
@@ -9288,9 +9464,9 @@ function serverCoverageInWorker() {
|
|
|
9288
9464
|
}
|
|
9289
9465
|
|
|
9290
9466
|
// src/internal/capture/code-reach.ts
|
|
9291
|
-
var
|
|
9467
|
+
var crypto5 = __toESM(require("crypto"));
|
|
9292
9468
|
var fs22 = __toESM(require("fs"));
|
|
9293
|
-
var
|
|
9469
|
+
var path22 = __toESM(require("path"));
|
|
9294
9470
|
var import_node_child_process3 = require("child_process");
|
|
9295
9471
|
|
|
9296
9472
|
// ../core/src/source-map.ts
|
|
@@ -9434,14 +9610,6 @@ function decodeSourceMap(raw) {
|
|
|
9434
9610
|
return originalAt(line, column)?.source ?? null;
|
|
9435
9611
|
},
|
|
9436
9612
|
originalAt,
|
|
9437
|
-
mapsBetween(source, start, end) {
|
|
9438
|
-
let found = false;
|
|
9439
|
-
eachSegmentBetween(segments, start, end, (i) => {
|
|
9440
|
-
found = segments.sources[i] === source;
|
|
9441
|
-
return found;
|
|
9442
|
-
});
|
|
9443
|
-
return found;
|
|
9444
|
-
},
|
|
9445
9613
|
firstOriginalLineBetween(source, start, end) {
|
|
9446
9614
|
let first = null;
|
|
9447
9615
|
eachSegmentBetween(segments, start, end, (i) => {
|
|
@@ -9643,12 +9811,12 @@ function repositoryRoot(from) {
|
|
|
9643
9811
|
}).trim();
|
|
9644
9812
|
} catch {
|
|
9645
9813
|
}
|
|
9646
|
-
cachedRepoRoot = { from, root:
|
|
9814
|
+
cachedRepoRoot = { from, root: path22.resolve(root) };
|
|
9647
9815
|
return cachedRepoRoot.root;
|
|
9648
9816
|
}
|
|
9649
9817
|
function codeReachRoots(configDir, configured) {
|
|
9650
9818
|
const repoRoot = repositoryRoot(configDir);
|
|
9651
|
-
const resolve9 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) =>
|
|
9819
|
+
const resolve9 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path22.resolve(configDir, r));
|
|
9652
9820
|
const global = Array.isArray(configured) ? resolve9(configured) : resolve9(configured?.["*"]);
|
|
9653
9821
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
9654
9822
|
if (configured && !Array.isArray(configured)) {
|
|
@@ -9685,10 +9853,10 @@ function isFile(file) {
|
|
|
9685
9853
|
function resolveSourcePath(source, roots) {
|
|
9686
9854
|
const normalized = normalizeSourcePath(source);
|
|
9687
9855
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
9688
|
-
if (
|
|
9689
|
-
const
|
|
9856
|
+
if (path22.isAbsolute(normalized) && isFile(normalized)) return path22.resolve(normalized);
|
|
9857
|
+
const relative11 = normalized.replace(/^\/+/, "");
|
|
9690
9858
|
for (const root of roots) {
|
|
9691
|
-
const candidate =
|
|
9859
|
+
const candidate = path22.resolve(root, relative11);
|
|
9692
9860
|
if (isFile(candidate)) return candidate;
|
|
9693
9861
|
}
|
|
9694
9862
|
return null;
|
|
@@ -9720,10 +9888,10 @@ function fileForUrl(url, roots, bases) {
|
|
|
9720
9888
|
return file;
|
|
9721
9889
|
}
|
|
9722
9890
|
function relativeToRepo(file, repoRoot) {
|
|
9723
|
-
return
|
|
9891
|
+
return path22.relative(repoRoot, file).split(path22.sep).join("/");
|
|
9724
9892
|
}
|
|
9725
9893
|
function scriptVersion(entry) {
|
|
9726
|
-
return entry.hash ??
|
|
9894
|
+
return entry.hash ?? crypto5.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
9727
9895
|
}
|
|
9728
9896
|
async function loadMap(entry, fetchMap) {
|
|
9729
9897
|
const ref = entry.source ? sourceMappingUrl(entry.source) : null;
|
|
@@ -9777,10 +9945,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
9777
9945
|
const files = /* @__PURE__ */ new Map();
|
|
9778
9946
|
const known = /* @__PURE__ */ new Map();
|
|
9779
9947
|
const addTo = (into, file, functions) => {
|
|
9780
|
-
const
|
|
9781
|
-
const listed = into.get(
|
|
9948
|
+
const path32 = relativeToRepo(file, roots.repoRoot);
|
|
9949
|
+
const listed = into.get(path32);
|
|
9782
9950
|
if (listed) listed.push(...functions);
|
|
9783
|
-
else into.set(
|
|
9951
|
+
else into.set(path32, [...functions]);
|
|
9784
9952
|
};
|
|
9785
9953
|
const add = (file, functions) => addTo(files, file, functions);
|
|
9786
9954
|
const bases = viteBases(entries);
|
|
@@ -10044,11 +10212,11 @@ async function stopCodeReach(page) {
|
|
|
10044
10212
|
|
|
10045
10213
|
// src/internal/collect/blob-ids.ts
|
|
10046
10214
|
var import_node_child_process4 = require("child_process");
|
|
10047
|
-
var
|
|
10215
|
+
var crypto6 = __toESM(require("crypto"));
|
|
10048
10216
|
var fs23 = __toESM(require("fs"));
|
|
10049
|
-
var
|
|
10217
|
+
var path23 = __toESM(require("path"));
|
|
10050
10218
|
function gitBlobId(content) {
|
|
10051
|
-
return
|
|
10219
|
+
return crypto6.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
|
|
10052
10220
|
}
|
|
10053
10221
|
function namesLines(file) {
|
|
10054
10222
|
return Boolean(file.functions?.length || file.lines?.length || file.executable?.length || file.allFunctions?.length);
|
|
@@ -10113,14 +10281,14 @@ var BlobIds = class {
|
|
|
10113
10281
|
async read(relativePaths) {
|
|
10114
10282
|
let root;
|
|
10115
10283
|
try {
|
|
10116
|
-
root =
|
|
10284
|
+
root = path23.resolve(this.repoRoot());
|
|
10117
10285
|
} catch {
|
|
10118
10286
|
return relativePaths.map(() => null);
|
|
10119
10287
|
}
|
|
10120
10288
|
const files = await Promise.all(
|
|
10121
10289
|
relativePaths.map(async (relativePath2) => {
|
|
10122
|
-
const file =
|
|
10123
|
-
if (!file.startsWith(root +
|
|
10290
|
+
const file = path23.resolve(root, relativePath2);
|
|
10291
|
+
if (!file.startsWith(root + path23.sep)) return null;
|
|
10124
10292
|
const stat = await fs23.promises.stat(file).catch(() => null);
|
|
10125
10293
|
return stat?.isFile() ? file : null;
|
|
10126
10294
|
})
|
|
@@ -10129,7 +10297,7 @@ var BlobIds = class {
|
|
|
10129
10297
|
const fromGit = /* @__PURE__ */ new Map();
|
|
10130
10298
|
const ids = forGit.length > 0 ? await this.hashObjects(
|
|
10131
10299
|
root,
|
|
10132
|
-
forGit.map((file) =>
|
|
10300
|
+
forGit.map((file) => path23.relative(root, file))
|
|
10133
10301
|
) : null;
|
|
10134
10302
|
if (ids) forGit.forEach((file, i) => fromGit.set(file, ids[i]));
|
|
10135
10303
|
return Promise.all(
|
|
@@ -10398,17 +10566,26 @@ var CoverageCollection = class {
|
|
|
10398
10566
|
}
|
|
10399
10567
|
/**
|
|
10400
10568
|
* Send the run-level coverage of every origin, and the browser's, to the
|
|
10401
|
-
* dashboard run, in requests within its limits
|
|
10402
|
-
*
|
|
10569
|
+
* dashboard run, in requests within its limits; the first names the origins
|
|
10570
|
+
* whose server coverage is incomplete (`incomplete`), and is sent for them
|
|
10571
|
+
* alone when no file is. A failure is logged once, never thrown.
|
|
10403
10572
|
*/
|
|
10404
10573
|
async sendRunCoverage(httpClient, runId, auth) {
|
|
10405
10574
|
if (!this.collector && !this.browserBlobs) return;
|
|
10406
10575
|
this.fetchRunCoverage();
|
|
10407
|
-
const
|
|
10576
|
+
const runs = await Promise.all(this.runFiles.values());
|
|
10577
|
+
const server = runs.flatMap((result) => result?.files ?? []);
|
|
10578
|
+
const gaps = runs.flatMap((result) => result?.gap ? [result.gap] : []);
|
|
10408
10579
|
const files = [...server, ...await this.browserRunFiles()];
|
|
10409
|
-
|
|
10580
|
+
const chunks = chunkRunCoverage(files);
|
|
10581
|
+
if (chunks.length === 0 && gaps.length > 0) chunks.push([]);
|
|
10582
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
10410
10583
|
try {
|
|
10411
|
-
await httpClient.putJSON(
|
|
10584
|
+
await httpClient.putJSON(
|
|
10585
|
+
`/api/test-runs/${runId}/coverage`,
|
|
10586
|
+
{ v: 1, files: chunk, ...i === 0 && gaps.length > 0 ? { incomplete: gaps } : {} },
|
|
10587
|
+
auth
|
|
10588
|
+
);
|
|
10412
10589
|
} catch (error) {
|
|
10413
10590
|
this.logger.warn(`Could not send the run's coverage to the dashboard: ${errorMessage(error)}`);
|
|
10414
10591
|
return;
|
|
@@ -10425,11 +10602,11 @@ var CoverageCollection = class {
|
|
|
10425
10602
|
};
|
|
10426
10603
|
|
|
10427
10604
|
// src/public/reporter.ts
|
|
10428
|
-
function testLocation(test) {
|
|
10429
|
-
return `${testFile(test)}:${test.location.line}:${test.location.column}`;
|
|
10605
|
+
function testLocation(test, base) {
|
|
10606
|
+
return `${testFile(test, base)}:${test.location.line}:${test.location.column}`;
|
|
10430
10607
|
}
|
|
10431
|
-
function testFile(test) {
|
|
10432
|
-
return
|
|
10608
|
+
function testFile(test, base) {
|
|
10609
|
+
return relativeTestPath(test.location.file, base);
|
|
10433
10610
|
}
|
|
10434
10611
|
var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
10435
10612
|
constructor(rawOptions = {}) {
|
|
@@ -10449,6 +10626,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10449
10626
|
this.reportedTestIds = /* @__PURE__ */ new Set();
|
|
10450
10627
|
/** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
|
|
10451
10628
|
this.attemptsByTest = /* @__PURE__ */ new Map();
|
|
10629
|
+
/** The folder test files are named relative to: the Playwright config's, set in `onBegin`. */
|
|
10630
|
+
this.testPathBase = process.cwd();
|
|
10452
10631
|
/** Derived in `onBegin`, once the shard is known. */
|
|
10453
10632
|
this.instanceId = "";
|
|
10454
10633
|
this.shardInfo = null;
|
|
@@ -10492,7 +10671,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10492
10671
|
this.httpClient = new HttpClient(this.options.serverUrl ?? "http://localhost:3000", logger);
|
|
10493
10672
|
this.fileHandler = new FileHandler(logger);
|
|
10494
10673
|
this.uploader = new Uploader(this.httpClient, this.fileHandler, logger);
|
|
10495
|
-
this.recovery = new CrashRecovery(this.options.projectName, logger);
|
|
10674
|
+
this.recovery = new CrashRecovery(this.httpClient.baseUrl, this.options.projectName, logger);
|
|
10496
10675
|
this.metadataCollector = new MetadataCollector(logger);
|
|
10497
10676
|
this.failureLinks = new FailureLinks(this.httpClient.baseUrl, logger);
|
|
10498
10677
|
const streamBuffer = new StreamBuffer(this.options.projectName);
|
|
@@ -10523,6 +10702,15 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10523
10702
|
static {
|
|
10524
10703
|
this.createGlobalSetup = createGlobalSetup;
|
|
10525
10704
|
}
|
|
10705
|
+
/**
|
|
10706
|
+
* Playwright reporter hook: whether this reporter writes the run's progress to
|
|
10707
|
+
* the terminal. It does not (it prints only its own `[Piwi Dashboard]` lines),
|
|
10708
|
+
* so Playwright keeps its own terminal reporter when Piwi is the only one
|
|
10709
|
+
* configured.
|
|
10710
|
+
*/
|
|
10711
|
+
printsToStdio() {
|
|
10712
|
+
return false;
|
|
10713
|
+
}
|
|
10526
10714
|
/** Playwright reporter hook: called once at the start of the test run */
|
|
10527
10715
|
onBegin(config, suite) {
|
|
10528
10716
|
if (this.listMode) {
|
|
@@ -10537,10 +10725,11 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10537
10725
|
this.logger.info(`Using the desktop app running at ${this.options.serverUrl} (no serverUrl configured).`);
|
|
10538
10726
|
}
|
|
10539
10727
|
this.startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
10728
|
+
this.testPathBase = configDirFromConfig(config);
|
|
10540
10729
|
this.playwrightVersion = config.version;
|
|
10541
10730
|
this.maxFailures = config.maxFailures ?? 0;
|
|
10542
10731
|
if (this.options.captureResources !== false) {
|
|
10543
|
-
this.resourcesFile =
|
|
10732
|
+
this.resourcesFile = path24.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
|
|
10544
10733
|
process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
|
|
10545
10734
|
try {
|
|
10546
10735
|
this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
|
|
@@ -10642,7 +10831,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10642
10831
|
const beginEvent = {
|
|
10643
10832
|
type: "begin",
|
|
10644
10833
|
title: test.title,
|
|
10645
|
-
location: testLocation(test),
|
|
10834
|
+
location: testLocation(test, this.testPathBase),
|
|
10646
10835
|
workerIndex: workerIndexOf(result),
|
|
10647
10836
|
shardIndex: this.shardInfo?.current ?? null,
|
|
10648
10837
|
browser: this.metadataCollector.getBrowserConfig(test) || void 0,
|
|
@@ -10738,8 +10927,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10738
10927
|
this.attemptsByTest.set(test.id, attempts);
|
|
10739
10928
|
const testCase = {
|
|
10740
10929
|
type: "complete",
|
|
10930
|
+
testId: test.id,
|
|
10741
10931
|
title: test.title,
|
|
10742
|
-
location: testLocation(test),
|
|
10932
|
+
location: testLocation(test, this.testPathBase),
|
|
10743
10933
|
status,
|
|
10744
10934
|
duration: result.duration,
|
|
10745
10935
|
// Effective per-test timeout (reflects project config + describe-level
|
|
@@ -10747,7 +10937,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10747
10937
|
timeout: test.timeout ?? null,
|
|
10748
10938
|
// A `test.fail()` test that passed is now `failed` with no recorded error;
|
|
10749
10939
|
// Playwright reports the same line, so synthesize it.
|
|
10750
|
-
error: expectedFailureError(result.status, annotations) ?? buildErrorText(result),
|
|
10940
|
+
error: expectedFailureError(result.status, annotations) ?? buildErrorText(result, this.testPathBase),
|
|
10751
10941
|
retries: result.retry,
|
|
10752
10942
|
attempts: attempts.map((a) => ({ ...a })),
|
|
10753
10943
|
workerIndex: workerIndexOf(result),
|
|
@@ -10831,7 +11021,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10831
11021
|
if (isFailure && result.retry >= (test.retries ?? 0)) {
|
|
10832
11022
|
this.failureLinks.add({
|
|
10833
11023
|
title: test.title,
|
|
10834
|
-
file: testFile(test),
|
|
11024
|
+
file: testFile(test, this.testPathBase),
|
|
10835
11025
|
retry: result.retry,
|
|
10836
11026
|
browser: typeof testCase.browser?.projectName === "string" ? testCase.browser.projectName : null,
|
|
10837
11027
|
headline: failureHeadline(testCase.error, testCase.performanceMetrics?.steps)
|
|
@@ -10881,7 +11071,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10881
11071
|
return;
|
|
10882
11072
|
}
|
|
10883
11073
|
if (!result.steps?.length) return;
|
|
10884
|
-
const tally = tallyLifecycleSteps(testFile(test), result.steps);
|
|
11074
|
+
const tally = tallyLifecycleSteps(testFile(test, this.testPathBase), result.steps);
|
|
10885
11075
|
if (tally.opened.length > 0 || tally.closed.context > 0 || tally.closed.browser > 0)
|
|
10886
11076
|
this.lifecycleTallies.push(tally);
|
|
10887
11077
|
}
|
|
@@ -10944,8 +11134,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10944
11134
|
const locks = collectTestLocks(test);
|
|
10945
11135
|
const testCase = {
|
|
10946
11136
|
type: "complete",
|
|
11137
|
+
testId: test.id,
|
|
10947
11138
|
title: test.title,
|
|
10948
|
-
location: testLocation(test),
|
|
11139
|
+
location: testLocation(test, this.testPathBase),
|
|
10949
11140
|
status: "didnotrun",
|
|
10950
11141
|
duration: 0,
|
|
10951
11142
|
timeout: test.timeout ?? null,
|
|
@@ -11022,6 +11213,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11022
11213
|
const branch = resolveScmBranch(process.env) ?? null;
|
|
11023
11214
|
await this.manifests.upload(this.httpClient, submitted.auth, this.options.projectName, branch);
|
|
11024
11215
|
}
|
|
11216
|
+
} catch (error) {
|
|
11217
|
+
this.logger.error(`Could not report the run to the dashboard: ${errorMessage(error)}`);
|
|
11025
11218
|
} finally {
|
|
11026
11219
|
this.coverage?.close();
|
|
11027
11220
|
this.fileHandler.cleanupBodyAttachments();
|
|
@@ -11103,7 +11296,7 @@ async function resolveSelection(options = {}) {
|
|
|
11103
11296
|
}
|
|
11104
11297
|
|
|
11105
11298
|
// src/internal/capture/capture-fixtures.ts
|
|
11106
|
-
var
|
|
11299
|
+
var path28 = __toESM(require("path"));
|
|
11107
11300
|
var import_node_zlib2 = require("zlib");
|
|
11108
11301
|
|
|
11109
11302
|
// ../picker-dom/src/probe.ts
|
|
@@ -11534,26 +11727,26 @@ function installPickerOverlay(arg) {
|
|
|
11534
11727
|
}
|
|
11535
11728
|
};
|
|
11536
11729
|
mount(highlight, label, banner);
|
|
11537
|
-
const
|
|
11730
|
+
const quoteJs = (s) => `'${JSON.stringify(s).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
|
|
11538
11731
|
const describe = (el) => {
|
|
11539
11732
|
const tag = (el.tagName || "?").toLowerCase();
|
|
11540
11733
|
const testId = el.getAttribute && el.getAttribute(testIdAttr);
|
|
11541
|
-
if (testId) return `getByTestId(
|
|
11734
|
+
if (testId) return `getByTestId(${quoteJs(testId)})`;
|
|
11542
11735
|
if (el.labels && el.labels.length > 0) {
|
|
11543
11736
|
const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
11544
|
-
if (labelText) return `getByLabel(
|
|
11737
|
+
if (labelText) return `getByLabel(${quoteJs(labelText)})`;
|
|
11545
11738
|
}
|
|
11546
11739
|
const ariaLabel = el.getAttribute && el.getAttribute("aria-label");
|
|
11547
|
-
if (ariaLabel) return `getByLabel(
|
|
11740
|
+
if (ariaLabel) return `getByLabel(${quoteJs(ariaLabel)})`;
|
|
11548
11741
|
const placeholder = el.getAttribute && el.getAttribute("placeholder");
|
|
11549
|
-
if (placeholder) return `getByPlaceholder(
|
|
11742
|
+
if (placeholder) return `getByPlaceholder(${quoteJs(placeholder)})`;
|
|
11550
11743
|
const alt = el.getAttribute && el.getAttribute("alt");
|
|
11551
|
-
if (alt) return `getByAltText(
|
|
11744
|
+
if (alt) return `getByAltText(${quoteJs(alt)})`;
|
|
11552
11745
|
const titleAttr = el.getAttribute && el.getAttribute("title");
|
|
11553
|
-
if (titleAttr) return `getByTitle(
|
|
11554
|
-
if (el.id) return `locator(
|
|
11746
|
+
if (titleAttr) return `getByTitle(${quoteJs(titleAttr)})`;
|
|
11747
|
+
if (el.id) return `locator(${quoteJs(`#${el.id}`)})`;
|
|
11555
11748
|
const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
|
|
11556
|
-
return cls ? `locator(
|
|
11749
|
+
return cls ? `locator(${quoteJs(`.${cls}`)})` : tag;
|
|
11557
11750
|
};
|
|
11558
11751
|
const buildChain = (raw) => {
|
|
11559
11752
|
const chain2 = [];
|
|
@@ -12339,7 +12532,6 @@ function headingLevel(attrs, role) {
|
|
|
12339
12532
|
return null;
|
|
12340
12533
|
}
|
|
12341
12534
|
var attr = (a, key) => a.attributes[key] || null;
|
|
12342
|
-
var esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
12343
12535
|
var escCssAttrValue = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
12344
12536
|
var isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
|
|
12345
12537
|
function classifyCssStability(className) {
|
|
@@ -12394,7 +12586,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12394
12586
|
const testId = attr(attrs, testIdAttribute);
|
|
12395
12587
|
if (testId && isUnique(counts?.testId)) {
|
|
12396
12588
|
add({
|
|
12397
|
-
locator: `getByTestId(
|
|
12589
|
+
locator: `getByTestId(${quoteJsString(testId)})`,
|
|
12398
12590
|
method: "getByTestId",
|
|
12399
12591
|
args: { testId },
|
|
12400
12592
|
score: 100
|
|
@@ -12405,7 +12597,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12405
12597
|
const withLevel = (base) => level != null ? { ...base, level } : base;
|
|
12406
12598
|
if (role && accessibleName) {
|
|
12407
12599
|
add({
|
|
12408
|
-
locator: `getByRole(
|
|
12600
|
+
locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
|
|
12409
12601
|
method: "getByRole",
|
|
12410
12602
|
args: withLevel({ role, name: accessibleName }),
|
|
12411
12603
|
score: 90 - ambiguityPenalty(counts?.roleName)
|
|
@@ -12414,7 +12606,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12414
12606
|
const ariaLabel = attr(attrs, "aria-label");
|
|
12415
12607
|
if (role && ariaLabel && ariaLabel !== accessibleName) {
|
|
12416
12608
|
add({
|
|
12417
|
-
locator: `getByRole(
|
|
12609
|
+
locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
|
|
12418
12610
|
method: "getByRole",
|
|
12419
12611
|
args: withLevel({ role, name: ariaLabel }),
|
|
12420
12612
|
score: 85 - ambiguityPenalty(counts?.roleName)
|
|
@@ -12425,7 +12617,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12425
12617
|
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
12426
12618
|
if (labelBacked) {
|
|
12427
12619
|
add({
|
|
12428
|
-
locator: `getByLabel(
|
|
12620
|
+
locator: `getByLabel(${quoteJsString(accessibleName)})`,
|
|
12429
12621
|
method: "getByLabel",
|
|
12430
12622
|
args: { label: accessibleName },
|
|
12431
12623
|
score: 85
|
|
@@ -12435,7 +12627,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12435
12627
|
const placeholder = attr(attrs, "placeholder");
|
|
12436
12628
|
if (placeholder) {
|
|
12437
12629
|
add({
|
|
12438
|
-
locator: `getByPlaceholder(
|
|
12630
|
+
locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
|
|
12439
12631
|
method: "getByPlaceholder",
|
|
12440
12632
|
args: { placeholder },
|
|
12441
12633
|
score: 80 - ambiguityPenalty(counts?.placeholder)
|
|
@@ -12443,7 +12635,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12443
12635
|
}
|
|
12444
12636
|
if (text && text.length < 80) {
|
|
12445
12637
|
add({
|
|
12446
|
-
locator: `getByText(
|
|
12638
|
+
locator: `getByText(${quoteJsString(text)})`,
|
|
12447
12639
|
method: "getByText",
|
|
12448
12640
|
args: { text },
|
|
12449
12641
|
score: 75 - ambiguityPenalty(counts?.text)
|
|
@@ -12453,7 +12645,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12453
12645
|
if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
12454
12646
|
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
12455
12647
|
add({
|
|
12456
|
-
locator: `locator(
|
|
12648
|
+
locator: `locator(${quoteJsString(selector)})`,
|
|
12457
12649
|
method: "locator",
|
|
12458
12650
|
args: { selector },
|
|
12459
12651
|
score: 65
|
|
@@ -12463,7 +12655,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12463
12655
|
if (name && selectable("name") && isUnique(counts?.name)) {
|
|
12464
12656
|
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
12465
12657
|
add({
|
|
12466
|
-
locator: `locator(
|
|
12658
|
+
locator: `locator(${quoteJsString(selector)})`,
|
|
12467
12659
|
method: "locator",
|
|
12468
12660
|
args: { selector },
|
|
12469
12661
|
score: 60
|
|
@@ -12472,7 +12664,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12472
12664
|
const alt = attr(attrs, "alt");
|
|
12473
12665
|
if (alt) {
|
|
12474
12666
|
add({
|
|
12475
|
-
locator: `getByAltText(
|
|
12667
|
+
locator: `getByAltText(${quoteJsString(alt)})`,
|
|
12476
12668
|
method: "getByAltText",
|
|
12477
12669
|
args: { text: alt },
|
|
12478
12670
|
score: 60 - ambiguityPenalty(counts?.alt)
|
|
@@ -12481,7 +12673,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12481
12673
|
const title = attr(attrs, "title");
|
|
12482
12674
|
if (title) {
|
|
12483
12675
|
add({
|
|
12484
|
-
locator: `getByTitle(
|
|
12676
|
+
locator: `getByTitle(${quoteJsString(title)})`,
|
|
12485
12677
|
method: "getByTitle",
|
|
12486
12678
|
args: { title },
|
|
12487
12679
|
score: 50 - ambiguityPenalty(counts?.title)
|
|
@@ -12499,7 +12691,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12499
12691
|
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
12500
12692
|
testIdAnchorDone = true;
|
|
12501
12693
|
add({
|
|
12502
|
-
locator: `getByTestId(
|
|
12694
|
+
locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
|
|
12503
12695
|
method: leaf.method,
|
|
12504
12696
|
args: { ...leaf.args, anchorTestId: anc.testId },
|
|
12505
12697
|
score: scores.testId
|
|
@@ -12509,7 +12701,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12509
12701
|
idAnchorDone = true;
|
|
12510
12702
|
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
12511
12703
|
add({
|
|
12512
|
-
locator: `locator(
|
|
12704
|
+
locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
|
|
12513
12705
|
method: leaf.method,
|
|
12514
12706
|
args: { ...leaf.args, anchorSelector },
|
|
12515
12707
|
score: scores.id
|
|
@@ -12520,7 +12712,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12520
12712
|
const { name: name2, value } = anc.dataAttr;
|
|
12521
12713
|
const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
|
|
12522
12714
|
add({
|
|
12523
|
-
locator: `locator(
|
|
12715
|
+
locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
|
|
12524
12716
|
method: leaf.method,
|
|
12525
12717
|
args: { ...leaf.args, anchorSelector },
|
|
12526
12718
|
score: TEST_DATA_ATTRS.has(name2) ? scores.testData : scores.data
|
|
@@ -12530,7 +12722,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12530
12722
|
if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
|
|
12531
12723
|
roleAnchorDone = true;
|
|
12532
12724
|
add({
|
|
12533
|
-
locator: `getByRole(
|
|
12725
|
+
locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
|
|
12534
12726
|
method: leaf.method,
|
|
12535
12727
|
args: { ...leaf.args, anchorRole: ancestorRole },
|
|
12536
12728
|
score: scores.role
|
|
@@ -12539,7 +12731,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12539
12731
|
if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
|
|
12540
12732
|
filterAnchorDone = true;
|
|
12541
12733
|
add({
|
|
12542
|
-
locator: `getByRole(
|
|
12734
|
+
locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
|
|
12543
12735
|
method: leaf.method,
|
|
12544
12736
|
args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
|
|
12545
12737
|
score: scores.filter
|
|
@@ -12548,7 +12740,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12548
12740
|
}
|
|
12549
12741
|
};
|
|
12550
12742
|
if (role && !hasOwnTestId) {
|
|
12551
|
-
const rolePart = level != null ?
|
|
12743
|
+
const rolePart = level != null ? `${quoteJsString(role)}, { level: ${level} }` : quoteJsString(role);
|
|
12552
12744
|
const leafArgs = withLevel({ role });
|
|
12553
12745
|
addAnchoredChains(
|
|
12554
12746
|
{ expr: `getByRole(${rolePart})`, method: "getByRole", args: leafArgs, role },
|
|
@@ -12566,7 +12758,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12566
12758
|
}
|
|
12567
12759
|
} else if (!role && text && text.length < 80 && !hasOwnTestId) {
|
|
12568
12760
|
addAnchoredChains(
|
|
12569
|
-
{ expr: `getByText(
|
|
12761
|
+
{ expr: `getByText(${quoteJsString(text)})`, method: "getByText", args: { text }, role: null },
|
|
12570
12762
|
(anc) => anc.scopedTextCount,
|
|
12571
12763
|
{ testId: 68, testData: 66, id: 60, data: 56, role: 51, filter: 49 }
|
|
12572
12764
|
);
|
|
@@ -12577,7 +12769,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
12577
12769
|
const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
|
|
12578
12770
|
for (const { cls, score } of classes) {
|
|
12579
12771
|
add({
|
|
12580
|
-
locator: `locator(
|
|
12772
|
+
locator: `locator(${quoteJsString(`.${cls}`)})`,
|
|
12581
12773
|
method: "locator",
|
|
12582
12774
|
args: { selector: `.${cls}` },
|
|
12583
12775
|
score
|
|
@@ -12605,26 +12797,25 @@ function approximateAccessibleName(attrs) {
|
|
|
12605
12797
|
}
|
|
12606
12798
|
|
|
12607
12799
|
// ../picker-dom/src/anchor-alternatives.ts
|
|
12608
|
-
var escStr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
12609
12800
|
var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
|
|
12610
12801
|
var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
|
|
12611
12802
|
function anchorSegment(a) {
|
|
12612
12803
|
if (a.testId && a.testIdCount === 1) {
|
|
12613
|
-
return { code: `getByTestId(
|
|
12804
|
+
return { code: `getByTestId(${quoteJsString(a.testId)})`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
|
|
12614
12805
|
}
|
|
12615
12806
|
if (a.id && !isAutoGenerated(a.id) && a.idCount === 1) {
|
|
12616
12807
|
const anchorSelector = cssIdSelector(a.id);
|
|
12617
|
-
return { code: `locator(
|
|
12808
|
+
return { code: `locator(${quoteJsString(anchorSelector)})`, kind: "id", flatArgs: { anchorSelector } };
|
|
12618
12809
|
}
|
|
12619
12810
|
if (a.role && a.ariaLabel && a.labeledRoleCount === 1) {
|
|
12620
12811
|
return {
|
|
12621
|
-
code: `getByRole(
|
|
12812
|
+
code: `getByRole(${quoteJsString(a.role)}, { name: ${quoteJsString(a.ariaLabel)} })`,
|
|
12622
12813
|
kind: "labeledRole",
|
|
12623
12814
|
flatArgs: { anchorRole: a.role, anchorName: a.ariaLabel }
|
|
12624
12815
|
};
|
|
12625
12816
|
}
|
|
12626
12817
|
if (a.role && a.roleCount === 1) {
|
|
12627
|
-
return { code: `getByRole(
|
|
12818
|
+
return { code: `getByRole(${quoteJsString(a.role)})`, kind: "role", flatArgs: { anchorRole: a.role } };
|
|
12628
12819
|
}
|
|
12629
12820
|
return null;
|
|
12630
12821
|
}
|
|
@@ -12632,7 +12823,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
12632
12823
|
if (!leaf.role || anchors.length === 0) return [];
|
|
12633
12824
|
const out = [];
|
|
12634
12825
|
const seen = /* @__PURE__ */ new Set();
|
|
12635
|
-
const rolePart = leaf.level != null ?
|
|
12826
|
+
const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
|
|
12636
12827
|
const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
|
|
12637
12828
|
const add = (l) => {
|
|
12638
12829
|
if (!seen.has(l.locator)) {
|
|
@@ -12687,21 +12878,21 @@ var MAX_INDETERMINATE = 20;
|
|
|
12687
12878
|
var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
|
|
12688
12879
|
var LEVEL = `${STEP}(?:/${STEP})*`;
|
|
12689
12880
|
var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
|
|
12690
|
-
function isElementPath(
|
|
12691
|
-
return typeof
|
|
12881
|
+
function isElementPath(path32) {
|
|
12882
|
+
return typeof path32 === "string" && path32.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path32);
|
|
12692
12883
|
}
|
|
12693
12884
|
var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
|
|
12694
12885
|
function elementRef(v) {
|
|
12695
12886
|
if (!v || typeof v !== "object") return null;
|
|
12696
|
-
const { path:
|
|
12697
|
-
if (!isElementPath(
|
|
12698
|
-
return { path:
|
|
12887
|
+
const { path: path32, label } = v;
|
|
12888
|
+
if (!isElementPath(path32)) return null;
|
|
12889
|
+
return { path: path32, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
|
|
12699
12890
|
}
|
|
12700
12891
|
function textPoint(v) {
|
|
12701
12892
|
if (!v || typeof v !== "object") return null;
|
|
12702
|
-
const { path:
|
|
12893
|
+
const { path: path32, offset } = v;
|
|
12703
12894
|
const at = offsetOf(offset);
|
|
12704
|
-
return isElementPath(
|
|
12895
|
+
return isElementPath(path32) && at !== null ? { path: path32, offset: at } : null;
|
|
12705
12896
|
}
|
|
12706
12897
|
function sanitizePageInteraction(raw) {
|
|
12707
12898
|
if (!raw || typeof raw !== "object") return {};
|
|
@@ -12732,7 +12923,7 @@ function sanitizePageInteraction(raw) {
|
|
|
12732
12923
|
}
|
|
12733
12924
|
|
|
12734
12925
|
// src/internal/capture/locator-healing.ts
|
|
12735
|
-
var
|
|
12926
|
+
var path25 = __toESM(require("path"));
|
|
12736
12927
|
|
|
12737
12928
|
// ../core/src/locator-fingerprint.ts
|
|
12738
12929
|
var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
|
|
@@ -12758,7 +12949,6 @@ var TEXT_CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
|
12758
12949
|
"switch"
|
|
12759
12950
|
]);
|
|
12760
12951
|
var FORM_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
|
|
12761
|
-
var escapeQuote = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
12762
12952
|
function parseAriaCandidates(ariaSnapshot) {
|
|
12763
12953
|
if (!ariaSnapshot) return [];
|
|
12764
12954
|
const out = [];
|
|
@@ -12786,6 +12976,23 @@ function textSimilarity(a, b) {
|
|
|
12786
12976
|
for (const t of sa) if (sb.has(t)) common++;
|
|
12787
12977
|
return 2 * common / (sa.size + sb.size);
|
|
12788
12978
|
}
|
|
12979
|
+
var FUNCTION_WORDS = new Set(
|
|
12980
|
+
"the and for with from your you our this that les des une pour avec par sur aux dans vos votre nos notre".split(" ")
|
|
12981
|
+
);
|
|
12982
|
+
function significantWords(text) {
|
|
12983
|
+
const all = (text ?? "").replace(new RegExp("(\\p{Ll})(\\p{Lu})", "gu"), "$1 $2").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
12984
|
+
const meaningful = all.filter((w) => w.length >= 3 && !FUNCTION_WORDS.has(w));
|
|
12985
|
+
return new Set(meaningful.length > 0 ? meaningful : all);
|
|
12986
|
+
}
|
|
12987
|
+
function significantWordSimilarity(a, b) {
|
|
12988
|
+
const sa = significantWords(a);
|
|
12989
|
+
const sb = significantWords(b);
|
|
12990
|
+
if (sa.size === 0 && sb.size === 0) return 1;
|
|
12991
|
+
if (sa.size === 0 || sb.size === 0) return 0;
|
|
12992
|
+
let common = 0;
|
|
12993
|
+
for (const w of sa) if (sb.has(w)) common++;
|
|
12994
|
+
return 2 * common / (sa.size + sb.size);
|
|
12995
|
+
}
|
|
12789
12996
|
function fingerprintPresent(fp, candidates) {
|
|
12790
12997
|
if (!fp.name) return false;
|
|
12791
12998
|
return candidates.some(
|
|
@@ -12807,7 +13014,7 @@ function matchRenamedElement(fp, candidates) {
|
|
|
12807
13014
|
let best = null;
|
|
12808
13015
|
let bestScore = -1;
|
|
12809
13016
|
for (const c of pool) {
|
|
12810
|
-
const s =
|
|
13017
|
+
const s = significantWordSimilarity(c.name, fp.name);
|
|
12811
13018
|
if (s > bestScore) {
|
|
12812
13019
|
bestScore = s;
|
|
12813
13020
|
best = c;
|
|
@@ -12827,12 +13034,12 @@ function freshLocatorsFromCandidate(c) {
|
|
|
12827
13034
|
if (!name) return out;
|
|
12828
13035
|
out.push(
|
|
12829
13036
|
c.level != null ? {
|
|
12830
|
-
locator: `getByRole(
|
|
13037
|
+
locator: `getByRole(${quoteJsString(c.role)}, { name: ${quoteJsString(name)}, level: ${c.level} })`,
|
|
12831
13038
|
method: "getByRole",
|
|
12832
13039
|
args: { role: c.role, name, level: c.level },
|
|
12833
13040
|
score: ELEMENT_MATCH_SCORES.role
|
|
12834
13041
|
} : {
|
|
12835
|
-
locator: `getByRole(
|
|
13042
|
+
locator: `getByRole(${quoteJsString(c.role)}, { name: ${quoteJsString(name)} })`,
|
|
12836
13043
|
method: "getByRole",
|
|
12837
13044
|
args: { role: c.role, name },
|
|
12838
13045
|
score: ELEMENT_MATCH_SCORES.role
|
|
@@ -12840,14 +13047,14 @@ function freshLocatorsFromCandidate(c) {
|
|
|
12840
13047
|
);
|
|
12841
13048
|
if (TEXT_CONTENT_ROLES.has(c.role)) {
|
|
12842
13049
|
out.push({
|
|
12843
|
-
locator: `getByText(
|
|
13050
|
+
locator: `getByText(${quoteJsString(name)})`,
|
|
12844
13051
|
method: "getByText",
|
|
12845
13052
|
args: { text: name },
|
|
12846
13053
|
score: ELEMENT_MATCH_SCORES.text
|
|
12847
13054
|
});
|
|
12848
13055
|
} else if (FORM_FIELD_ROLES.has(c.role)) {
|
|
12849
13056
|
out.push({
|
|
12850
|
-
locator: `getByLabel(
|
|
13057
|
+
locator: `getByLabel(${quoteJsString(name)})`,
|
|
12851
13058
|
method: "getByLabel",
|
|
12852
13059
|
args: { label: name },
|
|
12853
13060
|
score: ELEMENT_MATCH_SCORES.label
|
|
@@ -12992,7 +13199,6 @@ var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
|
|
|
12992
13199
|
"getByTitle",
|
|
12993
13200
|
"getByAltText"
|
|
12994
13201
|
]);
|
|
12995
|
-
var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
12996
13202
|
var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
|
|
12997
13203
|
"button",
|
|
12998
13204
|
"link",
|
|
@@ -13025,9 +13231,9 @@ function failedNameAndRole(failed) {
|
|
|
13025
13231
|
function renderFailing(failed) {
|
|
13026
13232
|
const { role, name } = failedNameAndRole(failed);
|
|
13027
13233
|
if (failed.method === "getByRole") {
|
|
13028
|
-
return name ? `getByRole(
|
|
13234
|
+
return name ? `getByRole(${quoteJsString(role ?? "")}, { name: ${quoteJsString(name)} })` : `getByRole(${quoteJsString(role ?? "")})`;
|
|
13029
13235
|
}
|
|
13030
|
-
return `${failed.method}(
|
|
13236
|
+
return `${failed.method}(${quoteJsString(name ?? "")})`;
|
|
13031
13237
|
}
|
|
13032
13238
|
function freshSuggestions(candidate, failedMethod) {
|
|
13033
13239
|
const out = [];
|
|
@@ -13037,9 +13243,9 @@ function freshSuggestions(candidate, failedMethod) {
|
|
|
13037
13243
|
if (!out.includes(s)) out.push(s);
|
|
13038
13244
|
};
|
|
13039
13245
|
const levelPart = candidate.level != null ? `, level: ${candidate.level}` : "";
|
|
13040
|
-
const roleLoc = `getByRole(
|
|
13041
|
-
const textLoc = `getByText(
|
|
13042
|
-
const labelLoc = `getByLabel(
|
|
13246
|
+
const roleLoc = `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(name)}${levelPart} })`;
|
|
13247
|
+
const textLoc = `getByText(${quoteJsString(name)})`;
|
|
13248
|
+
const labelLoc = `getByLabel(${quoteJsString(name)})`;
|
|
13043
13249
|
if (failedMethod === "getByText" && SUGG_TEXT_ROLES.has(role)) push(textLoc);
|
|
13044
13250
|
if (failedMethod === "getByLabel" && SUGG_FIELD_ROLES.has(role)) push(labelLoc);
|
|
13045
13251
|
push(roleLoc);
|
|
@@ -13102,10 +13308,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
13102
13308
|
}
|
|
13103
13309
|
let rel = file;
|
|
13104
13310
|
try {
|
|
13105
|
-
rel =
|
|
13311
|
+
rel = path25.relative(process.cwd(), file);
|
|
13106
13312
|
} catch {
|
|
13107
13313
|
}
|
|
13108
|
-
rel = rel.split(
|
|
13314
|
+
rel = rel.split(path25.sep).join("/");
|
|
13109
13315
|
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
13110
13316
|
return `${rel}:${m[3]}:${m[4]}`;
|
|
13111
13317
|
}
|
|
@@ -13335,8 +13541,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
13335
13541
|
}
|
|
13336
13542
|
|
|
13337
13543
|
// src/internal/capture/pick-on-failure.ts
|
|
13338
|
-
var
|
|
13339
|
-
var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
|
|
13544
|
+
var path26 = __toESM(require("path"));
|
|
13340
13545
|
function parseLeafLocatorExpression(rawExpr) {
|
|
13341
13546
|
const leaf = parseLeafLocatorCall(rawExpr);
|
|
13342
13547
|
return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
|
|
@@ -13345,14 +13550,13 @@ function deriveFailedLocator(testInfo) {
|
|
|
13345
13550
|
const info = testInfo;
|
|
13346
13551
|
const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
|
|
13347
13552
|
for (const err of errors) {
|
|
13348
|
-
const
|
|
13349
|
-
${err.stack ?? ""}
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
const parsed2 = parseLeafLocatorExpression(line[1].trim());
|
|
13553
|
+
const header = readLocatorHeader(stripAnsi(`${err.message ?? ""}
|
|
13554
|
+
${err.stack ?? ""}`));
|
|
13555
|
+
if (!header) continue;
|
|
13556
|
+
const parsed2 = parseLeafLocatorExpression(header);
|
|
13353
13557
|
if (!parsed2) continue;
|
|
13354
13558
|
const loc = err.location;
|
|
13355
|
-
const location = loc ? `${
|
|
13559
|
+
const location = loc ? `${path26.relative(process.cwd(), loc.file).split(path26.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
13356
13560
|
return { method: parsed2.method, args: parsed2.args, location };
|
|
13357
13561
|
}
|
|
13358
13562
|
return null;
|
|
@@ -13505,7 +13709,7 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
13505
13709
|
}
|
|
13506
13710
|
|
|
13507
13711
|
// src/internal/capture/pause-at.ts
|
|
13508
|
-
var
|
|
13712
|
+
var path27 = __toESM(require("path"));
|
|
13509
13713
|
|
|
13510
13714
|
// ../core/src/editor-send.ts
|
|
13511
13715
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
@@ -13549,9 +13753,9 @@ var PauseSet = class {
|
|
|
13549
13753
|
var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
|
|
13550
13754
|
function callSiteFile(file, cwd) {
|
|
13551
13755
|
const slashed = file.replace(/\\/g, "/");
|
|
13552
|
-
const absolute =
|
|
13553
|
-
let rel = absolute ?
|
|
13554
|
-
rel = rel.split(
|
|
13756
|
+
const absolute = path27.isAbsolute(file) || path27.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
|
|
13757
|
+
let rel = absolute ? path27.relative(cwd, file) : path27.normalize(slashed);
|
|
13758
|
+
rel = rel.split(path27.sep).join("/").replace(/\\/g, "/");
|
|
13555
13759
|
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
13556
13760
|
return rel;
|
|
13557
13761
|
}
|
|
@@ -14035,7 +14239,7 @@ function configuredCodeReachRoots() {
|
|
|
14035
14239
|
}
|
|
14036
14240
|
} catch {
|
|
14037
14241
|
}
|
|
14038
|
-
return raw.split(
|
|
14242
|
+
return raw.split(path28.delimiter).filter(Boolean);
|
|
14039
14243
|
}
|
|
14040
14244
|
async function stopSinkCodeReach(sink, closing) {
|
|
14041
14245
|
const stopping = [];
|
|
@@ -14051,9 +14255,7 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
14051
14255
|
}
|
|
14052
14256
|
if (stopping.length > 0) {
|
|
14053
14257
|
const info = sink.testInfo;
|
|
14054
|
-
const
|
|
14055
|
-
const configDir = configFile ? path30.dirname(configFile) : info?.config.rootDir ?? process.cwd();
|
|
14056
|
-
const roots = codeReachRoots(configDir, configuredCodeReachRoots());
|
|
14258
|
+
const roots = codeReachRoots(configDirFromConfig(info?.config), configuredCodeReachRoots());
|
|
14057
14259
|
for (const [page, session] of stopping) {
|
|
14058
14260
|
session.stopping = stopPageCodeReach(sink, page, session, roots);
|
|
14059
14261
|
underway.push(session.stopping);
|
|
@@ -14195,8 +14397,8 @@ function buildPageState(raw, cookies, interaction) {
|
|
|
14195
14397
|
length: typeof e.length === "number" ? e.length : 0
|
|
14196
14398
|
}));
|
|
14197
14399
|
return {
|
|
14198
|
-
url: raw.url,
|
|
14199
|
-
hash: raw.hash
|
|
14400
|
+
url: raw.url.split("#")[0],
|
|
14401
|
+
hash: raw.hash ? maskUrlFragment(raw.hash) : null,
|
|
14200
14402
|
historyState: historyState || null,
|
|
14201
14403
|
localStorage: capStorage(raw.localStorage),
|
|
14202
14404
|
sessionStorage: capStorage(raw.sessionStorage),
|
|
@@ -15213,6 +15415,7 @@ var piwiFixtures = {
|
|
|
15213
15415
|
const plan = loadFlakePlan();
|
|
15214
15416
|
const role = flakeRoleForTest(plan, {
|
|
15215
15417
|
file: testInfo.file,
|
|
15418
|
+
base: configDirFromConfig(testInfo.config),
|
|
15216
15419
|
title: testInfo.title,
|
|
15217
15420
|
titlePath: testInfo.titlePath,
|
|
15218
15421
|
project: testInfo.project.name
|
|
@@ -15226,7 +15429,8 @@ var piwiFixtures = {
|
|
|
15226
15429
|
sink.probeItem = probeItemForTest({
|
|
15227
15430
|
title: testInfo.title,
|
|
15228
15431
|
file: testInfo.file,
|
|
15229
|
-
titlePath: testInfo.titlePath
|
|
15432
|
+
titlePath: testInfo.titlePath,
|
|
15433
|
+
base: configDirFromConfig(testInfo.config)
|
|
15230
15434
|
});
|
|
15231
15435
|
activeResourceLedger()?.testStarted(testInfo);
|
|
15232
15436
|
currentSink = sink;
|
|
@@ -15270,6 +15474,7 @@ async function recordFlakeAttempt(flake, testInfo) {
|
|
|
15270
15474
|
flakeResultLine(flake.plan, {
|
|
15271
15475
|
role: flake.role,
|
|
15272
15476
|
file: testInfo.file,
|
|
15477
|
+
base: configDirFromConfig(testInfo.config),
|
|
15273
15478
|
title: testInfo.title,
|
|
15274
15479
|
project: testInfo.project.name || null,
|
|
15275
15480
|
browserName: flake.browserName,
|
|
@@ -15291,12 +15496,12 @@ function extendPiwiFixtures(test) {
|
|
|
15291
15496
|
|
|
15292
15497
|
// src/internal/ai/ai-fixtures.ts
|
|
15293
15498
|
var fs26 = __toESM(require("fs"));
|
|
15294
|
-
var
|
|
15499
|
+
var path31 = __toESM(require("path"));
|
|
15295
15500
|
var import_test = require("@playwright/test");
|
|
15296
15501
|
|
|
15297
15502
|
// src/internal/ai/artifact.ts
|
|
15298
15503
|
var fs25 = __toESM(require("fs"));
|
|
15299
|
-
var
|
|
15504
|
+
var path29 = __toESM(require("path"));
|
|
15300
15505
|
var ARTIFACT_VERSION = 1;
|
|
15301
15506
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
15302
15507
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -15400,7 +15605,7 @@ function writeEntry(file, entry) {
|
|
|
15400
15605
|
const canonical = serializeEntry(entry);
|
|
15401
15606
|
return withEntryLock(file, () => {
|
|
15402
15607
|
if (fs25.existsSync(file) && fs25.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
15403
|
-
fs25.mkdirSync(
|
|
15608
|
+
fs25.mkdirSync(path29.dirname(file), { recursive: true });
|
|
15404
15609
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
15405
15610
|
fs25.writeFileSync(tmp, canonical);
|
|
15406
15611
|
fs25.renameSync(tmp, file);
|
|
@@ -15409,7 +15614,7 @@ function writeEntry(file, entry) {
|
|
|
15409
15614
|
}
|
|
15410
15615
|
function withEntryLock(file, fn) {
|
|
15411
15616
|
const lock = `${file}.lock`;
|
|
15412
|
-
fs25.mkdirSync(
|
|
15617
|
+
fs25.mkdirSync(path29.dirname(file), { recursive: true });
|
|
15413
15618
|
const deadline = Date.now() + 5e3;
|
|
15414
15619
|
for (; ; ) {
|
|
15415
15620
|
try {
|
|
@@ -15437,28 +15642,28 @@ function withEntryLock(file, fn) {
|
|
|
15437
15642
|
}
|
|
15438
15643
|
|
|
15439
15644
|
// src/internal/ai/keys.ts
|
|
15440
|
-
var
|
|
15441
|
-
var
|
|
15645
|
+
var crypto7 = __toESM(require("crypto"));
|
|
15646
|
+
var path30 = __toESM(require("path"));
|
|
15442
15647
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
15443
15648
|
function normalizeTemplate(template) {
|
|
15444
15649
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
15445
15650
|
}
|
|
15446
15651
|
function hashTemplate(normalized, ordinal = 0) {
|
|
15447
15652
|
const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
|
|
15448
|
-
return
|
|
15653
|
+
return crypto7.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
15449
15654
|
}
|
|
15450
15655
|
function slug(text, maxLength = 40) {
|
|
15451
15656
|
const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
15452
15657
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
15453
15658
|
}
|
|
15454
15659
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
15455
|
-
return
|
|
15660
|
+
return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
|
|
15456
15661
|
}
|
|
15457
15662
|
function entryPath(params) {
|
|
15458
15663
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
15459
15664
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
15460
15665
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
15461
|
-
return
|
|
15666
|
+
return path30.join(entryDir(params.specFile, params.dir), name);
|
|
15462
15667
|
}
|
|
15463
15668
|
function parseLocation2(location) {
|
|
15464
15669
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -15865,6 +16070,8 @@ async function groundElement(page, element, params) {
|
|
|
15865
16070
|
// src/internal/ai/resolution.ts
|
|
15866
16071
|
var MAX_SNAPSHOT_CHARS = 24e3;
|
|
15867
16072
|
var DEFAULT_MAX_STEPS = 20;
|
|
16073
|
+
var MAX_OBSERVED_RESPONSES = 50;
|
|
16074
|
+
var MAX_OBSERVED_URL_CHARS = 4e3;
|
|
15868
16075
|
var ServerStepResolver = class {
|
|
15869
16076
|
constructor(serverUrl, apiKey) {
|
|
15870
16077
|
this.serverUrl = serverUrl;
|
|
@@ -16016,14 +16223,15 @@ async function resolveRun(template, ctx) {
|
|
|
16016
16223
|
ctx.responseWaitTimeout
|
|
16017
16224
|
);
|
|
16018
16225
|
history.push({ action: decision.action, element: decision.element, value: decision.value });
|
|
16019
|
-
|
|
16226
|
+
const observedResponses = [...new Set(observed.map((url) => maskValues(url, ctx.params)))].filter((url) => url.length <= MAX_OBSERVED_URL_CHARS).slice(0, MAX_OBSERVED_RESPONSES);
|
|
16227
|
+
if (observedResponses.length > 0) {
|
|
16020
16228
|
const waitDecision = await ctx.resolver.resolveStep({
|
|
16021
16229
|
kind: "wait",
|
|
16022
16230
|
template,
|
|
16023
16231
|
paramNames,
|
|
16024
16232
|
ariaSnapshot: "",
|
|
16025
16233
|
history,
|
|
16026
|
-
observedResponses
|
|
16234
|
+
observedResponses
|
|
16027
16235
|
});
|
|
16028
16236
|
if (waitDecision.waitForResponse) step.waitForResponse = waitDecision.waitForResponse;
|
|
16029
16237
|
}
|
|
@@ -16058,7 +16266,7 @@ function testIdentity(testInfo) {
|
|
|
16058
16266
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
16059
16267
|
}
|
|
16060
16268
|
function relativeToCwd(file) {
|
|
16061
|
-
return
|
|
16269
|
+
return path31.relative(process.cwd(), file).split(path31.sep).join("/");
|
|
16062
16270
|
}
|
|
16063
16271
|
function recordIntents(intents, entry) {
|
|
16064
16272
|
const add = (locator, kind) => {
|