@piwitests/reporter 0.61.0 → 0.62.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 +369 -223
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1794 -266
- package/dist/internal/capture/attachments.d.ts +24 -1
- package/dist/internal/capture/attachments.js +21 -2
- package/dist/internal/capture/capture-fixtures.d.ts +114 -6
- package/dist/internal/capture/capture-fixtures.js +1597 -176
- package/package.json +1 -1
- package/templates/skills/apply-locator-healing/SKILL.md +2 -2
- package/templates/skills/fix-a-reported-bug/SKILL.md +11 -8
- package/templates/skills/investigate-failure/SKILL.md +3 -3
- package/templates/skills/setup-piwi/SKILL.md +11 -8
- package/templates/skills/stabilize-flaky-tests/SKILL.md +3 -3
- package/templates/skills/write-the-missing-test/SKILL.md +13 -10
package/dist/index.js
CHANGED
|
@@ -65,9 +65,9 @@ function processAlive(pid) {
|
|
|
65
65
|
return error.code === "EPERM";
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
function readDesktopConfig(
|
|
68
|
+
function readDesktopConfig(filePath3) {
|
|
69
69
|
try {
|
|
70
|
-
const parsed2 = JSON.parse(fs.readFileSync(
|
|
70
|
+
const parsed2 = JSON.parse(fs.readFileSync(filePath3, "utf8"));
|
|
71
71
|
const url = parsed2?.url;
|
|
72
72
|
const token = parsed2?.token;
|
|
73
73
|
if (typeof url !== "string" || !url) return null;
|
|
@@ -102,9 +102,9 @@ function parseDotEnv(text) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// src/internal/config/dotenv.ts
|
|
105
|
-
function readDotEnv(
|
|
105
|
+
function readDotEnv(filePath3) {
|
|
106
106
|
try {
|
|
107
|
-
return parseDotEnv(fs2.readFileSync(
|
|
107
|
+
return parseDotEnv(fs2.readFileSync(filePath3, "utf-8"));
|
|
108
108
|
} catch {
|
|
109
109
|
return {};
|
|
110
110
|
}
|
|
@@ -1070,14 +1070,14 @@ var os3 = __toESM(require("os"));
|
|
|
1070
1070
|
var fs4 = __toESM(require("fs"));
|
|
1071
1071
|
|
|
1072
1072
|
// src/internal/support/instance-id.ts
|
|
1073
|
-
var
|
|
1073
|
+
var crypto2 = __toESM(require("crypto"));
|
|
1074
1074
|
var os2 = __toESM(require("os"));
|
|
1075
1075
|
function hashForProject(projectName) {
|
|
1076
|
-
return
|
|
1076
|
+
return crypto2.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
|
|
1077
1077
|
}
|
|
1078
1078
|
function computeInstanceId(projectName, runLabel) {
|
|
1079
1079
|
const seed = runLabel ? `${projectName}|${runLabel}` : `${os2.hostname()}|${projectName}`;
|
|
1080
|
-
return
|
|
1080
|
+
return crypto2.createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1081
1081
|
}
|
|
1082
1082
|
|
|
1083
1083
|
// src/internal/streaming/stream-buffer.ts
|
|
@@ -1139,7 +1139,7 @@ var StreamBuffer = class {
|
|
|
1139
1139
|
};
|
|
1140
1140
|
|
|
1141
1141
|
// src/internal/streaming/crash-recovery.ts
|
|
1142
|
-
var
|
|
1142
|
+
var crypto3 = __toESM(require("crypto"));
|
|
1143
1143
|
var fs5 = __toESM(require("fs"));
|
|
1144
1144
|
var os4 = __toESM(require("os"));
|
|
1145
1145
|
var path5 = __toESM(require("path"));
|
|
@@ -1155,7 +1155,7 @@ function recoveryServer(serverUrl) {
|
|
|
1155
1155
|
return serverUrl.replace(/\/+$/, "");
|
|
1156
1156
|
}
|
|
1157
1157
|
function recoveryFilePath(serverUrl, projectName) {
|
|
1158
|
-
const key =
|
|
1158
|
+
const key = crypto3.createHash("sha256").update(`${recoveryServer(serverUrl)}
|
|
1159
1159
|
${projectName}`).digest("hex").slice(0, 16);
|
|
1160
1160
|
return path5.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
|
|
1161
1161
|
}
|
|
@@ -1176,7 +1176,7 @@ var CrashRecovery = class {
|
|
|
1176
1176
|
/** Write the payload for a later retry, owner-only, replacing the file in one step. */
|
|
1177
1177
|
save(payload) {
|
|
1178
1178
|
const file = { v: 1, serverUrl: this.serverUrl, payload };
|
|
1179
|
-
const temp = `${this.filePath}.${process.pid}.${
|
|
1179
|
+
const temp = `${this.filePath}.${process.pid}.${crypto3.randomBytes(6).toString("hex")}.tmp`;
|
|
1180
1180
|
try {
|
|
1181
1181
|
fs5.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
|
|
1182
1182
|
fs5.renameSync(temp, this.filePath);
|
|
@@ -1247,7 +1247,7 @@ var CrashRecovery = class {
|
|
|
1247
1247
|
var fs7 = __toESM(require("fs"));
|
|
1248
1248
|
var os5 = __toESM(require("os"));
|
|
1249
1249
|
var path7 = __toESM(require("path"));
|
|
1250
|
-
var
|
|
1250
|
+
var crypto4 = __toESM(require("crypto"));
|
|
1251
1251
|
|
|
1252
1252
|
// src/internal/files/compression.ts
|
|
1253
1253
|
var fs6 = __toESM(require("fs"));
|
|
@@ -1332,6 +1332,19 @@ var ATTACHMENT_NAMES = {
|
|
|
1332
1332
|
pauses: "piwi-pauses"
|
|
1333
1333
|
};
|
|
1334
1334
|
var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
1335
|
+
var ARIA_SNAPSHOT_CONTENT_TYPE = "text/plain; charset=utf-8";
|
|
1336
|
+
function contentTypeOf(name) {
|
|
1337
|
+
return name === ATTACHMENT_NAMES.ariaSnapshot ? ARIA_SNAPSHOT_CONTENT_TYPE : "application/json";
|
|
1338
|
+
}
|
|
1339
|
+
function isPiwiAttachment(attachment, name) {
|
|
1340
|
+
return attachment.name === name && attachment.contentType === contentTypeOf(name);
|
|
1341
|
+
}
|
|
1342
|
+
function findPiwiAttachment(attachments, name) {
|
|
1343
|
+
for (let i = (attachments?.length ?? 0) - 1; i >= 0; i--) {
|
|
1344
|
+
if (isPiwiAttachment(attachments[i], name)) return attachments[i];
|
|
1345
|
+
}
|
|
1346
|
+
return void 0;
|
|
1347
|
+
}
|
|
1335
1348
|
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
1336
1349
|
var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
|
|
1337
1350
|
|
|
@@ -1450,18 +1463,18 @@ var FileHandler = class {
|
|
|
1450
1463
|
}
|
|
1451
1464
|
const contentType = a.contentType || "application/octet-stream";
|
|
1452
1465
|
const originalName = `${safeFileName(a.name || "attachment")}${BODY_EXTENSIONS[contentType] ?? ""}`;
|
|
1453
|
-
let
|
|
1454
|
-
if (!
|
|
1466
|
+
let filePath3 = this.bodyFiles.get(a);
|
|
1467
|
+
if (!filePath3) {
|
|
1455
1468
|
try {
|
|
1456
|
-
|
|
1457
|
-
fs7.writeFileSync(
|
|
1458
|
-
this.bodyFiles.set(a,
|
|
1469
|
+
filePath3 = path7.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
|
|
1470
|
+
fs7.writeFileSync(filePath3, body);
|
|
1471
|
+
this.bodyFiles.set(a, filePath3);
|
|
1459
1472
|
} catch (error) {
|
|
1460
1473
|
this.logger.warn(`Could not stage attachment "${a.name}" for upload: ${errorMessage(error)}`);
|
|
1461
1474
|
return null;
|
|
1462
1475
|
}
|
|
1463
1476
|
}
|
|
1464
|
-
return { name: a.name || "attachment", path:
|
|
1477
|
+
return { name: a.name || "attachment", path: filePath3, contentType, originalName };
|
|
1465
1478
|
}
|
|
1466
1479
|
bodyAttachmentDir() {
|
|
1467
1480
|
if (!this.bodyDir) this.bodyDir = fs7.mkdtempSync(path7.join(os5.tmpdir(), "piwi-dashboard-attachments-"));
|
|
@@ -1486,11 +1499,11 @@ var FileHandler = class {
|
|
|
1486
1499
|
}
|
|
1487
1500
|
/** Parse Piwi-internal attachment bodies (`piwi-network`, `piwi-web-vitals`, `piwi-console`, `piwi-aria-snapshot`, `piwi-aria-snapshot-json`) into structured fields on the test case */
|
|
1488
1501
|
parsePerformanceAttachments(testCase, attachments) {
|
|
1489
|
-
const find = (name) => attachments
|
|
1490
|
-
const
|
|
1491
|
-
if (
|
|
1502
|
+
const find = (name) => findPiwiAttachment(attachments, name);
|
|
1503
|
+
const net5 = find(ATTACHMENT_NAMES.network);
|
|
1504
|
+
if (net5?.body) {
|
|
1492
1505
|
try {
|
|
1493
|
-
testCase.networkRequests = JSON.parse(
|
|
1506
|
+
testCase.networkRequests = JSON.parse(net5.body.toString());
|
|
1494
1507
|
} catch {
|
|
1495
1508
|
}
|
|
1496
1509
|
}
|
|
@@ -1549,7 +1562,7 @@ var FileHandler = class {
|
|
|
1549
1562
|
if (fs7.existsSync(tp)) lastPath = tp;
|
|
1550
1563
|
}
|
|
1551
1564
|
if (!lastPath) return null;
|
|
1552
|
-
const hash =
|
|
1565
|
+
const hash = crypto4.createHash("sha256");
|
|
1553
1566
|
await new Promise((resolve11, reject) => {
|
|
1554
1567
|
fs7.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve11).on("error", reject);
|
|
1555
1568
|
});
|
|
@@ -3387,6 +3400,258 @@ function maskUrlFragment(hash) {
|
|
|
3387
3400
|
const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
|
|
3388
3401
|
return hash.startsWith("#") ? `#${out}` : out;
|
|
3389
3402
|
}
|
|
3403
|
+
var HIDDEN_VALUE = "<hidden>";
|
|
3404
|
+
var SECRET_AUTOCOMPLETE = [
|
|
3405
|
+
"current-password",
|
|
3406
|
+
"new-password",
|
|
3407
|
+
"one-time-code",
|
|
3408
|
+
"cc-number",
|
|
3409
|
+
"cc-csc"
|
|
3410
|
+
];
|
|
3411
|
+
var SECRET_WORDS = /* @__PURE__ */ new Set([
|
|
3412
|
+
"pass",
|
|
3413
|
+
"password",
|
|
3414
|
+
"passwd",
|
|
3415
|
+
"passphrase",
|
|
3416
|
+
"passcode",
|
|
3417
|
+
"pwd",
|
|
3418
|
+
"secret",
|
|
3419
|
+
"secrets",
|
|
3420
|
+
"token",
|
|
3421
|
+
"tokens",
|
|
3422
|
+
"jwt",
|
|
3423
|
+
"bearer",
|
|
3424
|
+
"sig",
|
|
3425
|
+
"signature",
|
|
3426
|
+
"key",
|
|
3427
|
+
"keys",
|
|
3428
|
+
"apikey",
|
|
3429
|
+
"auth",
|
|
3430
|
+
"oauth",
|
|
3431
|
+
"authorization",
|
|
3432
|
+
"session",
|
|
3433
|
+
"sessionid",
|
|
3434
|
+
"jsessionid",
|
|
3435
|
+
"sid",
|
|
3436
|
+
"cookie",
|
|
3437
|
+
"cookies",
|
|
3438
|
+
"credential",
|
|
3439
|
+
"credentials",
|
|
3440
|
+
"otp",
|
|
3441
|
+
"refresh",
|
|
3442
|
+
"csrf",
|
|
3443
|
+
"xsrf"
|
|
3444
|
+
]);
|
|
3445
|
+
var SECRET_NAMES = /* @__PURE__ */ new Set(["code", "pin"]);
|
|
3446
|
+
function nameWords(name) {
|
|
3447
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
3448
|
+
}
|
|
3449
|
+
function isSecretName(name) {
|
|
3450
|
+
let secret = namesRead.get(name);
|
|
3451
|
+
if (secret === void 0) {
|
|
3452
|
+
secret = SECRET_NAMES.has(name.trim().toLowerCase()) || nameWords(name).some((word) => SECRET_WORDS.has(word));
|
|
3453
|
+
if (namesRead.size >= 1024) namesRead.clear();
|
|
3454
|
+
namesRead.set(name, secret);
|
|
3455
|
+
}
|
|
3456
|
+
return secret;
|
|
3457
|
+
}
|
|
3458
|
+
var namesRead = /* @__PURE__ */ new Map();
|
|
3459
|
+
var SECRET_SHAPES = [
|
|
3460
|
+
/\beyJ[\w-]{5,}\.[\w-]{5,}\.[\w-]*/g,
|
|
3461
|
+
/\bBearer(?:\s+|%20)[\w.~+/-]{8,}=*/gi,
|
|
3462
|
+
/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
|
|
3463
|
+
/\bwhsec_[A-Za-z0-9]{20,}/g,
|
|
3464
|
+
/\bsk-(?=[\w-]{0,200}\d)(?=[\w-]{0,200}[A-Z])[\w-]{32,}/g,
|
|
3465
|
+
/\bglpat-[\w-]{20,}/g,
|
|
3466
|
+
/\bnpm_[A-Za-z0-9]{36}\b/g,
|
|
3467
|
+
/\bSK[0-9a-f]{32}\b/g,
|
|
3468
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
3469
|
+
/\bAIza[\w-]{30,}/g,
|
|
3470
|
+
/\bgh[opsu]_[A-Za-z0-9]{20,}/g,
|
|
3471
|
+
/\bgithub_pat_\w{20,}/g,
|
|
3472
|
+
/\bxox[abprs]-[\w-]{8,}/g
|
|
3473
|
+
];
|
|
3474
|
+
var WHOLE_SHAPES = SECRET_SHAPES.map(
|
|
3475
|
+
(re) => new RegExp(`^(?:${re.source})$`, re.flags.replace("g", ""))
|
|
3476
|
+
);
|
|
3477
|
+
var BASIC = /\bBasic\s+([A-Za-z0-9+/]{4,}={0,2})(?![\w+/=])/g;
|
|
3478
|
+
var PEM_BEGIN = /-----BEGIN ([A-Z0-9 ]{0,40}PRIVATE KEY)-----/g;
|
|
3479
|
+
var SET_COOKIE = /(\bSet-Cookie:[ \t]*[^=;\s]{1,256}=)([^;\r\n]{1,4096})/gi;
|
|
3480
|
+
var COOKIE_LINE = /((?<![\w-])Cookie:[ \t]*)([^\r\n]{1,16384})/gi;
|
|
3481
|
+
var JSON_FIELD = /("((?:[^"\\\r\n]|\\.){1,256})"\s*:\s*")((?:[^"\\\r\n]|\\.){1,4096})"/g;
|
|
3482
|
+
var TOKEN_RUN = /[A-Za-z0-9+/_-]{40,}={0,2}/g;
|
|
3483
|
+
var MIN_RANDOM = 40;
|
|
3484
|
+
var BASE64_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
3485
|
+
function base64OfText(run) {
|
|
3486
|
+
let bits = 0;
|
|
3487
|
+
let acc = 0;
|
|
3488
|
+
let bytes = 0;
|
|
3489
|
+
let printable = 0;
|
|
3490
|
+
for (const c of run.replace(/=+$/, "")) {
|
|
3491
|
+
const digit = BASE64_DIGITS.indexOf(c === "-" ? "+" : c === "_" ? "/" : c);
|
|
3492
|
+
if (digit < 0) return false;
|
|
3493
|
+
acc = (acc << 6 | digit) & 65535;
|
|
3494
|
+
bits += 6;
|
|
3495
|
+
if (bits < 8) continue;
|
|
3496
|
+
bits -= 8;
|
|
3497
|
+
const byte = acc >> bits & 255;
|
|
3498
|
+
bytes++;
|
|
3499
|
+
if (byte >= 32 && byte < 127 || byte === 9 || byte === 10 || byte === 13) printable++;
|
|
3500
|
+
}
|
|
3501
|
+
return bytes > 0 && printable * 10 >= bytes * 9;
|
|
3502
|
+
}
|
|
3503
|
+
function bitsPerChar(text) {
|
|
3504
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3505
|
+
for (const c of text) counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
3506
|
+
let bits = 0;
|
|
3507
|
+
for (const n of counts.values()) bits -= n / text.length * Math.log2(n / text.length);
|
|
3508
|
+
return bits;
|
|
3509
|
+
}
|
|
3510
|
+
function looksRandom(text) {
|
|
3511
|
+
if (text.length < MIN_RANDOM || !/[A-Z]/.test(text) || !/[a-z]/.test(text) || !/\d/.test(text)) return false;
|
|
3512
|
+
let letters = 0;
|
|
3513
|
+
for (const c of text) if (c >= "a" && c <= "z" || c >= "A" && c <= "Z") letters++;
|
|
3514
|
+
let wordy = 0;
|
|
3515
|
+
for (const run of text.matchAll(/[a-z]{6,}|[A-Z]{6,}/g)) wordy += run[0].length;
|
|
3516
|
+
return wordy * 5 <= letters && bitsPerChar(text) >= 4 && !base64OfText(text);
|
|
3517
|
+
}
|
|
3518
|
+
function isBasicCredential(encoded) {
|
|
3519
|
+
const text = encoded.replace(/=+$/, "");
|
|
3520
|
+
if (text.length % 4 === 1) return false;
|
|
3521
|
+
let decoded = "";
|
|
3522
|
+
let bits = 0;
|
|
3523
|
+
let acc = 0;
|
|
3524
|
+
for (const c of text) {
|
|
3525
|
+
const digit = BASE64_DIGITS.indexOf(c);
|
|
3526
|
+
if (digit < 0) return false;
|
|
3527
|
+
acc = (acc << 6 | digit) & 65535;
|
|
3528
|
+
bits += 6;
|
|
3529
|
+
if (bits < 8) continue;
|
|
3530
|
+
bits -= 8;
|
|
3531
|
+
decoded += String.fromCharCode(acc >> bits & 255);
|
|
3532
|
+
}
|
|
3533
|
+
return /^[\x20-\x7e]+:[\x20-\x7e]*$/.test(decoded);
|
|
3534
|
+
}
|
|
3535
|
+
function isSecretValue(value) {
|
|
3536
|
+
const text = value.trim();
|
|
3537
|
+
if (text.length < 10 || text.length > 4096) return false;
|
|
3538
|
+
if (WHOLE_SHAPES.some((re) => re.test(text))) return true;
|
|
3539
|
+
const basic = /^Basic\s+([A-Za-z0-9+/]{4,}={0,2})$/.exec(text);
|
|
3540
|
+
if (basic && isBasicCredential(basic[1])) return true;
|
|
3541
|
+
if (/^-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----/.test(text)) return true;
|
|
3542
|
+
return /^[A-Za-z0-9+/_-]{40,}={0,2}$/.test(text) && looksRandom(text);
|
|
3543
|
+
}
|
|
3544
|
+
var URL_IN_TEXT = /\b[a-z][a-z0-9+.-]{0,31}:\/\/[^\s"'<>`]+/gi;
|
|
3545
|
+
var URL_PARTS = /^(?:([a-z][a-z0-9+.-]*:)?\/\/([^/?#]*))?([^?#]*)(\?[^#]*)?(#[\s\S]*)?$/i;
|
|
3546
|
+
function decodeSafe(text) {
|
|
3547
|
+
try {
|
|
3548
|
+
return decodeURIComponent(text);
|
|
3549
|
+
} catch {
|
|
3550
|
+
return text;
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
function hider(count) {
|
|
3554
|
+
return () => {
|
|
3555
|
+
count.n++;
|
|
3556
|
+
return HIDDEN_VALUE;
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
function hideParams(params, count) {
|
|
3560
|
+
const hide = hider(count);
|
|
3561
|
+
return params.split(/([&;])/).map((part, i) => {
|
|
3562
|
+
const eq = part.indexOf("=");
|
|
3563
|
+
if (i % 2 === 1 || eq < 0) return part;
|
|
3564
|
+
const name = part.slice(0, eq);
|
|
3565
|
+
const value = part.slice(eq + 1);
|
|
3566
|
+
const decoded = decodeSafe(value);
|
|
3567
|
+
if (!value || decoded === HIDDEN_VALUE) return part;
|
|
3568
|
+
return isSecretName(decodeSafe(name)) || isSecretValue(decoded) ? `${name}=${hide()}` : part;
|
|
3569
|
+
}).join("");
|
|
3570
|
+
}
|
|
3571
|
+
function hidePath(path34, count) {
|
|
3572
|
+
const hide = hider(count);
|
|
3573
|
+
return path34.split("/").map((segment) => {
|
|
3574
|
+
const [own = "", ...params] = segment.split(";");
|
|
3575
|
+
const decoded = decodeSafe(own);
|
|
3576
|
+
const kept = decoded !== HIDDEN_VALUE && isSecretValue(decoded) ? hide() : own;
|
|
3577
|
+
return params.length ? `${kept};${hideParams(params.join(";"), count)}` : kept;
|
|
3578
|
+
}).join("/");
|
|
3579
|
+
}
|
|
3580
|
+
function hideSecretsInUrl(url, count = { n: 0 }) {
|
|
3581
|
+
const parts = URL_PARTS.exec(url);
|
|
3582
|
+
if (!parts) return url;
|
|
3583
|
+
const [, scheme = "", authority, path34 = "", query = "", fragment = ""] = parts;
|
|
3584
|
+
let out = "";
|
|
3585
|
+
if (authority !== void 0) {
|
|
3586
|
+
const at = authority.lastIndexOf("@");
|
|
3587
|
+
const colon = at >= 0 ? authority.slice(0, at).indexOf(":") : -1;
|
|
3588
|
+
out += colon >= 0 && colon < at - 1 ? `${scheme}//${authority.slice(0, colon + 1)}${hider(count)()}${authority.slice(at)}` : `${scheme}//${authority}`;
|
|
3589
|
+
}
|
|
3590
|
+
out += hidePath(path34, count);
|
|
3591
|
+
if (query) out += `?${hideParams(query.slice(1), count)}`;
|
|
3592
|
+
if (fragment) {
|
|
3593
|
+
const body = fragment.slice(1);
|
|
3594
|
+
const ask = body.indexOf("?");
|
|
3595
|
+
out += `#${ask >= 0 ? `${hidePath(body.slice(0, ask), count)}?${hideParams(body.slice(ask + 1), count)}` : body.includes("=") ? hideParams(body, count) : hidePath(body, count)}`;
|
|
3596
|
+
}
|
|
3597
|
+
return out;
|
|
3598
|
+
}
|
|
3599
|
+
var QUERY_PAIR = /([?&;#])([^=&#?;\s"'<>]{1,256})=([^&;#\s"'<>]*)/g;
|
|
3600
|
+
var URL_PASSWORD = /(\/\/[^/:@\s"'<>]{1,256}:)([^@/\s"'<>]{1,256})(@)/g;
|
|
3601
|
+
function hidePem(text, hide) {
|
|
3602
|
+
let out = "";
|
|
3603
|
+
let last = 0;
|
|
3604
|
+
let nextEnd = -2;
|
|
3605
|
+
PEM_BEGIN.lastIndex = 0;
|
|
3606
|
+
for (let begin = PEM_BEGIN.exec(text); begin; begin = PEM_BEGIN.exec(text)) {
|
|
3607
|
+
if (begin.index < last) continue;
|
|
3608
|
+
if (nextEnd !== -1 && nextEnd < begin.index) nextEnd = text.indexOf("-----END ", begin.index);
|
|
3609
|
+
const footer = nextEnd >= 0 ? /^-----END [A-Z0-9 ]{0,40}-----/.exec(text.slice(nextEnd, nextEnd + 60)) : null;
|
|
3610
|
+
const until = footer ? nextEnd + footer[0].length : begin.index + begin[0].length + (/^(?:\s*[A-Za-z0-9+/=]+)*/.exec(text.slice(begin.index + begin[0].length))?.[0].length ?? 0);
|
|
3611
|
+
out += `${text.slice(last, begin.index)}${hide()}`;
|
|
3612
|
+
last = until;
|
|
3613
|
+
PEM_BEGIN.lastIndex = until;
|
|
3614
|
+
}
|
|
3615
|
+
return out + text.slice(last);
|
|
3616
|
+
}
|
|
3617
|
+
function hideInText(text, count) {
|
|
3618
|
+
if (text.length < 4 || !/[A-Za-z0-9]/.test(text)) return text;
|
|
3619
|
+
const hide = hider(count);
|
|
3620
|
+
let out = hidePem(text, hide);
|
|
3621
|
+
out = out.replace(QUERY_PAIR, (all, sep13, name, value) => {
|
|
3622
|
+
const decoded = decodeSafe(value);
|
|
3623
|
+
if (!value || decoded === HIDDEN_VALUE) return all;
|
|
3624
|
+
return isSecretName(name) || isSecretValue(decoded) ? `${sep13}${name}=${hide()}` : all;
|
|
3625
|
+
});
|
|
3626
|
+
out = out.replace(URL_PASSWORD, (_all, before, _password, at) => `${before}${hide()}${at}`);
|
|
3627
|
+
out = out.replace(SET_COOKIE, (_all, before) => `${before}${hide()}`);
|
|
3628
|
+
out = out.replace(COOKIE_LINE, (_all, before, pairs) => {
|
|
3629
|
+
const values = pairs.replace(
|
|
3630
|
+
/([^=;\s]{1,256}=)([^;]*)/g,
|
|
3631
|
+
(pair, name, value) => value.trim() && value.trim() !== HIDDEN_VALUE ? `${name}${hide()}` : pair
|
|
3632
|
+
);
|
|
3633
|
+
return `${before}${values}`;
|
|
3634
|
+
});
|
|
3635
|
+
out = out.replace(
|
|
3636
|
+
JSON_FIELD,
|
|
3637
|
+
(all, before, name, value) => isSecretName(name) && value !== HIDDEN_VALUE ? `${before}${hide()}"` : all
|
|
3638
|
+
);
|
|
3639
|
+
out = out.replace(BASIC, (all, encoded) => isBasicCredential(encoded) ? hide() : all);
|
|
3640
|
+
for (const shape of SECRET_SHAPES) out = out.replace(shape, () => hide());
|
|
3641
|
+
return out.replace(
|
|
3642
|
+
TOKEN_RUN,
|
|
3643
|
+
(run) => run.includes("/") && !/[+=]/.test(run) ? run.split("/").map((part) => looksRandom(part) ? hide() : part).join("/") : looksRandom(run) ? hide() : run
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
function hideSecretsInText(text, count = { n: 0 }) {
|
|
3647
|
+
let out = "";
|
|
3648
|
+
let last = 0;
|
|
3649
|
+
for (const url of text.matchAll(URL_IN_TEXT)) {
|
|
3650
|
+
out += hideInText(text.slice(last, url.index), count) + hideSecretsInUrl(url[0], count);
|
|
3651
|
+
last = url.index + url[0].length;
|
|
3652
|
+
}
|
|
3653
|
+
return out + hideInText(text.slice(last), count);
|
|
3654
|
+
}
|
|
3390
3655
|
|
|
3391
3656
|
// ../core/src/locator-break.ts
|
|
3392
3657
|
var filePath = (path34) => path34.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -4060,8 +4325,8 @@ var fs15 = __toESM(require("fs"));
|
|
|
4060
4325
|
var path12 = __toESM(require("path"));
|
|
4061
4326
|
var os7 = __toESM(require("os"));
|
|
4062
4327
|
var fs12 = __toESM(require("fs"));
|
|
4063
|
-
function ariaSampleIdentity(
|
|
4064
|
-
return `${
|
|
4328
|
+
function ariaSampleIdentity(filePath3, title) {
|
|
4329
|
+
return `${filePath3}\0${title}`;
|
|
4065
4330
|
}
|
|
4066
4331
|
function getAriaSampleFilePath(projectName) {
|
|
4067
4332
|
return path12.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
|
|
@@ -4189,9 +4454,9 @@ function reachedRemotely(use, url, env) {
|
|
|
4189
4454
|
if (use?.connectOptions === void 0) {
|
|
4190
4455
|
return !!env.PW_TEST_CONNECT_WS_ENDPOINT && !exposeNetworkCovers(env.PW_TEST_CONNECT_EXPOSE_NETWORK, url);
|
|
4191
4456
|
}
|
|
4192
|
-
const
|
|
4193
|
-
if (!
|
|
4194
|
-
return !exposeNetworkCovers(typeof
|
|
4457
|
+
const connect3 = use.connectOptions;
|
|
4458
|
+
if (!connect3 || typeof connect3 !== "object" || typeof connect3.wsEndpoint !== "string") return false;
|
|
4459
|
+
return !exposeNetworkCovers(typeof connect3.exposeNetwork === "string" ? connect3.exposeNetwork : void 0, url);
|
|
4195
4460
|
}
|
|
4196
4461
|
function baseUrlTargets(config, argv = process.argv, env = process.env) {
|
|
4197
4462
|
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
@@ -4608,7 +4873,7 @@ function reduceCoverage(capture, granularity) {
|
|
|
4608
4873
|
}
|
|
4609
4874
|
|
|
4610
4875
|
// src/internal/collect/source-checksums.ts
|
|
4611
|
-
var
|
|
4876
|
+
var crypto5 = __toESM(require("crypto"));
|
|
4612
4877
|
var fs14 = __toESM(require("fs"));
|
|
4613
4878
|
var path14 = __toESM(require("path"));
|
|
4614
4879
|
var CHECKSUM = /^(sha1|sha256):[0-9a-f]{40,64}$/;
|
|
@@ -4626,7 +4891,7 @@ function contentChecksums(content) {
|
|
|
4626
4891
|
const out = /* @__PURE__ */ new Set();
|
|
4627
4892
|
for (const form of lineEndingForms(content)) {
|
|
4628
4893
|
for (const algorithm of ["sha1", "sha256"]) {
|
|
4629
|
-
out.add(`${algorithm}:${
|
|
4894
|
+
out.add(`${algorithm}:${crypto5.createHash(algorithm).update(form).digest("hex")}`);
|
|
4630
4895
|
}
|
|
4631
4896
|
}
|
|
4632
4897
|
return out;
|
|
@@ -4738,10 +5003,10 @@ function fileChecksums(files, into = /* @__PURE__ */ new Map()) {
|
|
|
4738
5003
|
if (!Array.isArray(files)) return into;
|
|
4739
5004
|
for (const item of files) {
|
|
4740
5005
|
if (!isRecord2(item)) continue;
|
|
4741
|
-
const
|
|
5006
|
+
const filePath3 = normalizeCoveragePath(item.path);
|
|
4742
5007
|
const checksums = checksumsOf(item.checksums);
|
|
4743
|
-
if (
|
|
4744
|
-
into.set(
|
|
5008
|
+
if (filePath3 === null || checksums.length === 0) continue;
|
|
5009
|
+
into.set(filePath3, [.../* @__PURE__ */ new Set([...into.get(filePath3) ?? [], ...checksums])]);
|
|
4745
5010
|
}
|
|
4746
5011
|
return into;
|
|
4747
5012
|
}
|
|
@@ -4750,9 +5015,9 @@ function runFilesOf(answer) {
|
|
|
4750
5015
|
const files = [];
|
|
4751
5016
|
for (const item of answer.files) {
|
|
4752
5017
|
if (!isRecord2(item) || item.runtime !== "server" && item.runtime !== "browser") continue;
|
|
4753
|
-
const
|
|
4754
|
-
if (
|
|
4755
|
-
const file = { path:
|
|
5018
|
+
const filePath3 = normalizeCoveragePath(item.path);
|
|
5019
|
+
if (filePath3 === null) continue;
|
|
5020
|
+
const file = { path: filePath3, runtime: item.runtime };
|
|
4756
5021
|
const functions = normalizeFunctionRanges(item.functions);
|
|
4757
5022
|
const lines = normalizeLineRanges(item.lines);
|
|
4758
5023
|
const executable = normalizeLineRanges(item.executable);
|
|
@@ -5375,9 +5640,9 @@ function loadPlan() {
|
|
|
5375
5640
|
function probeItemForTest(test) {
|
|
5376
5641
|
const plan = loadPlan();
|
|
5377
5642
|
if (!plan) return null;
|
|
5378
|
-
const
|
|
5643
|
+
const filePath3 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
|
|
5379
5644
|
const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
|
|
5380
|
-
return matchProbeItem(plan, { title: test.title, filePath:
|
|
5645
|
+
return matchProbeItem(plan, { title: test.title, filePath: filePath3, suitePath });
|
|
5381
5646
|
}
|
|
5382
5647
|
function classifyProbeHandled(signals) {
|
|
5383
5648
|
const blank = signals.blankPage === true;
|
|
@@ -6372,9 +6637,9 @@ function duplicateTestKey(test) {
|
|
|
6372
6637
|
}
|
|
6373
6638
|
function readDuplicateTest(raw) {
|
|
6374
6639
|
if (!raw || typeof raw !== "object") return null;
|
|
6375
|
-
const { filePath:
|
|
6376
|
-
if (typeof
|
|
6377
|
-
const test = { filePath:
|
|
6640
|
+
const { filePath: filePath3, suitePath, title, line } = raw;
|
|
6641
|
+
if (typeof filePath3 !== "string" || typeof title !== "string") return null;
|
|
6642
|
+
const test = { filePath: filePath3, title };
|
|
6378
6643
|
if (Array.isArray(suitePath) && suitePath.every((s) => typeof s === "string") && suitePath.length > 0)
|
|
6379
6644
|
test.suitePath = suitePath;
|
|
6380
6645
|
if (typeof line === "number" && Number.isFinite(line)) test.line = line;
|
|
@@ -6931,7 +7196,9 @@ var RunSubmitter = class {
|
|
|
6931
7196
|
};
|
|
6932
7197
|
|
|
6933
7198
|
// src/internal/capture/resource-ledger.ts
|
|
7199
|
+
var childProcess = __toESM(require("child_process"));
|
|
6934
7200
|
var fs21 = __toESM(require("fs"));
|
|
7201
|
+
var net2 = __toESM(require("net"));
|
|
6935
7202
|
var path20 = __toESM(require("path"));
|
|
6936
7203
|
var import_node_module2 = require("module");
|
|
6937
7204
|
|
|
@@ -7309,6 +7576,8 @@ var WorkerMetrics = class {
|
|
|
7309
7576
|
constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
|
|
7310
7577
|
this.proc = proc;
|
|
7311
7578
|
this.pid = pid;
|
|
7579
|
+
/** The lowest heap read right after a full garbage collection in the current window. */
|
|
7580
|
+
this.heapAfterGc = null;
|
|
7312
7581
|
this.loop = null;
|
|
7313
7582
|
this.usage = null;
|
|
7314
7583
|
/** Each process's CPU and wait at the start of the current window; null before the first. */
|
|
@@ -7323,6 +7592,28 @@ var WorkerMetrics = class {
|
|
|
7323
7592
|
histogram = null;
|
|
7324
7593
|
}
|
|
7325
7594
|
this.histogram = histogram;
|
|
7595
|
+
this.gcObserver = this.observeFullCollections();
|
|
7596
|
+
}
|
|
7597
|
+
/**
|
|
7598
|
+
* Read the heap after each full (major) garbage collection V8 runs on its
|
|
7599
|
+
* own: nothing is forced, so a test that triggers none reads null.
|
|
7600
|
+
*/
|
|
7601
|
+
observeFullCollections() {
|
|
7602
|
+
try {
|
|
7603
|
+
const observer = new import_node_perf_hooks.PerformanceObserver((list) => {
|
|
7604
|
+
for (const entry of list.getEntries()) {
|
|
7605
|
+
const detail = entry.detail;
|
|
7606
|
+
const kind = detail?.kind ?? entry.kind;
|
|
7607
|
+
if (kind !== import_node_perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR) continue;
|
|
7608
|
+
const heap = process.memoryUsage().heapUsed;
|
|
7609
|
+
this.heapAfterGc = Math.min(this.heapAfterGc ?? Infinity, heap);
|
|
7610
|
+
}
|
|
7611
|
+
});
|
|
7612
|
+
observer.observe({ entryTypes: ["gc"] });
|
|
7613
|
+
return observer;
|
|
7614
|
+
} catch {
|
|
7615
|
+
return null;
|
|
7616
|
+
}
|
|
7326
7617
|
}
|
|
7327
7618
|
/** The worker's processes under it, keyed by pid and start time. */
|
|
7328
7619
|
tree() {
|
|
@@ -7356,6 +7647,7 @@ var WorkerMetrics = class {
|
|
|
7356
7647
|
start(processes = true) {
|
|
7357
7648
|
try {
|
|
7358
7649
|
this.histogram?.reset();
|
|
7650
|
+
this.heapAfterGc = null;
|
|
7359
7651
|
this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
|
|
7360
7652
|
this.usage = process.resourceUsage();
|
|
7361
7653
|
if (this.linux && processes && !this.subtree) this.snapshot();
|
|
@@ -7378,6 +7670,7 @@ var WorkerMetrics = class {
|
|
|
7378
7670
|
loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
|
|
7379
7671
|
loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
|
|
7380
7672
|
heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
|
|
7673
|
+
heapAfterGcMb: this.heapAfterGc === null ? null : round1(this.heapAfterGc / MB),
|
|
7381
7674
|
fds: this.linux ? this.proc.fdCount(this.pid) : null
|
|
7382
7675
|
}
|
|
7383
7676
|
};
|
|
@@ -7412,6 +7705,7 @@ var WorkerMetrics = class {
|
|
|
7412
7705
|
}
|
|
7413
7706
|
dispose() {
|
|
7414
7707
|
this.histogram?.disable();
|
|
7708
|
+
this.gcObserver?.disconnect();
|
|
7415
7709
|
}
|
|
7416
7710
|
};
|
|
7417
7711
|
var PageReader = class {
|
|
@@ -7433,12 +7727,36 @@ var PageReader = class {
|
|
|
7433
7727
|
try {
|
|
7434
7728
|
const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
|
|
7435
7729
|
const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
|
|
7436
|
-
|
|
7730
|
+
const main = {
|
|
7437
7731
|
cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
|
|
7438
7732
|
heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
|
|
7439
7733
|
nodes: values.get("Nodes") ?? 0,
|
|
7440
7734
|
listeners: values.get("JSEventListeners") ?? 0
|
|
7441
7735
|
};
|
|
7736
|
+
const elements = await this.countElements(page, opened);
|
|
7737
|
+
if (elements !== null) main.elements = elements;
|
|
7738
|
+
return main;
|
|
7739
|
+
} catch {
|
|
7740
|
+
return null;
|
|
7741
|
+
}
|
|
7742
|
+
}
|
|
7743
|
+
/**
|
|
7744
|
+
* The elements attached to the page's main document, through the page's own
|
|
7745
|
+
* DevTools session: no Playwright call, so no step and no trace action. The
|
|
7746
|
+
* count runs in the page's main world and changes nothing in it.
|
|
7747
|
+
*/
|
|
7748
|
+
async countElements(page, session) {
|
|
7749
|
+
try {
|
|
7750
|
+
const result = await internalCall(
|
|
7751
|
+
page,
|
|
7752
|
+
() => session.send("Runtime.evaluate", {
|
|
7753
|
+
expression: "document.getElementsByTagName('*').length",
|
|
7754
|
+
returnByValue: true,
|
|
7755
|
+
silent: true
|
|
7756
|
+
})
|
|
7757
|
+
);
|
|
7758
|
+
const value = result.result?.value;
|
|
7759
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
7442
7760
|
} catch {
|
|
7443
7761
|
return null;
|
|
7444
7762
|
}
|
|
@@ -7447,6 +7765,9 @@ var PageReader = class {
|
|
|
7447
7765
|
|
|
7448
7766
|
// src/internal/capture/resource-ledger.ts
|
|
7449
7767
|
var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
|
|
7768
|
+
var COUNTED_WITH_RUNNING = ["FSEventWrap", "StatWatcher"];
|
|
7769
|
+
var PLAYWRIGHT_ROLES = /* @__PURE__ */ new Set(["worker", "runner", "browser", "renderer", "gpu", "utility", "ffmpeg"]);
|
|
7770
|
+
var HANDLE_STACK_LIMIT = 40;
|
|
7450
7771
|
var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
|
|
7451
7772
|
var CLOSE_TIMEOUT_MS = 5e3;
|
|
7452
7773
|
var PAGE_READ_CAP = 50;
|
|
@@ -7522,20 +7843,41 @@ function playwrightTestInfoSource() {
|
|
|
7522
7843
|
function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
|
|
7523
7844
|
return value === "fail" || value === "close" ? value : "report";
|
|
7524
7845
|
}
|
|
7525
|
-
function countHandles() {
|
|
7846
|
+
function countHandles(types = TRACKED_HANDLE_TYPES) {
|
|
7526
7847
|
const counts = {};
|
|
7527
7848
|
const read = process.getActiveResourcesInfo;
|
|
7528
7849
|
if (typeof read !== "function") return counts;
|
|
7529
7850
|
for (const type of read.call(process)) {
|
|
7530
|
-
if (
|
|
7851
|
+
if (types.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
|
|
7531
7852
|
}
|
|
7532
7853
|
return counts;
|
|
7533
7854
|
}
|
|
7534
|
-
async function settledHandles(start) {
|
|
7535
|
-
const end = countHandles();
|
|
7855
|
+
async function settledHandles(start, types = TRACKED_HANDLE_TYPES) {
|
|
7856
|
+
const end = countHandles(types);
|
|
7536
7857
|
if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
|
|
7537
7858
|
await new Promise((resolve11) => setTimeout(resolve11, 0));
|
|
7538
|
-
return countHandles();
|
|
7859
|
+
return countHandles(types);
|
|
7860
|
+
}
|
|
7861
|
+
function deepUserSite() {
|
|
7862
|
+
const limit = Error.stackTraceLimit;
|
|
7863
|
+
Error.stackTraceLimit = HANDLE_STACK_LIMIT;
|
|
7864
|
+
try {
|
|
7865
|
+
return userSite(new Error().stack);
|
|
7866
|
+
} finally {
|
|
7867
|
+
Error.stackTraceLimit = limit;
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7870
|
+
var SHELLS = /^(?:sh|bash|dash|zsh|cmd|cmd\.exe|powershell|powershell\.exe|pwsh)$/i;
|
|
7871
|
+
function commandOf(spawnfile, spawnargs) {
|
|
7872
|
+
const name = (file) => path20.basename(file.replace(/\\/g, "/").replace(/^["']|["']$/g, ""));
|
|
7873
|
+
const program = typeof spawnfile === "string" ? name(spawnfile) : "";
|
|
7874
|
+
const args = Array.isArray(spawnargs) ? spawnargs.map(String) : [];
|
|
7875
|
+
const script = args.findIndex((arg) => arg === "-c" || /^\/c$/i.test(arg));
|
|
7876
|
+
if (SHELLS.test(program) && script >= 0 && args[script + 1]) {
|
|
7877
|
+
const first = args[script + 1].trim().split(/\s+/)[0];
|
|
7878
|
+
if (first) return name(first);
|
|
7879
|
+
}
|
|
7880
|
+
return program || "a child process";
|
|
7539
7881
|
}
|
|
7540
7882
|
function settleWithin(promise, ms) {
|
|
7541
7883
|
let timer;
|
|
@@ -7547,7 +7889,7 @@ function settleWithin(promise, ms) {
|
|
|
7547
7889
|
]).finally(() => clearTimeout(timer));
|
|
7548
7890
|
}
|
|
7549
7891
|
var ResourceLedger = class {
|
|
7550
|
-
constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
|
|
7892
|
+
constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}, nodeHandles = false) {
|
|
7551
7893
|
this.playwright = playwright;
|
|
7552
7894
|
this.workerIndex = workerIndex;
|
|
7553
7895
|
this.leakCheck = leakCheck;
|
|
@@ -7555,6 +7897,7 @@ var ResourceLedger = class {
|
|
|
7555
7897
|
this.testInfoSource = testInfoSource;
|
|
7556
7898
|
this.now = now;
|
|
7557
7899
|
this.readers = readers;
|
|
7900
|
+
this.nodeHandles = nodeHandles;
|
|
7558
7901
|
this.nextId = 1;
|
|
7559
7902
|
this.entries = /* @__PURE__ */ new Map();
|
|
7560
7903
|
this.byObject = /* @__PURE__ */ new WeakMap();
|
|
@@ -7571,6 +7914,11 @@ var ResourceLedger = class {
|
|
|
7571
7914
|
this.listener = null;
|
|
7572
7915
|
/** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
|
|
7573
7916
|
this.sawBrowser = false;
|
|
7917
|
+
/** The servers and child processes still running, while the ledger tracks them. */
|
|
7918
|
+
this.handles = /* @__PURE__ */ new Set();
|
|
7919
|
+
this.byHandle = /* @__PURE__ */ new WeakMap();
|
|
7920
|
+
this.trackingHandles = false;
|
|
7921
|
+
this.restores = [];
|
|
7574
7922
|
}
|
|
7575
7923
|
/** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
|
|
7576
7924
|
install() {
|
|
@@ -7603,13 +7951,107 @@ var ResourceLedger = class {
|
|
|
7603
7951
|
for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
|
|
7604
7952
|
if (type) this.wrapBrowserType(type);
|
|
7605
7953
|
}
|
|
7954
|
+
if (this.nodeHandles) this.guard(() => this.wrapNodeHandles());
|
|
7606
7955
|
return true;
|
|
7607
7956
|
}
|
|
7608
|
-
/** Stop listening; the objects already tracked stay in the ledger. */
|
|
7957
|
+
/** Stop listening, and put Node's `listen` and `spawn` back; the objects already tracked stay in the ledger. */
|
|
7609
7958
|
uninstall() {
|
|
7610
7959
|
const remove = this.playwright?._instrumentation?.removeListener;
|
|
7611
7960
|
if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
|
|
7612
7961
|
this.listener = null;
|
|
7962
|
+
for (const restore of this.restores.splice(0)) restore();
|
|
7963
|
+
this.trackingHandles = false;
|
|
7964
|
+
}
|
|
7965
|
+
/**
|
|
7966
|
+
* Wrap `net.Server.prototype.listen` (every HTTP, HTTPS and TCP server) and
|
|
7967
|
+
* `ChildProcess.prototype.spawn` (`spawn`, `exec`, `execFile` and `fork`), so
|
|
7968
|
+
* each server and child process the worker starts carries the line and the
|
|
7969
|
+
* runnable that started it, as a context does. A worker-scoped fixture's
|
|
7970
|
+
* server is then the worker's, never the first test's. Each wrapper calls the
|
|
7971
|
+
* original on its receiver and only records; it never throws.
|
|
7972
|
+
*/
|
|
7973
|
+
wrapNodeHandles() {
|
|
7974
|
+
const onServer = (server2, site) => this.guard(() => this.onServer(server2, site));
|
|
7975
|
+
const onChild = (child2, site) => this.guard(() => this.onChild(child2, site));
|
|
7976
|
+
const server = net2.Server.prototype;
|
|
7977
|
+
const listen = server.listen;
|
|
7978
|
+
if (typeof listen === "function") {
|
|
7979
|
+
const wrapped = function(...args) {
|
|
7980
|
+
onServer(this, deepUserSite());
|
|
7981
|
+
return listen.apply(this, args);
|
|
7982
|
+
};
|
|
7983
|
+
server.listen = wrapped;
|
|
7984
|
+
this.restores.push(() => {
|
|
7985
|
+
if (server.listen === wrapped) server.listen = listen;
|
|
7986
|
+
});
|
|
7987
|
+
}
|
|
7988
|
+
const child = childProcess.ChildProcess?.prototype;
|
|
7989
|
+
const spawn = child?.spawn;
|
|
7990
|
+
if (child && typeof spawn === "function") {
|
|
7991
|
+
const wrapped = function(...args) {
|
|
7992
|
+
const site = deepUserSite();
|
|
7993
|
+
const result = spawn.apply(this, args);
|
|
7994
|
+
onChild(this, site);
|
|
7995
|
+
return result;
|
|
7996
|
+
};
|
|
7997
|
+
child.spawn = wrapped;
|
|
7998
|
+
this.restores.push(() => {
|
|
7999
|
+
if (child.spawn === wrapped) child.spawn = spawn;
|
|
8000
|
+
});
|
|
8001
|
+
}
|
|
8002
|
+
this.trackingHandles = true;
|
|
8003
|
+
}
|
|
8004
|
+
trackHandle(object, entry) {
|
|
8005
|
+
const info = this.testInfoSource();
|
|
8006
|
+
const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
|
|
8007
|
+
const tracked = { ...entry, phase, fixture, test: testRef2(info) ?? this.window };
|
|
8008
|
+
this.handles.add(tracked);
|
|
8009
|
+
this.byHandle.set(object, tracked);
|
|
8010
|
+
return tracked;
|
|
8011
|
+
}
|
|
8012
|
+
onServer(server, site) {
|
|
8013
|
+
const known = this.byHandle.get(server);
|
|
8014
|
+
if (known && this.handles.has(known)) return;
|
|
8015
|
+
let bound = false;
|
|
8016
|
+
let closed = false;
|
|
8017
|
+
const entry = this.trackHandle(server, {
|
|
8018
|
+
type: "TCPServerWrap",
|
|
8019
|
+
site,
|
|
8020
|
+
// `close()` unbinds at once, while its 'close' event waits for open connections.
|
|
8021
|
+
running: () => server.listening,
|
|
8022
|
+
pending: () => !bound && !closed && !server.listening
|
|
8023
|
+
});
|
|
8024
|
+
server.once("listening", () => {
|
|
8025
|
+
bound = true;
|
|
8026
|
+
});
|
|
8027
|
+
server.once("close", () => {
|
|
8028
|
+
closed = true;
|
|
8029
|
+
this.handles.delete(entry);
|
|
8030
|
+
});
|
|
8031
|
+
}
|
|
8032
|
+
onChild(child, site) {
|
|
8033
|
+
const command = commandOf(child.spawnfile, child.spawnargs);
|
|
8034
|
+
const role = roleOf(command, (child.spawnargs ?? []).join(" "));
|
|
8035
|
+
if (role && PLAYWRIGHT_ROLES.has(role) || child.pid === void 0) return;
|
|
8036
|
+
const entry = this.trackHandle(child, {
|
|
8037
|
+
type: "ChildProcess",
|
|
8038
|
+
site,
|
|
8039
|
+
command,
|
|
8040
|
+
running: () => child.exitCode === null && child.signalCode === null
|
|
8041
|
+
});
|
|
8042
|
+
child.once("exit", () => this.handles.delete(entry));
|
|
8043
|
+
}
|
|
8044
|
+
/**
|
|
8045
|
+
* The servers and child processes a test started (its body, its each-hooks
|
|
8046
|
+
* or a test-scoped fixture) that still run. A server whose `listen` has not
|
|
8047
|
+
* bound yet is read again one tick later.
|
|
8048
|
+
*/
|
|
8049
|
+
async runningOf(testId) {
|
|
8050
|
+
const own = () => [...this.handles].filter(
|
|
8051
|
+
(entry) => entry.test?.id === testId && !entry.fixture?.worker && (entry.phase === "test" || entry.phase === "beforeEach" || entry.phase === "afterEach")
|
|
8052
|
+
);
|
|
8053
|
+
if (own().some((entry) => entry.pending?.())) await new Promise((resolve11) => setTimeout(resolve11, 0));
|
|
8054
|
+
return own().filter((entry) => entry.running()).map((entry) => ({ type: entry.type, site: entry.site, ...entry.command ? { command: entry.command } : {} }));
|
|
7613
8055
|
}
|
|
7614
8056
|
/** Mark a page used: a locator was built on it. */
|
|
7615
8057
|
notePageUse(page) {
|
|
@@ -7619,7 +8061,7 @@ var ResourceLedger = class {
|
|
|
7619
8061
|
/** Open the census window of a test. */
|
|
7620
8062
|
testStarted(info) {
|
|
7621
8063
|
this.window = testRef2(info);
|
|
7622
|
-
this.handlesAtStart = countHandles();
|
|
8064
|
+
this.handlesAtStart = countHandles(this.countedHandleTypes());
|
|
7623
8065
|
this.openAtStart = { contexts: 0, pages: 0 };
|
|
7624
8066
|
for (const entry of this.openEntries()) {
|
|
7625
8067
|
if (entry.birth.kind === "page") this.openAtStart.pages++;
|
|
@@ -7639,9 +8081,14 @@ var ResourceLedger = class {
|
|
|
7639
8081
|
if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
|
|
7640
8082
|
const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
|
|
7641
8083
|
const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
|
|
7642
|
-
const handles = {
|
|
8084
|
+
const handles = {
|
|
8085
|
+
start: this.handlesAtStart,
|
|
8086
|
+
end: await settledHandles(this.handlesAtStart, this.countedHandleTypes())
|
|
8087
|
+
};
|
|
8088
|
+
const running = this.trackingHandles && test ? await this.runningOf(test.id) : null;
|
|
7643
8089
|
const census = this.census(ref, handles, await this.readPages());
|
|
7644
8090
|
if (metrics) census.metrics = metrics;
|
|
8091
|
+
if (running) census.running = running;
|
|
7645
8092
|
if (test) {
|
|
7646
8093
|
census.openAtStart = this.openAtStart;
|
|
7647
8094
|
if (!failed) census.leftOpen = leaks.length;
|
|
@@ -7680,6 +8127,10 @@ var ResourceLedger = class {
|
|
|
7680
8127
|
this.uninstall();
|
|
7681
8128
|
return census;
|
|
7682
8129
|
}
|
|
8130
|
+
/** The handle types counted at each boundary: servers leave the count once they are tracked one by one. */
|
|
8131
|
+
countedHandleTypes() {
|
|
8132
|
+
return this.trackingHandles ? COUNTED_WITH_RUNNING : TRACKED_HANDLE_TYPES;
|
|
8133
|
+
}
|
|
7683
8134
|
guard(run) {
|
|
7684
8135
|
try {
|
|
7685
8136
|
run();
|
|
@@ -7902,6 +8353,7 @@ var ResourceLedger = class {
|
|
|
7902
8353
|
open
|
|
7903
8354
|
};
|
|
7904
8355
|
if (handles) census.handles = handles;
|
|
8356
|
+
for (const close of this.closed) this.entries.delete(close.id);
|
|
7905
8357
|
this.born = [];
|
|
7906
8358
|
this.closed = [];
|
|
7907
8359
|
return census;
|
|
@@ -7940,7 +8392,8 @@ function startResourceLedger(playwright, workerIndex) {
|
|
|
7940
8392
|
process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
|
|
7941
8393
|
playwrightTestInfoSource(),
|
|
7942
8394
|
Date.now,
|
|
7943
|
-
{ metrics, pages: new PageReader() }
|
|
8395
|
+
{ metrics, pages: new PageReader() },
|
|
8396
|
+
true
|
|
7944
8397
|
);
|
|
7945
8398
|
if (!ledger.install()) {
|
|
7946
8399
|
metrics.dispose();
|
|
@@ -8099,7 +8552,15 @@ function buildResourceReport(input) {
|
|
|
8099
8552
|
if (!entry) continue;
|
|
8100
8553
|
entry.lastOpenAt = census.at;
|
|
8101
8554
|
if (open.used !== void 0) entry.used = open.used;
|
|
8102
|
-
if (census.test)
|
|
8555
|
+
if (census.test) {
|
|
8556
|
+
entry.series.push({
|
|
8557
|
+
pages: open.pages,
|
|
8558
|
+
listeners: open.listeners,
|
|
8559
|
+
routes: open.routes,
|
|
8560
|
+
elements: open.main?.elements,
|
|
8561
|
+
test: census.test
|
|
8562
|
+
});
|
|
8563
|
+
}
|
|
8103
8564
|
if (open.main) entry.mainCpu.push({ at: census.at, cpuMs: open.main.cpuMs });
|
|
8104
8565
|
}
|
|
8105
8566
|
}
|
|
@@ -8114,6 +8575,7 @@ function buildResourceReport(input) {
|
|
|
8114
8575
|
}
|
|
8115
8576
|
findings.push(...groupLeaks(leaked, all));
|
|
8116
8577
|
findings.push(...pilingFindings(all));
|
|
8578
|
+
findings.push(...workerPilingFindings(byWorker));
|
|
8117
8579
|
findings.push(...handleFindings(byWorker));
|
|
8118
8580
|
findings.push(...groupIdle(idle, input.fixturesByTest ?? /* @__PURE__ */ new Map()));
|
|
8119
8581
|
findings.push(...probableFindings(input.tallies ?? []));
|
|
@@ -8251,16 +8713,39 @@ function groupIdle(idle, fixturesByTest) {
|
|
|
8251
8713
|
}
|
|
8252
8714
|
return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => b.count - a.count);
|
|
8253
8715
|
}
|
|
8716
|
+
var PILING_RULES = {
|
|
8717
|
+
pages: { min: 2, share: 0, minPoints: 3 },
|
|
8718
|
+
listeners: { min: 2, share: 0, minPoints: 3 },
|
|
8719
|
+
routes: { min: 2, share: 0, minPoints: 3 },
|
|
8720
|
+
elements: { min: 200, share: 0.25, minPoints: 4 },
|
|
8721
|
+
heapMb: { min: 32, share: 0.2, minPoints: 3 },
|
|
8722
|
+
descriptors: { min: 8, share: 0.2, minPoints: 4 }
|
|
8723
|
+
};
|
|
8724
|
+
function pileUp(values, what) {
|
|
8725
|
+
const rule = PILING_RULES[what];
|
|
8726
|
+
if (values.length < rule.minPoints) return null;
|
|
8727
|
+
const enough = (from, to) => to - from >= Math.max(rule.min, from * rule.share);
|
|
8728
|
+
const first = values[0];
|
|
8729
|
+
const last = values[values.length - 1];
|
|
8730
|
+
if (values.every((value, i) => i === 0 || value >= values[i - 1]) && enough(first, last)) {
|
|
8731
|
+
return { from: first, to: last };
|
|
8732
|
+
}
|
|
8733
|
+
if (values.length < 4) return null;
|
|
8734
|
+
const floor = [...values];
|
|
8735
|
+
for (let i = floor.length - 2; i >= 0; i--) floor[i] = Math.min(floor[i], floor[i + 1]);
|
|
8736
|
+
const start = floor[0];
|
|
8737
|
+
const middle = floor[Math.floor((floor.length - 1) / 2)];
|
|
8738
|
+
const late = floor[Math.floor((floor.length - 1) * 3 / 4)];
|
|
8739
|
+
if (!enough(start, late) || middle - start < (late - start) / 4) return null;
|
|
8740
|
+
return { from: start, to: last };
|
|
8741
|
+
}
|
|
8254
8742
|
function pilingFindings(all) {
|
|
8255
8743
|
const out = [];
|
|
8256
8744
|
for (const entry of all) {
|
|
8257
|
-
for (const what of ["pages", "listeners", "routes"]) {
|
|
8745
|
+
for (const what of ["pages", "listeners", "routes", "elements"]) {
|
|
8258
8746
|
const values = entry.series.map((point) => point[what]).filter((v) => typeof v === "number");
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
const from = values[0];
|
|
8262
|
-
const to = values[values.length - 1];
|
|
8263
|
-
if (!rising || to - from < 2) continue;
|
|
8747
|
+
const growth = pileUp(values, what);
|
|
8748
|
+
if (!growth) continue;
|
|
8264
8749
|
out.push({
|
|
8265
8750
|
verdict: "piling",
|
|
8266
8751
|
kind: entry.birth.kind,
|
|
@@ -8268,34 +8753,91 @@ function pilingFindings(all) {
|
|
|
8268
8753
|
site: siteOf(entry.birth),
|
|
8269
8754
|
tests: values.length,
|
|
8270
8755
|
count: 1,
|
|
8271
|
-
growth: { what, from, to, tests: values.length }
|
|
8756
|
+
growth: { what, from: Math.round(growth.from), to: Math.round(growth.to), tests: values.length }
|
|
8272
8757
|
});
|
|
8273
8758
|
}
|
|
8274
8759
|
}
|
|
8275
8760
|
return out.sort((a, b) => b.growth.to - b.growth.from - (a.growth.to - a.growth.from));
|
|
8276
8761
|
}
|
|
8762
|
+
function leftSomethingOpen(census) {
|
|
8763
|
+
if ((census.leftOpen ?? 0) > 0 || (census.running?.length ?? 0) > 0) return true;
|
|
8764
|
+
const handles = census.handles;
|
|
8765
|
+
return !!handles && Object.entries(handles.end).some(([type, n]) => n > (handles.start[type] ?? 0));
|
|
8766
|
+
}
|
|
8767
|
+
function workerPilingFindings(byWorker) {
|
|
8768
|
+
const best = /* @__PURE__ */ new Map();
|
|
8769
|
+
for (const censuses of byWorker.values()) {
|
|
8770
|
+
const measured = censuses.filter((census) => census.test && !census.test.failed && census.metrics).sort((a, b) => a.at - b.at);
|
|
8771
|
+
const read = [
|
|
8772
|
+
["heapMb", (census) => census.metrics.worker.heapAfterGcMb],
|
|
8773
|
+
["descriptors", (census) => census.metrics.worker.fds]
|
|
8774
|
+
];
|
|
8775
|
+
for (const [what, value] of read) {
|
|
8776
|
+
const points = measured.map((census) => ({ census, value: value(census) })).filter((point) => typeof point.value === "number");
|
|
8777
|
+
const unexplained = [];
|
|
8778
|
+
let top = null;
|
|
8779
|
+
points.forEach((point, i) => {
|
|
8780
|
+
if (i === 0) return void unexplained.push(point.value);
|
|
8781
|
+
const gained = point.value - points[i - 1].value;
|
|
8782
|
+
const counted = leftSomethingOpen(point.census) && gained > 0 ? 0 : gained;
|
|
8783
|
+
unexplained.push(unexplained[i - 1] + counted);
|
|
8784
|
+
if (counted > 0 && (!top || counted > top.gained)) top = { test: point.census.test, gained: counted };
|
|
8785
|
+
});
|
|
8786
|
+
const growth = pileUp(unexplained, what);
|
|
8787
|
+
if (!growth) continue;
|
|
8788
|
+
const lead = points.length === measured.length ? top : null;
|
|
8789
|
+
const span = measured.indexOf(points[points.length - 1].census) - measured.indexOf(points[0].census) + 1;
|
|
8790
|
+
const finding = {
|
|
8791
|
+
verdict: "piling",
|
|
8792
|
+
kind: "worker",
|
|
8793
|
+
where: "worker process",
|
|
8794
|
+
tests: measured.length,
|
|
8795
|
+
count: 1,
|
|
8796
|
+
growth: {
|
|
8797
|
+
what,
|
|
8798
|
+
from: Math.round(Math.min(...points.map((point) => point.value))),
|
|
8799
|
+
to: Math.round(points[points.length - 1].value),
|
|
8800
|
+
tests: span
|
|
8801
|
+
},
|
|
8802
|
+
...lead ? { detail: `most of it during "${lead.test.title ?? ""}" (${lead.test.file})` } : {},
|
|
8803
|
+
gained: growth.to - growth.from
|
|
8804
|
+
};
|
|
8805
|
+
const seen = best.get(what);
|
|
8806
|
+
if (!seen || finding.gained > seen.gained) best.set(what, finding);
|
|
8807
|
+
}
|
|
8808
|
+
}
|
|
8809
|
+
return [...best.values()].map(({ gained: _gained, ...finding }) => finding);
|
|
8810
|
+
}
|
|
8277
8811
|
function handleFindings(byWorker) {
|
|
8278
8812
|
const groups = /* @__PURE__ */ new Map();
|
|
8813
|
+
const add2 = (test, type, count, site = null) => {
|
|
8814
|
+
const key = `${test.id}|${type}|${site ?? ""}`;
|
|
8815
|
+
const group = groups.get(key) ?? {
|
|
8816
|
+
verdict: "handle",
|
|
8817
|
+
kind: "handle",
|
|
8818
|
+
where: type,
|
|
8819
|
+
...site ? { site } : {},
|
|
8820
|
+
tests: 1,
|
|
8821
|
+
count: 0,
|
|
8822
|
+
detail: `"${test.title ?? ""}" (${test.file})`
|
|
8823
|
+
};
|
|
8824
|
+
group.count += count;
|
|
8825
|
+
groups.set(key, group);
|
|
8826
|
+
};
|
|
8279
8827
|
for (const censuses of byWorker.values()) {
|
|
8828
|
+
for (const census of censuses) {
|
|
8829
|
+
if (!census.test || census.test.failed) continue;
|
|
8830
|
+
for (const handle of census.running ?? []) add2(census.test, handle.type, 1, handle.site);
|
|
8831
|
+
}
|
|
8280
8832
|
const ordered = censuses.filter((census) => census.test && census.handles).sort((a, b) => a.at - b.at);
|
|
8281
8833
|
for (let i = 0; i < ordered.length - 1; i++) {
|
|
8282
8834
|
const census = ordered[i];
|
|
8283
8835
|
const next = ordered[i + 1];
|
|
8284
8836
|
for (const [type, end] of Object.entries(census.handles.end)) {
|
|
8837
|
+
if (census.running && type === "TCPServerWrap") continue;
|
|
8285
8838
|
const grew = end - (census.handles.start[type] ?? 0);
|
|
8286
8839
|
if (grew <= 0 || (next.handles.start[type] ?? 0) < end) continue;
|
|
8287
|
-
|
|
8288
|
-
const key = `${test.id}|${type}`;
|
|
8289
|
-
const group = groups.get(key) ?? {
|
|
8290
|
-
verdict: "handle",
|
|
8291
|
-
kind: "handle",
|
|
8292
|
-
where: type,
|
|
8293
|
-
tests: 1,
|
|
8294
|
-
count: 0,
|
|
8295
|
-
detail: `"${test.title ?? ""}" (${test.file})`
|
|
8296
|
-
};
|
|
8297
|
-
group.count += grew;
|
|
8298
|
-
groups.set(key, group);
|
|
8840
|
+
add2(census.test, type, grew);
|
|
8299
8841
|
}
|
|
8300
8842
|
}
|
|
8301
8843
|
}
|
|
@@ -8340,19 +8882,37 @@ var KIND_LABEL = {
|
|
|
8340
8882
|
context: "context",
|
|
8341
8883
|
page: "page",
|
|
8342
8884
|
request: "API context",
|
|
8343
|
-
handle: "handle"
|
|
8885
|
+
handle: "handle",
|
|
8886
|
+
worker: "worker"
|
|
8887
|
+
};
|
|
8888
|
+
var GROWTH_LABEL = {
|
|
8889
|
+
pages: "pages",
|
|
8890
|
+
listeners: "listeners",
|
|
8891
|
+
routes: "routes",
|
|
8892
|
+
elements: "elements",
|
|
8893
|
+
heapMb: "heap",
|
|
8894
|
+
descriptors: "descriptors"
|
|
8895
|
+
};
|
|
8896
|
+
var GROWTH_NOUN = {
|
|
8897
|
+
pages: "open pages",
|
|
8898
|
+
listeners: "listeners",
|
|
8899
|
+
routes: "route handlers",
|
|
8900
|
+
elements: "DOM elements",
|
|
8901
|
+
heapMb: "MB of heap after garbage collection",
|
|
8902
|
+
descriptors: "open file descriptors"
|
|
8344
8903
|
};
|
|
8345
8904
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
8346
8905
|
var HANDLE_LABEL = {
|
|
8347
8906
|
TCPServerWrap: "server",
|
|
8348
8907
|
FSEventWrap: "file watcher",
|
|
8349
|
-
StatWatcher: "file watcher"
|
|
8908
|
+
StatWatcher: "file watcher",
|
|
8909
|
+
ChildProcess: "process"
|
|
8350
8910
|
};
|
|
8351
8911
|
function findingLine(finding) {
|
|
8352
8912
|
const verdict = finding.verdict.padEnd(8);
|
|
8353
|
-
const label = finding.verdict === "handle" ? HANDLE_LABEL[finding.where] ?? finding.where : finding.growth ? finding.growth.what : KIND_LABEL[finding.kind];
|
|
8913
|
+
const label = finding.verdict === "handle" ? HANDLE_LABEL[finding.where] ?? finding.where : finding.growth ? GROWTH_LABEL[finding.growth.what] : KIND_LABEL[finding.kind];
|
|
8354
8914
|
const kind = label.padEnd(11);
|
|
8355
|
-
const parts = finding.verdict === "handle" ? [] : [finding.where];
|
|
8915
|
+
const parts = finding.verdict === "handle" ? finding.site ? [finding.site] : [] : [finding.where];
|
|
8356
8916
|
switch (finding.verdict) {
|
|
8357
8917
|
case "leaked": {
|
|
8358
8918
|
if (finding.count > 1) parts.push(`${finding.count} ${KIND_LABEL[finding.kind]}s`);
|
|
@@ -8375,11 +8935,12 @@ function findingLine(finding) {
|
|
|
8375
8935
|
break;
|
|
8376
8936
|
case "piling": {
|
|
8377
8937
|
const growth = finding.growth;
|
|
8378
|
-
|
|
8379
|
-
parts.push(
|
|
8938
|
+
parts.push(`${growth.from} \u2192 ${growth.to} ${GROWTH_NOUN[growth.what]} over ${growth.tests} tests`);
|
|
8939
|
+
if (finding.detail) parts.push(finding.detail);
|
|
8380
8940
|
break;
|
|
8381
8941
|
}
|
|
8382
8942
|
case "handle":
|
|
8943
|
+
if (finding.count > 1) parts.push(`${finding.count} ${label}${label.endsWith("s") ? "es" : "s"}`);
|
|
8383
8944
|
parts.push(`left running in the worker by ${finding.detail ?? "a test"}`);
|
|
8384
8945
|
break;
|
|
8385
8946
|
case "probable":
|
|
@@ -9244,7 +9805,7 @@ function resourceTimeline(samples, censuses) {
|
|
|
9244
9805
|
let startedAt = hasSamples ? samples.startedAt : Infinity;
|
|
9245
9806
|
for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
|
|
9246
9807
|
const shift = hasSamples ? samples.startedAt - startedAt : 0;
|
|
9247
|
-
const
|
|
9808
|
+
const relative12 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
|
|
9248
9809
|
const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
|
|
9249
9810
|
const perWorker = Math.max(
|
|
9250
9811
|
60,
|
|
@@ -9252,17 +9813,17 @@ function resourceTimeline(samples, censuses) {
|
|
|
9252
9813
|
);
|
|
9253
9814
|
return {
|
|
9254
9815
|
startedAt,
|
|
9255
|
-
cpuPct: hasSamples ? downsamplePoints(
|
|
9256
|
-
memoryBytes: hasSamples ? downsamplePoints(
|
|
9816
|
+
cpuPct: hasSamples ? downsamplePoints(relative12(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
|
|
9817
|
+
memoryBytes: hasSamples ? downsamplePoints(relative12(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
|
|
9257
9818
|
pages: workers.map(({ worker, events }) => {
|
|
9258
|
-
const points =
|
|
9819
|
+
const points = relative12(events, -startedAt);
|
|
9259
9820
|
const last = points.pop();
|
|
9260
9821
|
return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
|
|
9261
9822
|
}),
|
|
9262
9823
|
workers: trees.map((tree) => ({
|
|
9263
9824
|
worker: tree.worker,
|
|
9264
|
-
cpuCores: downsamplePoints(
|
|
9265
|
-
memoryBytes: downsamplePoints(
|
|
9825
|
+
cpuCores: downsamplePoints(relative12(tree.cpuCores, shift), perWorker, "mean"),
|
|
9826
|
+
memoryBytes: downsamplePoints(relative12(tree.memoryBytes, shift), perWorker, "max")
|
|
9266
9827
|
}))
|
|
9267
9828
|
};
|
|
9268
9829
|
}
|
|
@@ -9340,13 +9901,13 @@ var LiveResources = class {
|
|
|
9340
9901
|
const startedAt = samples?.startedAt || this.options.runStartedAt;
|
|
9341
9902
|
const censuses = this.options.censuses();
|
|
9342
9903
|
const cursors = this.reset ? /* @__PURE__ */ new Map() : this.sent;
|
|
9343
|
-
const
|
|
9904
|
+
const sent2 = new Map(cursors);
|
|
9344
9905
|
let fresh = false;
|
|
9345
9906
|
const take = (key, points, cap, merge) => {
|
|
9346
9907
|
const out = since(points, cursors.get(key), cap, merge);
|
|
9347
9908
|
const last = points[points.length - 1];
|
|
9348
9909
|
if (out.length > 0 && last) {
|
|
9349
|
-
|
|
9910
|
+
sent2.set(key, last[0]);
|
|
9350
9911
|
fresh = true;
|
|
9351
9912
|
}
|
|
9352
9913
|
return out;
|
|
@@ -9384,7 +9945,7 @@ var LiveResources = class {
|
|
|
9384
9945
|
memoryCapacityBytes: memory.capacityBytes,
|
|
9385
9946
|
memoryKind: memory.kind
|
|
9386
9947
|
},
|
|
9387
|
-
sent,
|
|
9948
|
+
sent: sent2,
|
|
9388
9949
|
censusCount: censuses.length
|
|
9389
9950
|
};
|
|
9390
9951
|
}
|
|
@@ -9816,8 +10377,8 @@ function detailOf(parsed2, headline) {
|
|
|
9816
10377
|
case "assertion-timeout": {
|
|
9817
10378
|
const received = parsed2.received;
|
|
9818
10379
|
if (received && !/element\(s\) not found/.test(received)) {
|
|
9819
|
-
const
|
|
9820
|
-
if (!headline.includes(
|
|
10380
|
+
const shown2 = truncateValue(receivedDisplay(parsed2), SHORT_VALUE_MAX_CHARS).slice(0, 8);
|
|
10381
|
+
if (!headline.includes(shown2)) return truncateValue(`Received: ${received}`, HEADLINE_MAX_CHARS);
|
|
9821
10382
|
}
|
|
9822
10383
|
const state = parsed2.lastStateLine;
|
|
9823
10384
|
if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
|
|
@@ -9970,7 +10531,7 @@ function browserFunctionCoverage(files) {
|
|
|
9970
10531
|
files: [...files].map(([path34, functions]) => ({ path: path34, runtime: "browser", functions }))
|
|
9971
10532
|
});
|
|
9972
10533
|
}
|
|
9973
|
-
function newKnownFunctions(known,
|
|
10534
|
+
function newKnownFunctions(known, sent2) {
|
|
9974
10535
|
const parts = [];
|
|
9975
10536
|
let files = [];
|
|
9976
10537
|
let budget = MAX_COVERAGE_RANGES;
|
|
@@ -9982,10 +10543,10 @@ function newKnownFunctions(known, sent) {
|
|
|
9982
10543
|
for (const [raw, ranges] of known) {
|
|
9983
10544
|
const path34 = normalizeCoveragePath(raw);
|
|
9984
10545
|
if (path34 === null) continue;
|
|
9985
|
-
let named =
|
|
10546
|
+
let named = sent2.get(path34);
|
|
9986
10547
|
let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
|
|
9987
10548
|
if (fresh.length === 0) continue;
|
|
9988
|
-
if (!named)
|
|
10549
|
+
if (!named) sent2.set(path34, named = /* @__PURE__ */ new Set());
|
|
9989
10550
|
for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
|
|
9990
10551
|
while (fresh.length > 0) {
|
|
9991
10552
|
if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
|
|
@@ -10003,7 +10564,7 @@ function serverCoverageInWorker() {
|
|
|
10003
10564
|
}
|
|
10004
10565
|
|
|
10005
10566
|
// src/internal/capture/code-reach.ts
|
|
10006
|
-
var
|
|
10567
|
+
var crypto6 = __toESM(require("crypto"));
|
|
10007
10568
|
var fs23 = __toESM(require("fs"));
|
|
10008
10569
|
var path23 = __toESM(require("path"));
|
|
10009
10570
|
var import_node_child_process3 = require("child_process");
|
|
@@ -10393,9 +10954,9 @@ function resolveSourcePath(source, roots) {
|
|
|
10393
10954
|
const normalized = normalizeSourcePath(source);
|
|
10394
10955
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
10395
10956
|
if (path23.isAbsolute(normalized) && isFile(normalized)) return path23.resolve(normalized);
|
|
10396
|
-
const
|
|
10957
|
+
const relative12 = normalized.replace(/^\/+/, "");
|
|
10397
10958
|
for (const root of roots) {
|
|
10398
|
-
const candidate = path23.resolve(root,
|
|
10959
|
+
const candidate = path23.resolve(root, relative12);
|
|
10399
10960
|
if (isFile(candidate)) return candidate;
|
|
10400
10961
|
}
|
|
10401
10962
|
return null;
|
|
@@ -10430,7 +10991,7 @@ function relativeToRepo(file, repoRoot) {
|
|
|
10430
10991
|
return path23.relative(repoRoot, file).split(path23.sep).join("/");
|
|
10431
10992
|
}
|
|
10432
10993
|
function scriptVersion(entry) {
|
|
10433
|
-
return entry.hash ??
|
|
10994
|
+
return entry.hash ?? crypto6.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
10434
10995
|
}
|
|
10435
10996
|
async function loadMap(entry, fetchMap) {
|
|
10436
10997
|
const ref = entry.source ? sourceMappingUrl(entry.source) : null;
|
|
@@ -10828,11 +11389,11 @@ async function stopPageCoverage(page) {
|
|
|
10828
11389
|
|
|
10829
11390
|
// src/internal/collect/blob-ids.ts
|
|
10830
11391
|
var import_node_child_process4 = require("child_process");
|
|
10831
|
-
var
|
|
11392
|
+
var crypto7 = __toESM(require("crypto"));
|
|
10832
11393
|
var fs24 = __toESM(require("fs"));
|
|
10833
11394
|
var path24 = __toESM(require("path"));
|
|
10834
11395
|
function gitBlobId(content) {
|
|
10835
|
-
return
|
|
11396
|
+
return crypto7.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
|
|
10836
11397
|
}
|
|
10837
11398
|
function namesLines(file) {
|
|
10838
11399
|
return Boolean(file.functions?.length || file.lines?.length || file.executable?.length || file.allFunctions?.length);
|
|
@@ -10970,10 +11531,10 @@ function composeTestCoverage(server, browserFiles, settings, cssFiles) {
|
|
|
10970
11531
|
const merged = mergeCoverage(serverCapture, mergeCoverage(browserCapture, cssCapture));
|
|
10971
11532
|
return merged ? sanitizeCoverage(merged) : null;
|
|
10972
11533
|
}
|
|
10973
|
-
function addRanges(into,
|
|
11534
|
+
function addRanges(into, filePath3, ranges) {
|
|
10974
11535
|
if (ranges.length === 0) return;
|
|
10975
|
-
let known = into.get(
|
|
10976
|
-
if (!known) into.set(
|
|
11536
|
+
let known = into.get(filePath3);
|
|
11537
|
+
if (!known) into.set(filePath3, known = /* @__PURE__ */ new Map());
|
|
10977
11538
|
for (const range of ranges) known.set(`${range[0]}:${range[1]}`, range);
|
|
10978
11539
|
}
|
|
10979
11540
|
function chunkRunCoverage(files) {
|
|
@@ -11052,8 +11613,9 @@ var CoverageCollection = class {
|
|
|
11052
11613
|
if (browser === "off" && css !== "rules") return {};
|
|
11053
11614
|
const paths = browser === "off" ? void 0 : testCase.codeReach;
|
|
11054
11615
|
for (const known of attachments ?? []) {
|
|
11055
|
-
if (browser === "functions" && known
|
|
11056
|
-
|
|
11616
|
+
if (browser === "functions" && isPiwiAttachment(known, ATTACHMENT_NAMES.browserFunctions))
|
|
11617
|
+
this.addKnownFunctions(known);
|
|
11618
|
+
if (css === "rules" && isPiwiAttachment(known, ATTACHMENT_NAMES.cssRules)) this.addKnownFunctions(known);
|
|
11057
11619
|
}
|
|
11058
11620
|
const scripts = browser === "functions" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.browserCoverage) : [];
|
|
11059
11621
|
const sheets = css === "rules" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.cssCoverage) : [];
|
|
@@ -11067,7 +11629,7 @@ var CoverageCollection = class {
|
|
|
11067
11629
|
}
|
|
11068
11630
|
/** The browser files of a test's attachment holding line ranges, added to the run's. */
|
|
11069
11631
|
rangedFiles(attachments, name) {
|
|
11070
|
-
const attachment = attachments
|
|
11632
|
+
const attachment = findPiwiAttachment(attachments, name);
|
|
11071
11633
|
if (!attachment?.body) return [];
|
|
11072
11634
|
let files = [];
|
|
11073
11635
|
try {
|
|
@@ -11091,8 +11653,8 @@ var CoverageCollection = class {
|
|
|
11091
11653
|
const files = parsed2?.v === 1 ? parsed2.files : null;
|
|
11092
11654
|
if (!Array.isArray(files)) return;
|
|
11093
11655
|
for (const item of files) {
|
|
11094
|
-
const
|
|
11095
|
-
if (
|
|
11656
|
+
const filePath3 = normalizeCoveragePath(item?.path);
|
|
11657
|
+
if (filePath3 !== null) addRanges(this.browserKnown, filePath3, normalizeFunctionRanges(item.allFunctions));
|
|
11096
11658
|
}
|
|
11097
11659
|
}
|
|
11098
11660
|
/**
|
|
@@ -11103,13 +11665,13 @@ var CoverageCollection = class {
|
|
|
11103
11665
|
async browserRunFiles() {
|
|
11104
11666
|
if (!this.browserBlobs) return [];
|
|
11105
11667
|
const paths = [.../* @__PURE__ */ new Set([...this.browserKnown.keys(), ...this.browserRan.keys()])].sort();
|
|
11106
|
-
const files = paths.map((
|
|
11107
|
-
const functions = normalizeFunctionRanges([...this.browserRan.get(
|
|
11668
|
+
const files = paths.map((filePath3) => {
|
|
11669
|
+
const functions = normalizeFunctionRanges([...this.browserRan.get(filePath3)?.values() ?? []]);
|
|
11108
11670
|
const allFunctions = normalizeFunctionRanges([
|
|
11109
|
-
...this.browserKnown.get(
|
|
11671
|
+
...this.browserKnown.get(filePath3)?.values() ?? [],
|
|
11110
11672
|
...functions
|
|
11111
11673
|
]);
|
|
11112
|
-
const file = { path:
|
|
11674
|
+
const file = { path: filePath3, runtime: "browser" };
|
|
11113
11675
|
if (functions.length > 0) file.functions = functions;
|
|
11114
11676
|
if (allFunctions.length > 0) file.allFunctions = allFunctions;
|
|
11115
11677
|
return file;
|
|
@@ -11128,7 +11690,7 @@ var CoverageCollection = class {
|
|
|
11128
11690
|
*/
|
|
11129
11691
|
apply(testCase, attachments) {
|
|
11130
11692
|
const browser = this.browserCoverage(testCase, attachments);
|
|
11131
|
-
const attachment = this.collector ? attachments
|
|
11693
|
+
const attachment = this.collector ? findPiwiAttachment(attachments, ATTACHMENT_NAMES.serverCoverageRequests) : void 0;
|
|
11132
11694
|
const requests = attachment?.body ? parseServerCoverageRequests(attachment.body) : null;
|
|
11133
11695
|
if (!this.collector || !requests) {
|
|
11134
11696
|
if (!(browser instanceof Promise)) {
|
|
@@ -11674,7 +12236,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11674
12236
|
if (frames.length) testCase.testSourceFrames = frames;
|
|
11675
12237
|
}
|
|
11676
12238
|
if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
|
|
11677
|
-
const pauses = readPauses(result.attachments
|
|
12239
|
+
const pauses = readPauses(findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.pauses)?.body);
|
|
11678
12240
|
testCase.performanceMetrics = collectStepMetrics(result.steps, result.errors, pauses);
|
|
11679
12241
|
const stepEvents = extractTestStepEvents(result.steps, result.startTime);
|
|
11680
12242
|
const waitEvents = extractWaitEvents(result.steps);
|
|
@@ -11684,14 +12246,14 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11684
12246
|
if (this.options.collectPerformanceMetrics && result.attachments) {
|
|
11685
12247
|
this.fileHandler.parsePerformanceAttachments(testCase, result.attachments);
|
|
11686
12248
|
this.manifests?.note(testCase.networkRequests);
|
|
11687
|
-
const locatorAttachment = this.options.captureLocators !== false ? result.attachments
|
|
12249
|
+
const locatorAttachment = this.options.captureLocators !== false ? findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.locators) : void 0;
|
|
11688
12250
|
if (locatorAttachment?.body) {
|
|
11689
12251
|
try {
|
|
11690
12252
|
testCase.locatorSnapshots = JSON.parse(locatorAttachment.body.toString());
|
|
11691
12253
|
} catch {
|
|
11692
12254
|
}
|
|
11693
12255
|
}
|
|
11694
|
-
const pagesAttachment = this.options.captureLocators !== false ? result.attachments
|
|
12256
|
+
const pagesAttachment = this.options.captureLocators !== false ? findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.locatorPages) : void 0;
|
|
11695
12257
|
if (pagesAttachment?.body) {
|
|
11696
12258
|
try {
|
|
11697
12259
|
testCase.locatorPages = JSON.parse(pagesAttachment.body.toString());
|
|
@@ -11700,7 +12262,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11700
12262
|
}
|
|
11701
12263
|
}
|
|
11702
12264
|
if (this.enabled && this.options.captureResources !== false) this.collectResources(test, result, testCase);
|
|
11703
|
-
const reachAttachment = this.coverageSettings.browser !== "off" ? result.attachments
|
|
12265
|
+
const reachAttachment = this.coverageSettings.browser !== "off" ? findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.codeReach) : void 0;
|
|
11704
12266
|
if (reachAttachment?.body) {
|
|
11705
12267
|
try {
|
|
11706
12268
|
const files = JSON.parse(reachAttachment.body.toString());
|
|
@@ -11772,7 +12334,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11772
12334
|
} catch {
|
|
11773
12335
|
}
|
|
11774
12336
|
}
|
|
11775
|
-
const attachment = result.attachments
|
|
12337
|
+
const attachment = findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.resources);
|
|
11776
12338
|
const census = parseResourceCensus(attachment?.body);
|
|
11777
12339
|
if (census) {
|
|
11778
12340
|
this.resourceCensuses.push(census);
|
|
@@ -12792,14 +13354,14 @@ function showAnchorPicker(arg) {
|
|
|
12792
13354
|
} catch {
|
|
12793
13355
|
shadowRoots.length = 0;
|
|
12794
13356
|
}
|
|
12795
|
-
const
|
|
13357
|
+
const within2 = (node2, scope) => {
|
|
12796
13358
|
if (scope === doc) return true;
|
|
12797
13359
|
for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
|
|
12798
13360
|
return false;
|
|
12799
13361
|
};
|
|
12800
13362
|
const queryAll = (scope, sel) => {
|
|
12801
13363
|
const found = Array.from(scope.querySelectorAll(sel));
|
|
12802
|
-
for (const sr of shadowRoots) if (
|
|
13364
|
+
for (const sr of shadowRoots) if (within2(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
|
|
12803
13365
|
return found;
|
|
12804
13366
|
};
|
|
12805
13367
|
const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
|
|
@@ -12880,8 +13442,8 @@ function showAnchorPicker(arg) {
|
|
|
12880
13442
|
return;
|
|
12881
13443
|
}
|
|
12882
13444
|
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
12883
|
-
const
|
|
12884
|
-
|
|
13445
|
+
const outline2 = doc.createElement("div");
|
|
13446
|
+
outline2.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;background:rgba(34,197,94,.10);border-radius:4px;display:none;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(5,46,22,.5);`;
|
|
12885
13447
|
const outlineLabel = doc.createElement("div");
|
|
12886
13448
|
outlineLabel.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(420px,90vw);background:#0b1120;color:#f9fafb;border:1px solid #22c55e;border-radius:6px;padding:3px 7px;font:12px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
|
|
12887
13449
|
const pickedOutline = doc.createElement("div");
|
|
@@ -12970,11 +13532,11 @@ function showAnchorPicker(arg) {
|
|
|
12970
13532
|
line.appendChild(text);
|
|
12971
13533
|
line.addEventListener("mouseenter", () => {
|
|
12972
13534
|
const r = row.node.getBoundingClientRect();
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
13535
|
+
outline2.style.display = "block";
|
|
13536
|
+
outline2.style.left = r.left + "px";
|
|
13537
|
+
outline2.style.top = r.top + "px";
|
|
13538
|
+
outline2.style.width = r.width + "px";
|
|
13539
|
+
outline2.style.height = r.height + "px";
|
|
12978
13540
|
outlineLabel.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
12979
13541
|
outlineLabel.style.display = "block";
|
|
12980
13542
|
const lr = outlineLabel.getBoundingClientRect();
|
|
@@ -12984,7 +13546,7 @@ function showAnchorPicker(arg) {
|
|
|
12984
13546
|
outlineLabel.style.top = top + "px";
|
|
12985
13547
|
});
|
|
12986
13548
|
line.addEventListener("mouseleave", () => {
|
|
12987
|
-
|
|
13549
|
+
outline2.style.display = "none";
|
|
12988
13550
|
outlineLabel.style.display = "none";
|
|
12989
13551
|
});
|
|
12990
13552
|
box.addEventListener("change", () => {
|
|
@@ -12998,7 +13560,7 @@ function showAnchorPicker(arg) {
|
|
|
12998
13560
|
const cleanup = () => {
|
|
12999
13561
|
doc.removeEventListener("keydown", onKey, true);
|
|
13000
13562
|
panel.remove();
|
|
13001
|
-
|
|
13563
|
+
outline2.remove();
|
|
13002
13564
|
outlineLabel.remove();
|
|
13003
13565
|
pickedOutline.remove();
|
|
13004
13566
|
if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
|
|
@@ -13063,7 +13625,7 @@ function showAnchorPicker(arg) {
|
|
|
13063
13625
|
);
|
|
13064
13626
|
}
|
|
13065
13627
|
};
|
|
13066
|
-
mount(pickedOutline,
|
|
13628
|
+
mount(pickedOutline, outline2, outlineLabel, panel);
|
|
13067
13629
|
}
|
|
13068
13630
|
|
|
13069
13631
|
// ../picker-dom/src/overlay-confirm.ts
|
|
@@ -13167,7 +13729,7 @@ function showPauseBar(arg) {
|
|
|
13167
13729
|
bar.style.cssText = `position:fixed;top:0;left:0;right:0;z-index:${Z + 4};display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:8px 12px;background:#111827;color:#f9fafb;border-bottom:2px solid #a78bfa;box-shadow:0 4px 20px rgba(0,0,0,.35);font:13px/1.5 system-ui,sans-serif;`;
|
|
13168
13730
|
const where = doc.createElement("div");
|
|
13169
13731
|
where.style.cssText = "font-weight:600;white-space:nowrap;";
|
|
13170
|
-
where.textContent =
|
|
13732
|
+
where.textContent = `${arg.failure ? "Failed at" : "Paused at"} ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
|
|
13171
13733
|
const what = doc.createElement("code");
|
|
13172
13734
|
what.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#d8b4fe;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;";
|
|
13173
13735
|
what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
|
|
@@ -13190,13 +13752,13 @@ function showPauseBar(arg) {
|
|
|
13190
13752
|
delete g.__piwiPauseCleanup;
|
|
13191
13753
|
};
|
|
13192
13754
|
const buttons = [
|
|
13193
|
-
["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
|
|
13194
|
-
["Step", "step", "Pause again before the next call", false],
|
|
13755
|
+
arg.failure ? ["Resume", "resume", "Let the test end as it failed (Esc)", true] : ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
|
|
13756
|
+
...arg.failure ? [] : [["Step", "step", "Pause again before the next call", false]],
|
|
13195
13757
|
["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
|
|
13196
13758
|
...arg.record ? [
|
|
13197
13759
|
["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
|
|
13198
13760
|
] : [],
|
|
13199
|
-
["Finish", "finish", "Run on and pause no more in this test", false]
|
|
13761
|
+
...arg.failure ? [] : [["Finish", "finish", "Run on and pause no more in this test", false]]
|
|
13200
13762
|
];
|
|
13201
13763
|
for (const [label, choice, hint, primary] of buttons) {
|
|
13202
13764
|
const btn = doc.createElement("button");
|
|
@@ -14604,11 +15166,31 @@ function parsePauseAt(value, cwd = process.cwd()) {
|
|
|
14604
15166
|
return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
|
|
14605
15167
|
}
|
|
14606
15168
|
var parsed = null;
|
|
15169
|
+
var sent = null;
|
|
14607
15170
|
function currentPauseSet() {
|
|
15171
|
+
if (sent) return sent;
|
|
14608
15172
|
const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
|
|
14609
15173
|
if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
|
|
14610
15174
|
return parsed.set;
|
|
14611
15175
|
}
|
|
15176
|
+
function replacePauseSet(places, cwd = process.cwd()) {
|
|
15177
|
+
if (!places) {
|
|
15178
|
+
sent = null;
|
|
15179
|
+
return currentPauseSet();
|
|
15180
|
+
}
|
|
15181
|
+
const lines = new Set(places.map((place) => placeLine(place, cwd)));
|
|
15182
|
+
sent = lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
|
|
15183
|
+
return sent;
|
|
15184
|
+
}
|
|
15185
|
+
function callSitePlace(callSite) {
|
|
15186
|
+
const m = /^(.+):(\d+):(\d+)$/.exec(callSite);
|
|
15187
|
+
if (!m || Number(m[2]) < 1) return null;
|
|
15188
|
+
const column = Number(m[3]);
|
|
15189
|
+
return column > 0 ? { file: m[1], line: Number(m[2]), column } : { file: m[1], line: Number(m[2]) };
|
|
15190
|
+
}
|
|
15191
|
+
function placeLine(place, cwd = process.cwd()) {
|
|
15192
|
+
return `${callSiteFile(place.file, cwd)}:${place.line}`;
|
|
15193
|
+
}
|
|
14612
15194
|
function pauseGateFromTestInfo(testInfo) {
|
|
14613
15195
|
const use = testInfo.project?.use ?? {};
|
|
14614
15196
|
const debug = testInfo._configInternal?.configCLIOverrides?.debug;
|
|
@@ -14616,7 +15198,7 @@ function pauseGateFromTestInfo(testInfo) {
|
|
|
14616
15198
|
}
|
|
14617
15199
|
function pauseSkipReason(gate) {
|
|
14618
15200
|
if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
|
|
14619
|
-
if (gate.headless !== false)
|
|
15201
|
+
if (gate.headless !== false && !gate.channel)
|
|
14620
15202
|
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
14621
15203
|
if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
|
|
14622
15204
|
return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
|
|
@@ -14624,20 +15206,42 @@ function pauseSkipReason(gate) {
|
|
|
14624
15206
|
return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
|
|
14625
15207
|
return null;
|
|
14626
15208
|
}
|
|
15209
|
+
function pauseState(set, pause = false) {
|
|
15210
|
+
return { set, step: false, finished: false, pause, runTo: null };
|
|
15211
|
+
}
|
|
15212
|
+
function wantsStops(state) {
|
|
15213
|
+
return state.step || state.pause || state.runTo !== null || !state.finished && state.set.size > 0;
|
|
15214
|
+
}
|
|
15215
|
+
function atBreakpoint(state, callSite) {
|
|
15216
|
+
return !state.finished && state.set.matches(callSite);
|
|
15217
|
+
}
|
|
14627
15218
|
function atStop(state, callSite) {
|
|
14628
|
-
if (callSite
|
|
14629
|
-
|
|
14630
|
-
|
|
14631
|
-
|
|
15219
|
+
if (callSite) {
|
|
15220
|
+
const reason = state.step ? "step" : state.pause ? "pause" : state.runTo !== null && callSiteLine(callSite) === state.runTo ? "run-to" : atBreakpoint(state, callSite) ? "breakpoint" : null;
|
|
15221
|
+
if (reason) {
|
|
15222
|
+
stopShown(state);
|
|
15223
|
+
return { show: reason };
|
|
14632
15224
|
}
|
|
14633
|
-
if (state.set.matches(callSite)) return { show: "breakpoint" };
|
|
14634
15225
|
}
|
|
14635
15226
|
return { go: goOn(state) };
|
|
14636
15227
|
}
|
|
15228
|
+
function stopShown(state) {
|
|
15229
|
+
state.step = false;
|
|
15230
|
+
state.pause = false;
|
|
15231
|
+
state.runTo = null;
|
|
15232
|
+
}
|
|
15233
|
+
function askedOnce(state) {
|
|
15234
|
+
return { step: state.step, pause: state.pause, runTo: state.runTo };
|
|
15235
|
+
}
|
|
15236
|
+
function askAgain(state, asked) {
|
|
15237
|
+
state.step ||= asked.step;
|
|
15238
|
+
state.pause ||= asked.pause;
|
|
15239
|
+
state.runTo ??= asked.runTo;
|
|
15240
|
+
}
|
|
14637
15241
|
function goOn(state, answer) {
|
|
14638
15242
|
if (answer === "step") state.step = true;
|
|
14639
15243
|
else if (answer === "finish") state.finished = true;
|
|
14640
|
-
return
|
|
15244
|
+
return wantsStops(state) ? "next" : "resume";
|
|
14641
15245
|
}
|
|
14642
15246
|
function contextDebugger(context) {
|
|
14643
15247
|
const dbg = context.debugger;
|
|
@@ -14773,38 +15377,83 @@ function logPauseSkipOnce(reason) {
|
|
|
14773
15377
|
var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
|
|
14774
15378
|
var MAX_NAVIGATED = 10;
|
|
14775
15379
|
var HIGHLIGHT_MS = 2e3;
|
|
14776
|
-
|
|
14777
|
-
|
|
15380
|
+
var BarIntents = class {
|
|
15381
|
+
constructor() {
|
|
15382
|
+
this.take = null;
|
|
15383
|
+
this.queued = null;
|
|
15384
|
+
this.busy = false;
|
|
15385
|
+
}
|
|
15386
|
+
/** Hand the bar an intent; false while it runs one, or holds one already. */
|
|
15387
|
+
offer(intent) {
|
|
15388
|
+
if (this.busy || this.queued) return false;
|
|
15389
|
+
const take = this.take;
|
|
15390
|
+
this.take = null;
|
|
15391
|
+
if (take) take(intent);
|
|
15392
|
+
else this.queued = intent;
|
|
15393
|
+
return true;
|
|
15394
|
+
}
|
|
15395
|
+
/** The bar waits for its answer: settles with the next intent handed to it. */
|
|
15396
|
+
wait() {
|
|
15397
|
+
this.busy = false;
|
|
15398
|
+
const queued = this.queued;
|
|
15399
|
+
this.queued = null;
|
|
15400
|
+
if (queued) return Promise.resolve(queued);
|
|
15401
|
+
return new Promise((resolve11) => {
|
|
15402
|
+
this.take = resolve11;
|
|
15403
|
+
});
|
|
15404
|
+
}
|
|
15405
|
+
/** The bar got its answer: it takes no intent until it waits again. */
|
|
15406
|
+
answered() {
|
|
15407
|
+
this.take = null;
|
|
15408
|
+
this.busy = true;
|
|
15409
|
+
}
|
|
15410
|
+
};
|
|
15411
|
+
var TAKE_DOWN_MS = 1e3;
|
|
15412
|
+
async function within(run, ms) {
|
|
15413
|
+
let timer;
|
|
15414
|
+
try {
|
|
15415
|
+
return await Promise.race([
|
|
15416
|
+
run,
|
|
15417
|
+
new Promise((resolve11) => timer = setTimeout(() => resolve11(void 0), ms))
|
|
15418
|
+
]);
|
|
15419
|
+
} finally {
|
|
15420
|
+
clearTimeout(timer);
|
|
15421
|
+
}
|
|
15422
|
+
}
|
|
15423
|
+
async function pauseHere(page, point, probe, onPick, onRecord, ended, intents, onIntent) {
|
|
15424
|
+
let shown2 = null;
|
|
14778
15425
|
try {
|
|
14779
15426
|
const highlight = point.target?.highlight;
|
|
14780
15427
|
if (typeof highlight === "function") {
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
|
|
14784
|
-
new Promise((resolve11) => {
|
|
14785
|
-
timer = setTimeout(() => resolve11(null), HIGHLIGHT_MS);
|
|
14786
|
-
})
|
|
14787
|
-
]);
|
|
14788
|
-
clearTimeout(timer);
|
|
15428
|
+
const highlighted = Promise.resolve().then(() => highlight.call(point.target)).catch(() => null);
|
|
15429
|
+
shown2 = await within(highlighted, HIGHLIGHT_MS) ?? null;
|
|
14789
15430
|
}
|
|
14790
15431
|
const arg = {
|
|
14791
15432
|
place: pausePlace(point.location),
|
|
14792
15433
|
attempt: point.attempt,
|
|
14793
15434
|
action: point.call,
|
|
14794
15435
|
locator: null,
|
|
14795
|
-
record: !!onRecord
|
|
15436
|
+
record: !!onRecord,
|
|
15437
|
+
...point.failure ? { failure: true } : {}
|
|
14796
15438
|
};
|
|
14797
15439
|
console.log(
|
|
14798
|
-
`[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
|
|
15440
|
+
point.failure ? `[piwi] The test failed at ${callSiteLine(point.location)}: Resume, Pick a locator${onRecord ? " or Record here" : ""} in the browser (Esc resumes, and the test ends as it failed).` : `[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
|
|
14799
15441
|
);
|
|
14800
15442
|
let navigated = 0;
|
|
14801
15443
|
for (; ; ) {
|
|
14802
15444
|
try {
|
|
14803
15445
|
if (ended?.aborted) return "resume";
|
|
14804
|
-
await page.evaluate(showPauseBar, arg);
|
|
14805
|
-
|
|
15446
|
+
await unlessEnded(page.evaluate(showPauseBar, arg), ended);
|
|
15447
|
+
const answered = page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 }).then(() => null);
|
|
15448
|
+
const intent = await unlessEnded(intents ? Promise.race([answered, intents.wait()]) : answered, ended);
|
|
15449
|
+
intents?.answered();
|
|
15450
|
+
if (ended?.aborted) return "resume";
|
|
15451
|
+
const taken = await unlessEnded(page.evaluate(takePauseState), ended);
|
|
15452
|
+
if (ended?.aborted) return "resume";
|
|
15453
|
+
const choice = taken ?? intent ?? null;
|
|
15454
|
+
const raised = intent && choice === intent ? page.bringToFront().catch(() => void 0) : null;
|
|
15455
|
+
if (raised) await unlessEnded(raised, ended);
|
|
14806
15456
|
if (ended?.aborted) return "resume";
|
|
14807
|
-
const choice = await page.evaluate(takePauseState);
|
|
14808
15457
|
navigated = 0;
|
|
14809
15458
|
if (choice === "resume" || choice === "step" || choice === "finish") return choice;
|
|
14810
15459
|
if (choice === "record" && onRecord) {
|
|
@@ -14814,31 +15463,290 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
|
|
|
14814
15463
|
);
|
|
14815
15464
|
continue;
|
|
14816
15465
|
}
|
|
15466
|
+
if ((choice === "assert" || choice === "assert-region") && onIntent) {
|
|
15467
|
+
await unlessEnded(
|
|
15468
|
+
onIntent(choice, ended).catch(() => void 0),
|
|
15469
|
+
ended
|
|
15470
|
+
);
|
|
15471
|
+
continue;
|
|
15472
|
+
}
|
|
14817
15473
|
if (choice !== "pick") continue;
|
|
14818
15474
|
const picked = await pickLocator(page, probe, null, ended).catch(() => null);
|
|
14819
15475
|
if (picked && !ended?.aborted) await onPick(picked);
|
|
14820
15476
|
} catch (error) {
|
|
14821
15477
|
if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
|
|
14822
|
-
await
|
|
15478
|
+
await unlessEnded(
|
|
15479
|
+
page.waitForLoadState("domcontentloaded").catch(() => void 0),
|
|
15480
|
+
ended
|
|
15481
|
+
);
|
|
14823
15482
|
}
|
|
14824
15483
|
}
|
|
14825
15484
|
} catch {
|
|
14826
15485
|
return "resume";
|
|
14827
15486
|
} finally {
|
|
15487
|
+
const takeDown = async () => {
|
|
15488
|
+
try {
|
|
15489
|
+
if (ended?.aborted) await page.evaluate(takePauseState);
|
|
15490
|
+
const dispose = shown2?.dispose;
|
|
15491
|
+
const hide = page.hideHighlight;
|
|
15492
|
+
if (typeof dispose === "function") await dispose.call(shown2);
|
|
15493
|
+
else if (typeof hide === "function") await hide.call(page);
|
|
15494
|
+
} catch {
|
|
15495
|
+
}
|
|
15496
|
+
};
|
|
15497
|
+
await within(takeDown(), TAKE_DOWN_MS);
|
|
15498
|
+
}
|
|
15499
|
+
}
|
|
15500
|
+
|
|
15501
|
+
// src/internal/capture/chain-locator.ts
|
|
15502
|
+
function argValue(page, arg) {
|
|
15503
|
+
switch (arg.type) {
|
|
15504
|
+
case "string":
|
|
15505
|
+
case "number":
|
|
15506
|
+
case "boolean":
|
|
15507
|
+
return arg.value;
|
|
15508
|
+
case "regex":
|
|
15509
|
+
return new RegExp(arg.source, arg.flags);
|
|
15510
|
+
case "object":
|
|
15511
|
+
return Object.fromEntries(arg.entries.map(([key, value]) => [key, argValue(page, value)]));
|
|
15512
|
+
case "chain":
|
|
15513
|
+
return chainOn(page, arg.chain);
|
|
15514
|
+
}
|
|
15515
|
+
}
|
|
15516
|
+
function chainOn(page, chain) {
|
|
15517
|
+
let current = page;
|
|
15518
|
+
for (const call of chain.calls) {
|
|
15519
|
+
if (typeof current[call.method] !== "function") {
|
|
15520
|
+
throw new Error(`${call.method}() cannot be called there.`);
|
|
15521
|
+
}
|
|
15522
|
+
current = callMethod(
|
|
15523
|
+
current,
|
|
15524
|
+
call.method,
|
|
15525
|
+
call.args.map((arg) => argValue(page, arg))
|
|
15526
|
+
);
|
|
15527
|
+
}
|
|
15528
|
+
if (!current || typeof current.count !== "function") {
|
|
15529
|
+
throw new Error("The chain does not end on a locator.");
|
|
15530
|
+
}
|
|
15531
|
+
return current;
|
|
15532
|
+
}
|
|
15533
|
+
function locatorOn(page, source) {
|
|
15534
|
+
const chain = tryParseLocatorChain(source);
|
|
15535
|
+
if (!chain?.calls.length) return null;
|
|
15536
|
+
try {
|
|
15537
|
+
return chainOn(page, chain);
|
|
15538
|
+
} catch {
|
|
15539
|
+
return null;
|
|
15540
|
+
}
|
|
15541
|
+
}
|
|
15542
|
+
|
|
15543
|
+
// src/internal/capture/region-snapshot.ts
|
|
15544
|
+
function regionSnapshotWithoutSecrets(snapshot, secrets) {
|
|
15545
|
+
const hidden = new Set(secrets.filter(Boolean));
|
|
15546
|
+
const kept = [];
|
|
15547
|
+
for (const line of snapshot.split("\n")) {
|
|
15548
|
+
const box = /^(\s*- textbox(?: "(?:[^"\\]|\\.)*")?(?: \[[^\]]*\])*): (.*)$/.exec(line);
|
|
15549
|
+
if (box && hidden.size && hidden.has(yamlScalar(box[2]))) {
|
|
15550
|
+
kept.push({ line: box[1], lostChild: false });
|
|
15551
|
+
continue;
|
|
15552
|
+
}
|
|
15553
|
+
const url = /^\s*- \/url: (.*)$/.exec(line);
|
|
15554
|
+
if (url && hideSecretsInUrl(yamlScalar(url[1])) !== yamlScalar(url[1])) {
|
|
15555
|
+
let parent = kept.length - 1;
|
|
15556
|
+
while (parent >= 0 && indentOf(kept[parent].line) >= indentOf(line)) parent--;
|
|
15557
|
+
if (parent >= 0) kept[parent].lostChild = true;
|
|
15558
|
+
continue;
|
|
15559
|
+
}
|
|
15560
|
+
kept.push({ line, lostChild: false });
|
|
15561
|
+
}
|
|
15562
|
+
return kept.map(({ line, lostChild }, i) => {
|
|
15563
|
+
const next = kept[i + 1];
|
|
15564
|
+
const childless = !next || indentOf(next.line) <= indentOf(line);
|
|
15565
|
+
return lostChild && childless && line.endsWith(":") ? line.slice(0, -1) : line;
|
|
15566
|
+
}).join("\n");
|
|
15567
|
+
}
|
|
15568
|
+
function indentOf(line) {
|
|
15569
|
+
return line.length - line.trimStart().length;
|
|
15570
|
+
}
|
|
15571
|
+
function yamlScalar(text) {
|
|
15572
|
+
const value = text.trim();
|
|
15573
|
+
if (value.startsWith('"')) {
|
|
14828
15574
|
try {
|
|
14829
|
-
|
|
14830
|
-
const dispose = shown?.dispose;
|
|
14831
|
-
const hide = page.hideHighlight;
|
|
14832
|
-
if (typeof dispose === "function") await dispose.call(shown);
|
|
14833
|
-
else if (typeof hide === "function") await hide.call(page);
|
|
15575
|
+
return String(JSON.parse(value));
|
|
14834
15576
|
} catch {
|
|
15577
|
+
return value;
|
|
15578
|
+
}
|
|
15579
|
+
}
|
|
15580
|
+
if (value.startsWith("'") && value.endsWith("'") && value.length > 1) return value.slice(1, -1).replace(/''/g, "'");
|
|
15581
|
+
return value;
|
|
15582
|
+
}
|
|
15583
|
+
|
|
15584
|
+
// ../core/src/run-channel.ts
|
|
15585
|
+
var RUN_CHANNEL_ENV = "PIWI_RUN_CHANNEL";
|
|
15586
|
+
var RUN_HOLDS = ["failure"];
|
|
15587
|
+
var RUN_INTENTS = ["pick", "record", "assert", "assert-region"];
|
|
15588
|
+
var RUN_TAKES = [...RUN_INTENTS, "requests", "mock", "locator"];
|
|
15589
|
+
var RUN_INTENT_NAMES = {
|
|
15590
|
+
pick: "Pick a locator",
|
|
15591
|
+
record: "Record here",
|
|
15592
|
+
assert: "Assert",
|
|
15593
|
+
"assert-region": "Assert a region"
|
|
15594
|
+
};
|
|
15595
|
+
var MAX_KEPT_RESPONSES = 50;
|
|
15596
|
+
var MAX_KEPT_BODY = 256 * 1024;
|
|
15597
|
+
var MAX_REGION_SNAPSHOT = 1024 * 1024;
|
|
15598
|
+
var MAX_STOP_ERROR = 1e3;
|
|
15599
|
+
function isObject2(value) {
|
|
15600
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15601
|
+
}
|
|
15602
|
+
function readJson(line) {
|
|
15603
|
+
try {
|
|
15604
|
+
const value = JSON.parse(line);
|
|
15605
|
+
return isObject2(value) ? value : null;
|
|
15606
|
+
} catch {
|
|
15607
|
+
return null;
|
|
15608
|
+
}
|
|
15609
|
+
}
|
|
15610
|
+
function isInt(value, min) {
|
|
15611
|
+
return typeof value === "number" && Number.isInteger(value) && value >= min;
|
|
15612
|
+
}
|
|
15613
|
+
function filePath2(value) {
|
|
15614
|
+
return typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0");
|
|
15615
|
+
}
|
|
15616
|
+
function readPlace(value) {
|
|
15617
|
+
if (!isObject2(value) || !filePath2(value.file) || !isInt(value.line, 1)) return null;
|
|
15618
|
+
if (value.column === void 0) return { file: value.file, line: value.line };
|
|
15619
|
+
return isInt(value.column, 1) ? { file: value.file, line: value.line, column: value.column } : null;
|
|
15620
|
+
}
|
|
15621
|
+
function isIntent(value) {
|
|
15622
|
+
return RUN_INTENTS.includes(value);
|
|
15623
|
+
}
|
|
15624
|
+
function readHolds(value) {
|
|
15625
|
+
if (!Array.isArray(value)) return void 0;
|
|
15626
|
+
return value.length > 16 ? [] : RUN_HOLDS.filter((hold) => value.includes(hold));
|
|
15627
|
+
}
|
|
15628
|
+
function isLocatorText(value) {
|
|
15629
|
+
return typeof value === "string" && value.length > 0 && value.length <= 1e3 && !/[\r\n\u2028\u2029]/.test(value);
|
|
15630
|
+
}
|
|
15631
|
+
var MAX_PLACES = 1e4;
|
|
15632
|
+
function parseHubToRun(line) {
|
|
15633
|
+
const value = readJson(line);
|
|
15634
|
+
if (!value) return null;
|
|
15635
|
+
const unknown = (what) => ({ type: "unknown", what: what.slice(0, 64) });
|
|
15636
|
+
switch (value.type) {
|
|
15637
|
+
case "attached": {
|
|
15638
|
+
const holds = readHolds(value.holds);
|
|
15639
|
+
return holds ? { type: "attached", holds } : null;
|
|
15640
|
+
}
|
|
15641
|
+
case "breakpoints": {
|
|
15642
|
+
if (!Array.isArray(value.places) || value.places.length > MAX_PLACES) return null;
|
|
15643
|
+
const places = value.places.map(readPlace);
|
|
15644
|
+
return places.every((p) => p !== null) ? { type: "breakpoints", places } : null;
|
|
15645
|
+
}
|
|
15646
|
+
case "control": {
|
|
15647
|
+
const { action, stop } = value;
|
|
15648
|
+
if (action === "pause") return { type: "control", action };
|
|
15649
|
+
if (action === "step" || action === "resume" || action === "finish") {
|
|
15650
|
+
return isInt(stop, 1) ? { type: "control", action, stop } : null;
|
|
15651
|
+
}
|
|
15652
|
+
return typeof action === "string" ? unknown(`control ${action}`) : null;
|
|
15653
|
+
}
|
|
15654
|
+
case "run-to": {
|
|
15655
|
+
const place = readPlace(value.place);
|
|
15656
|
+
return isInt(value.stop, 1) && place ? { type: "run-to", stop: value.stop, place } : null;
|
|
15657
|
+
}
|
|
15658
|
+
case "request": {
|
|
15659
|
+
const { request: request3, what, stop } = value;
|
|
15660
|
+
if (!isInt(request3, 1)) return null;
|
|
15661
|
+
if (what === "image") return { type: "request", request: request3, what: "image" };
|
|
15662
|
+
if (typeof what !== "string") return null;
|
|
15663
|
+
if (!isIntent(what) && what !== "requests" && what !== "mock" && what !== "locator") {
|
|
15664
|
+
return unknown(`request ${what}`);
|
|
15665
|
+
}
|
|
15666
|
+
if (!isInt(stop, 1)) return null;
|
|
15667
|
+
if (isIntent(what) || what === "requests") return { type: "request", request: request3, what, stop };
|
|
15668
|
+
if (what === "mock") return isInt(value.id, 1) ? { type: "request", request: request3, what, stop, id: value.id } : null;
|
|
15669
|
+
const { locator } = value;
|
|
15670
|
+
return locator === null || isLocatorText(locator) ? { type: "request", request: request3, what, stop, locator } : null;
|
|
14835
15671
|
}
|
|
15672
|
+
default:
|
|
15673
|
+
return typeof value.type === "string" && value.type ? unknown(value.type) : null;
|
|
15674
|
+
}
|
|
15675
|
+
}
|
|
15676
|
+
|
|
15677
|
+
// src/internal/capture/kept-responses.ts
|
|
15678
|
+
var keeping = null;
|
|
15679
|
+
var TEXT_TYPE = /^(?:text\/[\w.+-]+|application\/(?:[\w.+-]+\+)?(?:json|xml)|application\/(?:javascript|x-www-form-urlencoded|graphql))$/i;
|
|
15680
|
+
function mimeOf(contentType) {
|
|
15681
|
+
return (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
15682
|
+
}
|
|
15683
|
+
function textOf3(body) {
|
|
15684
|
+
try {
|
|
15685
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
15686
|
+
return text.includes("\0") ? null : text;
|
|
15687
|
+
} catch {
|
|
15688
|
+
return null;
|
|
15689
|
+
}
|
|
15690
|
+
}
|
|
15691
|
+
function startKeeping(now = Date.now()) {
|
|
15692
|
+
keeping = { startedAt: now, kept: [], last: 0 };
|
|
15693
|
+
}
|
|
15694
|
+
function stopKeeping() {
|
|
15695
|
+
keeping = null;
|
|
15696
|
+
}
|
|
15697
|
+
async function keepResponse(request3, response) {
|
|
15698
|
+
const into = keeping;
|
|
15699
|
+
if (!into || !response) return;
|
|
15700
|
+
try {
|
|
15701
|
+
const kind = request3.resourceType();
|
|
15702
|
+
if (kind !== "fetch" && kind !== "xhr") return;
|
|
15703
|
+
const url = hideSecretsInUrl(request3.url());
|
|
15704
|
+
const method = request3.method().toUpperCase();
|
|
15705
|
+
if (url.length > 8192 || !/^[A-Z]{1,16}$/.test(method)) return;
|
|
15706
|
+
const headers = response.headers();
|
|
15707
|
+
const type = mimeOf(headers["content-type"]);
|
|
15708
|
+
if (type && !TEXT_TYPE.test(type)) return;
|
|
15709
|
+
const status = response.status();
|
|
15710
|
+
if (status < 100 || status > 599) return;
|
|
15711
|
+
const time = Math.max(0, Math.round(Date.now() - into.startedAt));
|
|
15712
|
+
const size = /^\d+$/.test(headers["content-length"] ?? "") ? Number(headers["content-length"]) : null;
|
|
15713
|
+
if (size !== null && size > MAX_KEPT_BODY) return;
|
|
15714
|
+
into.kept.push({ id: ++into.last, method, url, status, type, size, time, response });
|
|
15715
|
+
if (into.kept.length > MAX_KEPT_RESPONSES) into.kept.splice(0, into.kept.length - MAX_KEPT_RESPONSES);
|
|
15716
|
+
} catch {
|
|
15717
|
+
}
|
|
15718
|
+
}
|
|
15719
|
+
function keptRequests() {
|
|
15720
|
+
return (keeping?.kept ?? []).map(({ id, method, url, status, type, size, time }) => ({
|
|
15721
|
+
id,
|
|
15722
|
+
method,
|
|
15723
|
+
url,
|
|
15724
|
+
status,
|
|
15725
|
+
type,
|
|
15726
|
+
size,
|
|
15727
|
+
time
|
|
15728
|
+
}));
|
|
15729
|
+
}
|
|
15730
|
+
async function keptResponse(id) {
|
|
15731
|
+
const found = keeping?.kept.find((kept) => kept.id === id);
|
|
15732
|
+
if (!found) return { refusal: "The test kept no such response: it keeps the latest 50." };
|
|
15733
|
+
let body;
|
|
15734
|
+
try {
|
|
15735
|
+
body = await internalCall(found.response, () => found.response.body());
|
|
15736
|
+
} catch {
|
|
15737
|
+
return {
|
|
15738
|
+
refusal: "The browser no longer holds this answer's body: mock one the page received since it last loaded."
|
|
15739
|
+
};
|
|
14836
15740
|
}
|
|
15741
|
+
if (body.length > MAX_KEPT_BODY) return { refusal: "This answer is larger than 256 KB: Piwi mocks a smaller one." };
|
|
15742
|
+
const text = textOf3(body);
|
|
15743
|
+
if (text === null) return { refusal: "This answer is not text: Piwi mocks a text answer." };
|
|
15744
|
+
return { method: found.method, url: found.url, status: found.status, type: found.type, body: text };
|
|
14837
15745
|
}
|
|
14838
15746
|
|
|
14839
15747
|
// src/internal/capture/record-handoff.ts
|
|
14840
15748
|
var fs26 = __toESM(require("fs"));
|
|
14841
|
-
var
|
|
15749
|
+
var net3 = __toESM(require("net"));
|
|
14842
15750
|
var path29 = __toESM(require("path"));
|
|
14843
15751
|
|
|
14844
15752
|
// ../core/src/ide-recorder.ts
|
|
@@ -14854,7 +15762,7 @@ function parseRecordChannel(value) {
|
|
|
14854
15762
|
const path34 = text.slice(0, hash);
|
|
14855
15763
|
return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path34.includes("\0") ? { path: path34, token } : null;
|
|
14856
15764
|
}
|
|
14857
|
-
function
|
|
15765
|
+
function isObject3(value) {
|
|
14858
15766
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14859
15767
|
}
|
|
14860
15768
|
function parseHandoffToTest(line) {
|
|
@@ -14864,7 +15772,7 @@ function parseHandoffToTest(line) {
|
|
|
14864
15772
|
} catch {
|
|
14865
15773
|
return null;
|
|
14866
15774
|
}
|
|
14867
|
-
if (!
|
|
15775
|
+
if (!isObject3(value)) return null;
|
|
14868
15776
|
switch (value.type) {
|
|
14869
15777
|
case "refused":
|
|
14870
15778
|
return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
|
|
@@ -14927,8 +15835,8 @@ async function recordFromHere(options) {
|
|
|
14927
15835
|
const use = testInfo.project?.use ?? {};
|
|
14928
15836
|
const context = page.context();
|
|
14929
15837
|
return new Promise((resolve11) => {
|
|
14930
|
-
const
|
|
14931
|
-
|
|
15838
|
+
const socket2 = net3.connect(channel.path);
|
|
15839
|
+
socket2.setEncoding("utf8");
|
|
14932
15840
|
const answers = /* @__PURE__ */ new Map();
|
|
14933
15841
|
let calls = 0;
|
|
14934
15842
|
let attached = false;
|
|
@@ -14936,7 +15844,7 @@ async function recordFromHere(options) {
|
|
|
14936
15844
|
let settled = false;
|
|
14937
15845
|
let initScript;
|
|
14938
15846
|
const send = (message) => {
|
|
14939
|
-
if (!
|
|
15847
|
+
if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
|
|
14940
15848
|
`);
|
|
14941
15849
|
};
|
|
14942
15850
|
const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
|
|
@@ -14986,7 +15894,7 @@ async function recordFromHere(options) {
|
|
|
14986
15894
|
page.off("close", onGone);
|
|
14987
15895
|
page.off("crash", onGone);
|
|
14988
15896
|
ended?.removeEventListener("abort", onEnded);
|
|
14989
|
-
|
|
15897
|
+
socket2.end();
|
|
14990
15898
|
resolve11(recording ? null : said);
|
|
14991
15899
|
};
|
|
14992
15900
|
const relayed = (source, payload) => new Promise((answer) => {
|
|
@@ -15026,7 +15934,7 @@ async function recordFromHere(options) {
|
|
|
15026
15934
|
context.on("page", onPage);
|
|
15027
15935
|
send({ type: "loaded" });
|
|
15028
15936
|
};
|
|
15029
|
-
|
|
15937
|
+
socket2.on("connect", () => {
|
|
15030
15938
|
attached = true;
|
|
15031
15939
|
send({
|
|
15032
15940
|
type: "attach",
|
|
@@ -15035,10 +15943,11 @@ async function recordFromHere(options) {
|
|
|
15035
15943
|
line: place.line,
|
|
15036
15944
|
reason: options.reason,
|
|
15037
15945
|
testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
|
|
15038
|
-
baseURL: typeof use.baseURL === "string" ? use.baseURL : null
|
|
15946
|
+
baseURL: typeof use.baseURL === "string" ? use.baseURL : null,
|
|
15947
|
+
...options.mode === "check" ? { mode: "check", stop: options.stop ?? null } : {}
|
|
15039
15948
|
});
|
|
15040
15949
|
});
|
|
15041
|
-
|
|
15950
|
+
socket2.on(
|
|
15042
15951
|
"data",
|
|
15043
15952
|
lineReader(
|
|
15044
15953
|
(line) => {
|
|
@@ -15071,13 +15980,257 @@ async function recordFromHere(options) {
|
|
|
15071
15980
|
() => finish("the editor sent a message too long to read.")
|
|
15072
15981
|
)
|
|
15073
15982
|
);
|
|
15074
|
-
|
|
15075
|
-
|
|
15983
|
+
socket2.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
|
|
15984
|
+
socket2.on("close", () => finish("the editor closed the recording."));
|
|
15076
15985
|
});
|
|
15077
15986
|
}
|
|
15078
15987
|
|
|
15988
|
+
// src/internal/capture/run-channel.ts
|
|
15989
|
+
var net4 = __toESM(require("net"));
|
|
15990
|
+
var CONNECT_MS = 2e3;
|
|
15991
|
+
var BAR_HIDDEN = "#__piwi-pause-bar { display: none !important; }";
|
|
15992
|
+
var OUTLINE_STYLE = "outline: 2px dashed #a78bfa; outline-offset: 2px; background: rgba(167, 139, 250, 0.15);";
|
|
15993
|
+
var WENT_ON = "The test went on.";
|
|
15994
|
+
var socket = null;
|
|
15995
|
+
var control = null;
|
|
15996
|
+
var shown = /* @__PURE__ */ new Set();
|
|
15997
|
+
var stops = 0;
|
|
15998
|
+
var held = null;
|
|
15999
|
+
var outline = null;
|
|
16000
|
+
var counting = Promise.resolve();
|
|
16001
|
+
async function openRunChannel(worker, handlers, env = process.env) {
|
|
16002
|
+
const channel = parseRecordChannel(env[RUN_CHANNEL_ENV]);
|
|
16003
|
+
const ref = env[PIWI_ORIGIN_ENV.ref];
|
|
16004
|
+
if (socket || !channel || !ref || !/^[\w-]{1,64}$/.test(ref)) return false;
|
|
16005
|
+
const opening = net4.connect(channel.path);
|
|
16006
|
+
opening.setEncoding("utf8");
|
|
16007
|
+
const opened = await new Promise((resolve11) => {
|
|
16008
|
+
const timer = setTimeout(() => resolve11(false), CONNECT_MS);
|
|
16009
|
+
opening.once("connect", () => {
|
|
16010
|
+
clearTimeout(timer);
|
|
16011
|
+
resolve11(true);
|
|
16012
|
+
});
|
|
16013
|
+
opening.once("error", () => {
|
|
16014
|
+
clearTimeout(timer);
|
|
16015
|
+
resolve11(false);
|
|
16016
|
+
});
|
|
16017
|
+
});
|
|
16018
|
+
if (!opened || opening.destroyed) {
|
|
16019
|
+
opening.destroy();
|
|
16020
|
+
return false;
|
|
16021
|
+
}
|
|
16022
|
+
socket = opening;
|
|
16023
|
+
control = handlers;
|
|
16024
|
+
shown = /* @__PURE__ */ new Set();
|
|
16025
|
+
opening.unref();
|
|
16026
|
+
opening.on(
|
|
16027
|
+
"data",
|
|
16028
|
+
lineReader(
|
|
16029
|
+
(line) => onMessage(parseHubToRun(line)),
|
|
16030
|
+
() => opening.destroy()
|
|
16031
|
+
)
|
|
16032
|
+
);
|
|
16033
|
+
opening.on("error", () => opening.destroy());
|
|
16034
|
+
opening.on("close", () => {
|
|
16035
|
+
if (socket === opening) socket = null;
|
|
16036
|
+
if (!held || held.bar) return;
|
|
16037
|
+
console.log("[piwi] The connection to the editor closed: the test goes on.");
|
|
16038
|
+
held.answer("resume");
|
|
16039
|
+
});
|
|
16040
|
+
tellHub({
|
|
16041
|
+
type: "run-attach",
|
|
16042
|
+
token: channel.token,
|
|
16043
|
+
ref,
|
|
16044
|
+
cwd: process.cwd(),
|
|
16045
|
+
workerIndex: worker.workerIndex,
|
|
16046
|
+
parallelIndex: worker.parallelIndex,
|
|
16047
|
+
pid: process.pid,
|
|
16048
|
+
playwright: worker.config.version,
|
|
16049
|
+
reporter: getReporterVersion(),
|
|
16050
|
+
takes: [...RUN_TAKES],
|
|
16051
|
+
holds: [...RUN_HOLDS]
|
|
16052
|
+
});
|
|
16053
|
+
return true;
|
|
16054
|
+
}
|
|
16055
|
+
function editorShows(what) {
|
|
16056
|
+
return runChannelOpen() && shown.has(what);
|
|
16057
|
+
}
|
|
16058
|
+
var HIDDEN_ENV = [RUN_CHANNEL_ENV, PIWI_LOCAL_DEBUG_ENV.editorSend];
|
|
16059
|
+
var hiddenEditorSend;
|
|
16060
|
+
function hideRunChannel(env = process.env) {
|
|
16061
|
+
const hidden = HIDDEN_ENV.flatMap((name) => {
|
|
16062
|
+
const value = env[name];
|
|
16063
|
+
return value === void 0 ? [] : [{ name, value }];
|
|
16064
|
+
});
|
|
16065
|
+
for (const { name } of hidden) delete env[name];
|
|
16066
|
+
hiddenEditorSend = hidden.find(({ name }) => name === PIWI_LOCAL_DEBUG_ENV.editorSend)?.value;
|
|
16067
|
+
return () => {
|
|
16068
|
+
for (const { name, value } of hidden) env[name] = value;
|
|
16069
|
+
hiddenEditorSend = void 0;
|
|
16070
|
+
};
|
|
16071
|
+
}
|
|
16072
|
+
function editorSendAddress(env = process.env) {
|
|
16073
|
+
return env[PIWI_LOCAL_DEBUG_ENV.editorSend] ?? hiddenEditorSend;
|
|
16074
|
+
}
|
|
16075
|
+
function closeRunChannel() {
|
|
16076
|
+
socket?.end();
|
|
16077
|
+
socket = null;
|
|
16078
|
+
control = null;
|
|
16079
|
+
}
|
|
16080
|
+
function runChannelOpen() {
|
|
16081
|
+
return !!socket && !socket.destroyed;
|
|
16082
|
+
}
|
|
16083
|
+
function tellHub(message) {
|
|
16084
|
+
if (socket && !socket.destroyed) socket.write(`${JSON.stringify(message)}
|
|
16085
|
+
`);
|
|
16086
|
+
}
|
|
16087
|
+
function holdStop(stop, onAnswer) {
|
|
16088
|
+
const id = ++stops;
|
|
16089
|
+
let answered = false;
|
|
16090
|
+
const holding = {
|
|
16091
|
+
stop: id,
|
|
16092
|
+
page: stop.page,
|
|
16093
|
+
bar: stop.bar,
|
|
16094
|
+
answer: (choice) => {
|
|
16095
|
+
if (answered) return;
|
|
16096
|
+
answered = true;
|
|
16097
|
+
onAnswer(choice);
|
|
16098
|
+
},
|
|
16099
|
+
intent: (what) => answered ? { ok: false, message: WENT_ON } : stop.intent?.(what) ?? {
|
|
16100
|
+
ok: false,
|
|
16101
|
+
message: `${RUN_INTENT_NAMES[what]} needs the pause bar in the test's window, and this stop shows none: run the test headed.`
|
|
16102
|
+
}
|
|
16103
|
+
};
|
|
16104
|
+
held = holding;
|
|
16105
|
+
tellHub({
|
|
16106
|
+
type: "stopped",
|
|
16107
|
+
stop: id,
|
|
16108
|
+
reason: stop.reason,
|
|
16109
|
+
title: stop.title,
|
|
16110
|
+
place: stop.place,
|
|
16111
|
+
locator: stop.locator,
|
|
16112
|
+
bar: stop.bar,
|
|
16113
|
+
...stop.error ? { error: stop.error } : {}
|
|
16114
|
+
});
|
|
16115
|
+
if (!runChannelOpen() && !stop.bar) holding.answer("resume");
|
|
16116
|
+
return {
|
|
16117
|
+
stop: id,
|
|
16118
|
+
release: async () => {
|
|
16119
|
+
answered = true;
|
|
16120
|
+
if (held === holding) held = null;
|
|
16121
|
+
await counting;
|
|
16122
|
+
if (outline?.stop === id) await removeOutline();
|
|
16123
|
+
tellHub({ type: "running", stop: id });
|
|
16124
|
+
}
|
|
16125
|
+
};
|
|
16126
|
+
}
|
|
16127
|
+
async function removeOutline() {
|
|
16128
|
+
const shown2 = outline;
|
|
16129
|
+
outline = null;
|
|
16130
|
+
await shown2?.remove().catch(() => void 0);
|
|
16131
|
+
}
|
|
16132
|
+
async function countLocator(holding, source) {
|
|
16133
|
+
await removeOutline();
|
|
16134
|
+
if (source === null) return { ok: true, message: "The outline is gone.", count: null };
|
|
16135
|
+
const page = holding.page;
|
|
16136
|
+
if (!page || page.isClosed()) return { ok: false, message: "This stop has no page to count on.", count: null };
|
|
16137
|
+
const locator = locatorOn(page, source);
|
|
16138
|
+
if (!locator) return { ok: false, message: `${source} is not a locator Piwi can build.`, count: null };
|
|
16139
|
+
try {
|
|
16140
|
+
const count = await internalCall(page, () => locator.count());
|
|
16141
|
+
if (holding.bar && count > 0 && held === holding) {
|
|
16142
|
+
const shown2 = await internalCall(page, () => locator.highlight({ style: OUTLINE_STYLE }));
|
|
16143
|
+
const dispose = shown2?.dispose;
|
|
16144
|
+
outline = {
|
|
16145
|
+
stop: holding.stop,
|
|
16146
|
+
remove: () => internalCall(page, () => typeof dispose === "function" ? dispose.call(shown2) : page.hideHighlight())
|
|
16147
|
+
};
|
|
16148
|
+
}
|
|
16149
|
+
return {
|
|
16150
|
+
ok: true,
|
|
16151
|
+
message: count === 0 ? "No element matches." : count === 1 ? "1 element matches." : `${count} elements match.`,
|
|
16152
|
+
count
|
|
16153
|
+
};
|
|
16154
|
+
} catch (error) {
|
|
16155
|
+
return { ok: false, message: `The locator could not be counted: ${errorMessage(error)}`, count: null };
|
|
16156
|
+
}
|
|
16157
|
+
}
|
|
16158
|
+
function onMessage(message) {
|
|
16159
|
+
if (!message) return;
|
|
16160
|
+
switch (message.type) {
|
|
16161
|
+
case "attached":
|
|
16162
|
+
shown = new Set(message.holds);
|
|
16163
|
+
return;
|
|
16164
|
+
case "breakpoints":
|
|
16165
|
+
control?.breakpoints(message.places);
|
|
16166
|
+
return;
|
|
16167
|
+
case "control":
|
|
16168
|
+
if (message.action === "pause") {
|
|
16169
|
+
if (!held) control?.pause();
|
|
16170
|
+
} else if (held?.stop === message.stop) held.answer(message.action);
|
|
16171
|
+
return;
|
|
16172
|
+
case "run-to":
|
|
16173
|
+
if (held?.stop !== message.stop) return;
|
|
16174
|
+
control?.runTo(message.place);
|
|
16175
|
+
held.answer("resume");
|
|
16176
|
+
return;
|
|
16177
|
+
case "request": {
|
|
16178
|
+
const { request: request3 } = message;
|
|
16179
|
+
const at = message.what !== "image" && held?.stop === message.stop ? held : null;
|
|
16180
|
+
switch (message.what) {
|
|
16181
|
+
case "image":
|
|
16182
|
+
void imageOf(held?.page ?? null).then((value) => tellHub({ type: "answer", request: request3, what: "image", value }));
|
|
16183
|
+
return;
|
|
16184
|
+
case "requests": {
|
|
16185
|
+
const requests = at ? keptRequests() : [];
|
|
16186
|
+
const said = !at ? WENT_ON : requests.length ? `The test kept ${requests.length === 1 ? "1 response" : `${requests.length} responses`}.` : "The test kept no response yet: Piwi keeps the text answers of its fetch and XHR requests.";
|
|
16187
|
+
tellHub({ type: "answer", request: request3, what: "requests", value: { ok: !!at, message: said, requests } });
|
|
16188
|
+
return;
|
|
16189
|
+
}
|
|
16190
|
+
case "mock": {
|
|
16191
|
+
void (at ? keptResponse(message.id) : Promise.resolve({ refusal: WENT_ON })).then((found) => {
|
|
16192
|
+
const value = "refusal" in found ? { ok: false, message: found.refusal, response: null } : { ok: true, message: `${found.method} ${found.url}`, response: found };
|
|
16193
|
+
tellHub({ type: "answer", request: request3, what: "mock", value });
|
|
16194
|
+
});
|
|
16195
|
+
return;
|
|
16196
|
+
}
|
|
16197
|
+
case "locator": {
|
|
16198
|
+
const counted = at ? counting.then(
|
|
16199
|
+
() => held === at ? countLocator(at, message.locator) : { ok: false, message: WENT_ON, count: null }
|
|
16200
|
+
) : Promise.resolve({ ok: false, message: WENT_ON, count: null });
|
|
16201
|
+
counting = counted.catch(() => void 0);
|
|
16202
|
+
void counted.then((value) => tellHub({ type: "answer", request: request3, what: "locator", value }));
|
|
16203
|
+
return;
|
|
16204
|
+
}
|
|
16205
|
+
default: {
|
|
16206
|
+
const value = at ? at.intent(message.what) : { ok: false, message: WENT_ON };
|
|
16207
|
+
tellHub({ type: "answer", request: request3, what: message.what, value });
|
|
16208
|
+
return;
|
|
16209
|
+
}
|
|
16210
|
+
}
|
|
16211
|
+
}
|
|
16212
|
+
case "unknown":
|
|
16213
|
+
tellHub({
|
|
16214
|
+
type: "notice",
|
|
16215
|
+
message: `@piwitests/reporter ${getReporterVersion()} does not take the editor's ${message.what}: update it.`
|
|
16216
|
+
});
|
|
16217
|
+
}
|
|
16218
|
+
}
|
|
16219
|
+
async function imageOf(page) {
|
|
16220
|
+
if (!page || page.isClosed()) return null;
|
|
16221
|
+
try {
|
|
16222
|
+
const shot = await internalCall(
|
|
16223
|
+
page,
|
|
16224
|
+
() => page.screenshot({ type: "jpeg", quality: 60, style: BAR_HIDDEN, timeout: 5e3 })
|
|
16225
|
+
);
|
|
16226
|
+
return { mime: "image/jpeg", data: shot.toString("base64") };
|
|
16227
|
+
} catch {
|
|
16228
|
+
return null;
|
|
16229
|
+
}
|
|
16230
|
+
}
|
|
16231
|
+
|
|
15079
16232
|
// src/internal/capture/css-coverage.ts
|
|
15080
|
-
var
|
|
16233
|
+
var crypto8 = __toESM(require("crypto"));
|
|
15081
16234
|
var fs27 = __toESM(require("fs"));
|
|
15082
16235
|
|
|
15083
16236
|
// ../core/src/css-rules.ts
|
|
@@ -15292,7 +16445,7 @@ async function resolveSheet(sheet, text, dirs, fetchMap) {
|
|
|
15292
16445
|
return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
|
|
15293
16446
|
}
|
|
15294
16447
|
function cachedResolution(sheet, text, dirs, fetchMap) {
|
|
15295
|
-
const version =
|
|
16448
|
+
const version = crypto8.createHash("sha1").update(text).digest("hex");
|
|
15296
16449
|
const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
|
|
15297
16450
|
" "
|
|
15298
16451
|
);
|
|
@@ -15635,10 +16788,15 @@ function createSink() {
|
|
|
15635
16788
|
locatorCalls: /* @__PURE__ */ new Map(),
|
|
15636
16789
|
stops: [],
|
|
15637
16790
|
shownStops: /* @__PURE__ */ new Map(),
|
|
16791
|
+
failureHeld: false,
|
|
16792
|
+
frozenAt: null,
|
|
16793
|
+
stopEnds: 0,
|
|
16794
|
+
ending: false,
|
|
15638
16795
|
released: [],
|
|
15639
16796
|
releasedRequests: /* @__PURE__ */ new Set(),
|
|
15640
16797
|
onReleasedRequestsEnded: null,
|
|
15641
16798
|
releasedQuietSince: 0,
|
|
16799
|
+
releasedUntil: null,
|
|
15642
16800
|
locatorPages: new LocatorPageLog(),
|
|
15643
16801
|
codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
|
|
15644
16802
|
serverCoverage: /* @__PURE__ */ new Map(),
|
|
@@ -15742,6 +16900,25 @@ async function drainPendingProbes(capMs) {
|
|
|
15742
16900
|
]);
|
|
15743
16901
|
clearTimeout(cap);
|
|
15744
16902
|
}
|
|
16903
|
+
async function settleBeforeClose(closing) {
|
|
16904
|
+
const drained = drainPendingProbes(1e3);
|
|
16905
|
+
const sink = currentSink;
|
|
16906
|
+
if (sink) {
|
|
16907
|
+
await endShownStops(sink, "closed", closing);
|
|
16908
|
+
await awaitReleasedCalls(sink);
|
|
16909
|
+
await stashPageState(sink, closing);
|
|
16910
|
+
}
|
|
16911
|
+
await drained;
|
|
16912
|
+
if (sink) {
|
|
16913
|
+
await maybeOpenPicker(sink, closing);
|
|
16914
|
+
await stopSinkCodeReach(sink, closing);
|
|
16915
|
+
}
|
|
16916
|
+
}
|
|
16917
|
+
async function settleContextClose(context) {
|
|
16918
|
+
CLOSING_CONTEXTS.add(context);
|
|
16919
|
+
await settleBeforeClose({ context });
|
|
16920
|
+
if (contextDebugger(context)?.pausedDetails()) await steerDebugger(context, "resume");
|
|
16921
|
+
}
|
|
15745
16922
|
function isPageClosed(page) {
|
|
15746
16923
|
try {
|
|
15747
16924
|
return typeof page.isClosed === "function" ? page.isClosed() : false;
|
|
@@ -15864,7 +17041,7 @@ function buildPageState(raw, cookies, interaction) {
|
|
|
15864
17041
|
}
|
|
15865
17042
|
async function readPageState(page) {
|
|
15866
17043
|
try {
|
|
15867
|
-
const
|
|
17044
|
+
const readRaw = page.evaluate(() => {
|
|
15868
17045
|
const listStorage = (s) => {
|
|
15869
17046
|
const out = [];
|
|
15870
17047
|
try {
|
|
@@ -15890,14 +17067,16 @@ async function readPageState(page) {
|
|
|
15890
17067
|
sessionStorage: listStorage(globalThis.sessionStorage)
|
|
15891
17068
|
};
|
|
15892
17069
|
});
|
|
17070
|
+
const readCookies = (async () => {
|
|
17071
|
+
try {
|
|
17072
|
+
return await pageContext(page)?.cookies() ?? null;
|
|
17073
|
+
} catch {
|
|
17074
|
+
return null;
|
|
17075
|
+
}
|
|
17076
|
+
})();
|
|
17077
|
+
const readInteraction = page.evaluate(readPageInteraction).catch(() => null);
|
|
17078
|
+
const [raw, cookies, interaction] = await Promise.all([readRaw, readCookies, readInteraction]);
|
|
15893
17079
|
if (!raw) return null;
|
|
15894
|
-
let cookies = null;
|
|
15895
|
-
try {
|
|
15896
|
-
cookies = await pageContext(page)?.cookies() ?? null;
|
|
15897
|
-
} catch {
|
|
15898
|
-
cookies = null;
|
|
15899
|
-
}
|
|
15900
|
-
const interaction = await page.evaluate(readPageInteraction).catch(() => null);
|
|
15901
17080
|
return buildPageState(raw, cookies, interaction);
|
|
15902
17081
|
} catch {
|
|
15903
17082
|
return null;
|
|
@@ -15961,7 +17140,7 @@ async function recordPageWindow(sink, page) {
|
|
|
15961
17140
|
}
|
|
15962
17141
|
async function stashPageState(sink, closing) {
|
|
15963
17142
|
const page = sink.lastActivePage;
|
|
15964
|
-
if (!page || isPageClosed(page)) return;
|
|
17143
|
+
if (!page || isPageClosed(page) || sink.frozenAt !== null) return;
|
|
15965
17144
|
const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
|
|
15966
17145
|
if (!belongsToClosing) return;
|
|
15967
17146
|
await internalCall(page, () => readPageBeforeClose(sink, page));
|
|
@@ -15973,13 +17152,12 @@ async function readBlankPage(page) {
|
|
|
15973
17152
|
return aria != null && aria.trim() === "";
|
|
15974
17153
|
}
|
|
15975
17154
|
async function readPageBeforeClose(sink, page) {
|
|
15976
|
-
const
|
|
17155
|
+
const readBlank = sink.probeItem?.level === "server" ? readBlankPage(page).catch(() => null) : null;
|
|
17156
|
+
const readState = process.env.PIWI_CAPTURE_PAGE_STATE !== "false" ? Promise.resolve(readBlank).then(() => readPageState(page)) : null;
|
|
17157
|
+
const [vitals, blankPage, pageState] = await Promise.all([readWebVitals(page), readBlank, readState]);
|
|
15977
17158
|
if (vitals) sink.stashedWebVitals = vitals;
|
|
15978
|
-
if (
|
|
15979
|
-
if (
|
|
15980
|
-
const pageState = await readPageState(page);
|
|
15981
|
-
if (pageState) sink.stashedPageState = pageState;
|
|
15982
|
-
}
|
|
17159
|
+
if (blankPage !== null) sink.probeBlankPage = blankPage;
|
|
17160
|
+
if (pageState) sink.stashedPageState = pageState;
|
|
15983
17161
|
const status = sink.testInfo?.status;
|
|
15984
17162
|
const sampleAria = async () => {
|
|
15985
17163
|
const root = page.locator(":root");
|
|
@@ -16173,16 +17351,35 @@ function locatorSource(target, method, args) {
|
|
|
16173
17351
|
return renderFailing({ method, args });
|
|
16174
17352
|
}
|
|
16175
17353
|
var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
|
|
17354
|
+
var CLOSING_CONTEXTS = /* @__PURE__ */ new WeakSet();
|
|
17355
|
+
var CLOSING_PAGES = /* @__PURE__ */ new WeakMap();
|
|
16176
17356
|
var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
|
|
16177
|
-
|
|
17357
|
+
function stopPageOf(context, call, paused) {
|
|
17358
|
+
const named = [call?.page, paused].find((page) => !!page && pageContext(page) === context);
|
|
17359
|
+
if (named) return { page: named };
|
|
17360
|
+
const open = openPages(context);
|
|
17361
|
+
return open.length === 1 ? { page: open[0] } : { among: open };
|
|
17362
|
+
}
|
|
17363
|
+
function openPages(context) {
|
|
17364
|
+
return context.pages().filter((page) => !isPageClosed(page));
|
|
17365
|
+
}
|
|
17366
|
+
function stopPages(of) {
|
|
17367
|
+
return "page" in of ? [of.page] : of.among;
|
|
17368
|
+
}
|
|
17369
|
+
function stopCanShow(context, of) {
|
|
17370
|
+
if (CLOSING_CONTEXTS.has(context)) return false;
|
|
17371
|
+
return !stopPages(of).some((page) => isPageClosed(page) || CLOSING_PAGES.has(page));
|
|
17372
|
+
}
|
|
17373
|
+
async function armStops(contexts) {
|
|
16178
17374
|
const state = currentSink?.pause;
|
|
16179
|
-
if (!state || state
|
|
17375
|
+
if (!state || !wantsStops(state)) return;
|
|
16180
17376
|
await Promise.all(
|
|
16181
17377
|
[...contexts].map(
|
|
16182
17378
|
(context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
|
|
16183
17379
|
)
|
|
16184
17380
|
);
|
|
16185
17381
|
}
|
|
17382
|
+
var stopTurn = Promise.resolve();
|
|
16186
17383
|
function onDebuggerStop(context) {
|
|
16187
17384
|
const details = contextDebugger(context)?.pausedDetails();
|
|
16188
17385
|
if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
|
|
@@ -16193,13 +17390,38 @@ function onDebuggerStop(context) {
|
|
|
16193
17390
|
try {
|
|
16194
17391
|
if (!sink || !state) return;
|
|
16195
17392
|
const callSite = stopSite(sink, details.location);
|
|
17393
|
+
const call = callSite ? sink.locatorCalls.get(callSiteLine(callSite)) : void 0;
|
|
16196
17394
|
const pagePause = PAGE_PAUSES.get(context);
|
|
17395
|
+
const asked = askedOnce(state);
|
|
16197
17396
|
const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
|
|
16198
17397
|
if ("go" in decided) {
|
|
16199
17398
|
how = decided.go;
|
|
16200
17399
|
return;
|
|
16201
17400
|
}
|
|
16202
|
-
|
|
17401
|
+
if (pagePause) stopShown(state);
|
|
17402
|
+
if (sink.ending || CLOSING_CONTEXTS.has(context)) {
|
|
17403
|
+
askAgain(state, asked);
|
|
17404
|
+
how = goOn(state, sink.ending ? endedWaiting(sink, call) : void 0);
|
|
17405
|
+
return;
|
|
17406
|
+
}
|
|
17407
|
+
const of = stopPageOf(context, call, pagePause?.page);
|
|
17408
|
+
const ends = sink.stopEnds;
|
|
17409
|
+
const shown2 = stopTurn.then(async () => {
|
|
17410
|
+
const ended = currentSink !== sink || sink.ending || sink.stopEnds !== ends;
|
|
17411
|
+
const gone = decided.show === "breakpoint" && !atBreakpoint(state, callSite);
|
|
17412
|
+
if (!ended && !gone && stopCanShow(context, of)) {
|
|
17413
|
+
const location = callSite ?? pagePause.location;
|
|
17414
|
+
const answer = await showStop(sink, context, details.title, location, decided.show, call, of);
|
|
17415
|
+
if (answer !== void 0) return answer;
|
|
17416
|
+
}
|
|
17417
|
+
askAgain(state, asked);
|
|
17418
|
+
if (ended) return endedWaiting(sink, call);
|
|
17419
|
+
const closing = stopPages(of).flatMap((page) => CLOSING_PAGES.get(page) ?? []);
|
|
17420
|
+
await settledWithin(closing, END_STOP_MS);
|
|
17421
|
+
return void 0;
|
|
17422
|
+
});
|
|
17423
|
+
stopTurn = shown2.catch(() => void 0);
|
|
17424
|
+
how = goOn(state, await shown2);
|
|
16203
17425
|
} catch {
|
|
16204
17426
|
if (state) how = goOn(state);
|
|
16205
17427
|
} finally {
|
|
@@ -16207,22 +17429,30 @@ function onDebuggerStop(context) {
|
|
|
16207
17429
|
}
|
|
16208
17430
|
})();
|
|
16209
17431
|
}
|
|
17432
|
+
function endedWaiting(sink, call) {
|
|
17433
|
+
releaseCall(sink, call?.settled ?? Promise.resolve());
|
|
17434
|
+
return "finish";
|
|
17435
|
+
}
|
|
16210
17436
|
function stopSite(sink, location) {
|
|
16211
17437
|
if (path30.dirname(path30.resolve(location.file)) !== __dirname) return stopCallSite(location);
|
|
16212
17438
|
return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
|
|
16213
17439
|
}
|
|
16214
|
-
function
|
|
16215
|
-
|
|
16216
|
-
|
|
16217
|
-
|
|
16218
|
-
const open = context
|
|
17440
|
+
function stopBarPage(sink, context, of) {
|
|
17441
|
+
if ("page" in of) return of.page;
|
|
17442
|
+
const last = sink.lastActivePage;
|
|
17443
|
+
if (last && !isPageClosed(last) && pageContext(last) === context) return last;
|
|
17444
|
+
const open = openPages(context);
|
|
16219
17445
|
return open[open.length - 1] ?? null;
|
|
16220
17446
|
}
|
|
16221
|
-
async function showStop(sink, context, title, location, reason) {
|
|
17447
|
+
async function showStop(sink, context, title, location, reason, call, of, failure = null) {
|
|
16222
17448
|
const testInfo = sink.testInfo;
|
|
16223
|
-
const
|
|
16224
|
-
const
|
|
16225
|
-
if (!testInfo
|
|
17449
|
+
const page = stopBarPage(sink, context, of);
|
|
17450
|
+
const guessed = !("page" in of);
|
|
17451
|
+
if (!testInfo) return void 0;
|
|
17452
|
+
const bar = !!page && testInfo.project?.use?.headless === false;
|
|
17453
|
+
const place = runChannelOpen() ? callSitePlace(location) : null;
|
|
17454
|
+
if (!bar && !place) return void 0;
|
|
17455
|
+
const failing = failure?.failed ?? null;
|
|
16226
17456
|
const onPick = async (picked) => {
|
|
16227
17457
|
if (call) {
|
|
16228
17458
|
call.picked = true;
|
|
@@ -16233,16 +17463,57 @@ async function showStop(sink, context, title, location, reason) {
|
|
|
16233
17463
|
...picked
|
|
16234
17464
|
});
|
|
16235
17465
|
sink.probedLocations.add(call.location);
|
|
17466
|
+
} else if (failing) {
|
|
17467
|
+
const pick = {
|
|
17468
|
+
failing: { method: failing.method, args: failing.args, rendered: renderFailing(failing), location },
|
|
17469
|
+
...picked
|
|
17470
|
+
};
|
|
17471
|
+
sink.userPick = pick;
|
|
17472
|
+
applyPickToSnapshots(sink.capturedLocators, pick);
|
|
16236
17473
|
}
|
|
16237
17474
|
console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
|
|
16238
|
-
await postPickToEditor(
|
|
17475
|
+
await postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location));
|
|
16239
17476
|
};
|
|
17477
|
+
const handoffReason = reason === "page-pause" ? "pause" : "breakpoint";
|
|
17478
|
+
const onRecord = bar && page ? recorderAt(page, testInfo, location, handoffReason) : void 0;
|
|
17479
|
+
const intents = bar && page ? new BarIntents() : null;
|
|
16240
17480
|
const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
|
|
16241
17481
|
sink.stops.push(record);
|
|
16242
17482
|
const end = new AbortController();
|
|
16243
|
-
const
|
|
16244
|
-
|
|
16245
|
-
|
|
17483
|
+
const over = new AbortController();
|
|
17484
|
+
end.signal.addEventListener("abort", () => over.abort(), { once: true });
|
|
17485
|
+
let fromEditor;
|
|
17486
|
+
const held2 = place ? holdStop(
|
|
17487
|
+
{
|
|
17488
|
+
reason,
|
|
17489
|
+
title,
|
|
17490
|
+
place,
|
|
17491
|
+
locator: call ? locatorSource(call.target, call.origin.method, call.origin.args) : failing ? renderFailing(failing) : null,
|
|
17492
|
+
bar,
|
|
17493
|
+
page: guessed ? null : page,
|
|
17494
|
+
...failure?.error ? { error: failure.error } : {},
|
|
17495
|
+
intent: intents ? (what) => intentAt(intents, what, {
|
|
17496
|
+
record: !!onRecord,
|
|
17497
|
+
guessed,
|
|
17498
|
+
// A failure's stop is held in the teardown it ends with.
|
|
17499
|
+
holds: () => !over.signal.aborted && (!!failure || !sink.ending && stopCanShow(context, of))
|
|
17500
|
+
}) : void 0
|
|
17501
|
+
},
|
|
17502
|
+
(answer2) => {
|
|
17503
|
+
fromEditor = answer2;
|
|
17504
|
+
over.abort();
|
|
17505
|
+
}
|
|
17506
|
+
) : null;
|
|
17507
|
+
const onIntent = bar && page && held2 ? (intent, ended) => intent === "assert" ? checkAt(page, testInfo, location, handoffReason, held2.stop, ended) : regionAt(page, location, held2.stop, ended) : void 0;
|
|
17508
|
+
const shown2 = withTestTimeoutHeld(testInfo, async () => {
|
|
17509
|
+
if (!bar || !page) {
|
|
17510
|
+
console.log(
|
|
17511
|
+
failure ? `[piwi] The test failed at ${callSiteLine(location)}: Resume in the editor lets it end as it failed.` : `[piwi] Paused at ${callSiteLine(location)} before ${title}: Resume, Step or Finish in the editor.`
|
|
17512
|
+
);
|
|
17513
|
+
await abortion(over.signal);
|
|
17514
|
+
return "resume";
|
|
17515
|
+
}
|
|
17516
|
+
return internalCall(
|
|
16246
17517
|
page,
|
|
16247
17518
|
() => pauseHere(
|
|
16248
17519
|
page,
|
|
@@ -16250,45 +17521,221 @@ async function showStop(sink, context, title, location, reason) {
|
|
|
16250
17521
|
location,
|
|
16251
17522
|
call: title,
|
|
16252
17523
|
attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
|
|
16253
|
-
target: call?.page === page ? call.target : null
|
|
17524
|
+
target: call?.page === page ? call.target : null,
|
|
17525
|
+
...failure ? { failure: true } : {}
|
|
16254
17526
|
},
|
|
16255
17527
|
{ fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
|
|
16256
17528
|
onPick,
|
|
16257
|
-
|
|
16258
|
-
|
|
17529
|
+
onRecord,
|
|
17530
|
+
over.signal,
|
|
17531
|
+
intents ?? void 0,
|
|
17532
|
+
onIntent
|
|
16259
17533
|
)
|
|
16260
|
-
)
|
|
16261
|
-
).finally(() => {
|
|
17534
|
+
);
|
|
17535
|
+
}).finally(async () => {
|
|
16262
17536
|
if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
|
|
16263
17537
|
else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
|
|
16264
17538
|
sink.shownStops.delete(end);
|
|
17539
|
+
await held2?.release();
|
|
16265
17540
|
});
|
|
16266
17541
|
const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
|
|
16267
|
-
|
|
16268
|
-
|
|
16269
|
-
|
|
16270
|
-
|
|
17542
|
+
const stop = { page, guessed, context, record, done: shown2.catch(() => void 0), heldAt, after: null };
|
|
17543
|
+
if (!failure) sink.shownStops.set(end, stop);
|
|
17544
|
+
const answer = await shown2;
|
|
17545
|
+
if (!end.signal.aborted) return fromEditor ?? answer;
|
|
17546
|
+
if (end.signal.reason === "ended") return "finish";
|
|
17547
|
+
await settledWithin(stop.after ? [stop.after] : [], END_STOP_MS);
|
|
17548
|
+
return "resume";
|
|
17549
|
+
}
|
|
17550
|
+
function intentAt(intents, what, stop) {
|
|
17551
|
+
if (!stop.holds()) return { ok: false, message: "The test went on." };
|
|
17552
|
+
if (stop.guessed) {
|
|
17553
|
+
return {
|
|
17554
|
+
ok: false,
|
|
17555
|
+
message: `${RUN_INTENT_NAMES[what]} needs the page the test stopped in, which this call does not name among its context's pages: use the pause bar in the test's window.`
|
|
17556
|
+
};
|
|
17557
|
+
}
|
|
17558
|
+
if ((what === "record" || what === "assert") && !stop.record) {
|
|
17559
|
+
return {
|
|
17560
|
+
ok: false,
|
|
17561
|
+
message: `${RUN_INTENT_NAMES[what]} needs the editor that started the run: this run names no record channel.`
|
|
17562
|
+
};
|
|
17563
|
+
}
|
|
17564
|
+
if (!intents.offer(what)) {
|
|
17565
|
+
return {
|
|
17566
|
+
ok: false,
|
|
17567
|
+
message: `${RUN_INTENT_NAMES[what]} waits: a pick, a check or a recording runs in the test's window.`
|
|
17568
|
+
};
|
|
17569
|
+
}
|
|
17570
|
+
const started = {
|
|
17571
|
+
pick: "Pick an element in the test's window: the locator goes to the editor.",
|
|
17572
|
+
record: "Recording in the test's window: the editor writes the steps above the stopped line.",
|
|
17573
|
+
assert: "Check an element in the test's window: the editor writes the check above the stopped line.",
|
|
17574
|
+
"assert-region": "Pick the region in the test's window: the editor writes its ARIA snapshot above the stopped line."
|
|
17575
|
+
};
|
|
17576
|
+
return { ok: true, message: started[what] };
|
|
17577
|
+
}
|
|
17578
|
+
async function checkAt(page, testInfo, location, reason, stop, ended) {
|
|
17579
|
+
const ref = process.env[PIWI_ORIGIN_ENV.ref] ?? "";
|
|
17580
|
+
console.log(`[piwi] Checking an element at ${callSiteLine(location)}: the editor writes the check.`);
|
|
17581
|
+
const why = await recordFromHere({
|
|
17582
|
+
page,
|
|
17583
|
+
testInfo,
|
|
17584
|
+
location,
|
|
17585
|
+
reason,
|
|
17586
|
+
mode: "check",
|
|
17587
|
+
stop: { ref, worker: testInfo.workerIndex, stop },
|
|
17588
|
+
ended
|
|
17589
|
+
});
|
|
17590
|
+
if (ended?.aborted) console.log("[piwi] The check ended with the stop.");
|
|
17591
|
+
else console.log(why ? `[piwi] Nothing was checked: ${why}` : "[piwi] The check ended: the test is paused again.");
|
|
17592
|
+
}
|
|
17593
|
+
async function regionAt(page, location, stop, ended) {
|
|
17594
|
+
const picked = await pickLocator(page, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG }, null, ended).catch(
|
|
17595
|
+
() => null
|
|
17596
|
+
);
|
|
17597
|
+
if (!picked || ended?.aborted) return;
|
|
17598
|
+
const source = picked.picked.locator;
|
|
17599
|
+
const say = (message) => {
|
|
17600
|
+
console.log(`[piwi] ${message}`);
|
|
17601
|
+
tellHub({ type: "region", stop, refused: message });
|
|
17602
|
+
};
|
|
17603
|
+
const region = locatorOn(page, source);
|
|
17604
|
+
if (!region) return say(`The region's locator, ${source}, is not one Piwi can read.`);
|
|
17605
|
+
const count = await region.count().catch(() => 0);
|
|
17606
|
+
if (count !== 1) return say(`${source} finds ${count} elements: pick a region it finds alone.`);
|
|
17607
|
+
const read = await region.ariaSnapshot({ timeout: 5e3 }).catch(() => null);
|
|
17608
|
+
if (!read?.trim()) return say(`The ARIA snapshot of ${source} could not be read.`);
|
|
17609
|
+
const secrets = await regionSecrets(region).catch(() => null);
|
|
17610
|
+
if (secrets === null) return say(`The fields of ${source} that hold a secret could not be read.`);
|
|
17611
|
+
const snapshot = regionSnapshotWithoutSecrets(read, secrets);
|
|
17612
|
+
if (snapshot.length > MAX_REGION_SNAPSHOT) {
|
|
17613
|
+
return say(
|
|
17614
|
+
`The ARIA snapshot of ${source} is longer than ${MAX_REGION_SNAPSHOT} characters: pick a smaller region.`
|
|
17615
|
+
);
|
|
17616
|
+
}
|
|
17617
|
+
console.log(`[piwi] Region picked at ${callSiteLine(location)}: ${source}, its ARIA snapshot sent to the editor.`);
|
|
17618
|
+
tellHub({ type: "region", stop, locator: source, snapshot });
|
|
17619
|
+
}
|
|
17620
|
+
function regionSecrets(region) {
|
|
17621
|
+
return region.locator(":scope:is(input, textarea), input, textarea").evaluateAll(
|
|
17622
|
+
(elements, secret) => {
|
|
17623
|
+
return elements.flatMap((field) => {
|
|
17624
|
+
const tokens = (field.getAttribute("autocomplete") ?? "").toLowerCase().split(/\s+/);
|
|
17625
|
+
const password = field.localName === "input" && String(field.type).toLowerCase() === "password";
|
|
17626
|
+
return password || tokens.some((token) => secret.includes(token)) ? [String(field.value ?? "")] : [];
|
|
17627
|
+
});
|
|
17628
|
+
},
|
|
17629
|
+
[...SECRET_AUTOCOMPLETE]
|
|
17630
|
+
);
|
|
17631
|
+
}
|
|
17632
|
+
async function holdFailure(sink, fallback) {
|
|
17633
|
+
const testInfo = sink.testInfo;
|
|
17634
|
+
if (sink.failureHeld || !sink.pause || !testInfo || !editorShows("failure")) return;
|
|
17635
|
+
const { status, expectedStatus } = testInfo;
|
|
17636
|
+
if (status !== "failed" && status !== "timedOut" || status === expectedStatus) return;
|
|
17637
|
+
const page = [sink.lastActivePage, fallback].find((open) => !!open && !isPageClosed(open));
|
|
17638
|
+
const context = page ? pageContext(page) : null;
|
|
17639
|
+
if (!page || !context) return;
|
|
17640
|
+
sink.failureHeld = true;
|
|
17641
|
+
const failed = sink.failedLocators[sink.failedLocators.length - 1] ?? deriveFailedLocator(testInfo);
|
|
17642
|
+
const errors = testInfo.errors.length ? testInfo.errors : testInfo.error ? [testInfo.error] : [];
|
|
17643
|
+
const declared = (place2) => place2.file === testInfo.file && place2.line === testInfo.line;
|
|
17644
|
+
const placed = errors.flatMap((error2) => {
|
|
17645
|
+
const place2 = errorPlace(error2);
|
|
17646
|
+
return place2 && !declared(place2) ? [{ error: error2, place: place2 }] : [];
|
|
17647
|
+
})[0];
|
|
17648
|
+
const error = placed?.error ?? errors[0];
|
|
17649
|
+
const place = placed?.place ?? (failed?.location ? null : stepInProgress(testInfo));
|
|
17650
|
+
const location = (place ? stopCallSite(place) : null) ?? failed?.location ?? stopCallSite({ file: testInfo.file, line: testInfo.line, column: testInfo.column });
|
|
17651
|
+
if (!location) return;
|
|
17652
|
+
const said = stripAnsi(error?.message ?? "").split("\n").find((line) => line.trim()) ?? status;
|
|
17653
|
+
if (page === sink.lastActivePage)
|
|
17654
|
+
await internalCall(page, () => readPageBeforeClose(sink, page)).catch(() => void 0);
|
|
17655
|
+
sink.frozenAt = Date.now();
|
|
17656
|
+
await stopSinkCodeReach(sink, { context });
|
|
17657
|
+
const title = hideSecretsInText(`Failed: ${said.trim().slice(0, 300)}`).slice(0, 1e3);
|
|
17658
|
+
const errorText = failureError(error?.message ?? error?.value);
|
|
17659
|
+
const named = errorText === null ? null : hideSecretsInText(errorText).slice(0, MAX_STOP_ERROR);
|
|
17660
|
+
await showStop(sink, context, title, location, "failure", void 0, { page }, { failed, error: named });
|
|
17661
|
+
}
|
|
17662
|
+
var FAILURE_ERROR_LINES = 4;
|
|
17663
|
+
function failureError(message) {
|
|
17664
|
+
const lines = [];
|
|
17665
|
+
for (const line of stripAnsi(message ?? "").trimStart().replace(/^Error: /, "").split("\n")) {
|
|
17666
|
+
if (!line.trim() || /^\s*Call log:/.test(line)) break;
|
|
17667
|
+
lines.push(line.trimEnd());
|
|
17668
|
+
if (lines.length === FAILURE_ERROR_LINES) break;
|
|
17669
|
+
}
|
|
17670
|
+
const text = lines.join("\n").slice(0, MAX_STOP_ERROR);
|
|
17671
|
+
return text.trim() ? text : null;
|
|
17672
|
+
}
|
|
17673
|
+
function errorPlace(error) {
|
|
17674
|
+
const given = error?.location;
|
|
17675
|
+
if (typeof given?.file === "string" && typeof given.line === "number") {
|
|
17676
|
+
return { file: given.file, line: given.line, column: given.column ?? 0 };
|
|
17677
|
+
}
|
|
17678
|
+
const stack = error?.stack;
|
|
17679
|
+
for (const line of typeof stack === "string" ? stack.split("\n") : []) {
|
|
17680
|
+
const frame = /^\s*at\s+(?:.*?\s+\()?(?:file:\/\/)?(\/[^():]+|[A-Za-z]:[\\/][^():]+):(\d+):(\d+)\)?$/.exec(line);
|
|
17681
|
+
if (!frame) continue;
|
|
17682
|
+
const file = frame[1];
|
|
17683
|
+
if (/[\\/]node_modules[\\/]/.test(file) || path30.dirname(path30.resolve(file)) === __dirname) continue;
|
|
17684
|
+
return { file, line: Number(frame[2]), column: Number(frame[3]) };
|
|
17685
|
+
}
|
|
17686
|
+
return null;
|
|
17687
|
+
}
|
|
17688
|
+
function stepInProgress(testInfo) {
|
|
17689
|
+
let found = null;
|
|
17690
|
+
const visit = (steps) => {
|
|
17691
|
+
if (!Array.isArray(steps)) return;
|
|
17692
|
+
for (const step of steps) {
|
|
17693
|
+
if (step.endWallTime) continue;
|
|
17694
|
+
const at = step.stack?.[0];
|
|
17695
|
+
const called = step.category === "pw:api" || step.category === "expect" || step.category === "test.step";
|
|
17696
|
+
if (called && typeof at?.file === "string" && typeof at.line === "number") {
|
|
17697
|
+
found = { file: at.file, line: at.line, column: typeof at.column === "number" ? at.column : 0 };
|
|
17698
|
+
}
|
|
17699
|
+
visit(step.steps);
|
|
17700
|
+
}
|
|
17701
|
+
};
|
|
17702
|
+
visit(testInfo._steps);
|
|
17703
|
+
return found;
|
|
17704
|
+
}
|
|
17705
|
+
function abortion(signal) {
|
|
17706
|
+
return new Promise((resolve11) => {
|
|
17707
|
+
if (signal.aborted) resolve11();
|
|
17708
|
+
else signal.addEventListener("abort", () => resolve11(), { once: true });
|
|
17709
|
+
});
|
|
16271
17710
|
}
|
|
16272
17711
|
var END_STOP_MS = 2e3;
|
|
16273
17712
|
var RELEASED_CALL_MS = 2e3;
|
|
16274
17713
|
var RELEASED_QUIET_MS = 200;
|
|
16275
17714
|
async function endShownStops(sink, why, closing) {
|
|
16276
17715
|
const ending = [];
|
|
17716
|
+
if (why === "ended") sink.stopEnds++;
|
|
16277
17717
|
let heldAt = Infinity;
|
|
16278
|
-
for (const [end,
|
|
16279
|
-
if (closing &&
|
|
17718
|
+
for (const [end, stop] of sink.shownStops) {
|
|
17719
|
+
if (closing && !closesStop(closing, stop)) continue;
|
|
16280
17720
|
if (!end.signal.aborted) {
|
|
16281
|
-
record.duration = Date.now() - record.startedAt;
|
|
17721
|
+
stop.record.duration = Date.now() - stop.record.startedAt;
|
|
17722
|
+
if (closing?.page) stop.after = CLOSING_PAGES.get(closing.page) ?? null;
|
|
16282
17723
|
end.abort(why);
|
|
16283
|
-
if (why === "ended" &&
|
|
17724
|
+
if (why === "ended" && stop.heldAt !== null) heldAt = Math.min(heldAt, stop.heldAt);
|
|
16284
17725
|
}
|
|
16285
|
-
ending.push(done);
|
|
17726
|
+
ending.push(stop.done);
|
|
16286
17727
|
}
|
|
16287
17728
|
if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
|
|
16288
17729
|
await settledWithin(ending, END_STOP_MS);
|
|
16289
17730
|
}
|
|
17731
|
+
function closesStop(closing, stop) {
|
|
17732
|
+
if (closing.context && stop.context === closing.context) return true;
|
|
17733
|
+
if (!closing.page) return false;
|
|
17734
|
+
return stop.page === closing.page || stop.guessed && pageContext(closing.page) === stop.context;
|
|
17735
|
+
}
|
|
16290
17736
|
function releaseCall(sink, settled) {
|
|
16291
17737
|
sink.releasedQuietSince = Date.now();
|
|
17738
|
+
sink.releasedUntil = null;
|
|
16292
17739
|
sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
|
|
16293
17740
|
}
|
|
16294
17741
|
function releasedRequestsEnded(sink) {
|
|
@@ -16297,8 +17744,8 @@ function releasedRequestsEnded(sink) {
|
|
|
16297
17744
|
}
|
|
16298
17745
|
async function awaitReleasedCalls(sink) {
|
|
16299
17746
|
if (sink.released.length === 0) return;
|
|
16300
|
-
const deadline = Date.now() + RELEASED_CALL_MS;
|
|
16301
|
-
await settledWithin(sink.released,
|
|
17747
|
+
const deadline = sink.releasedUntil ??= Date.now() + RELEASED_CALL_MS;
|
|
17748
|
+
await settledWithin(sink.released, Math.max(0, deadline - Date.now()));
|
|
16302
17749
|
for (let now = Date.now(); now < deadline; now = Date.now()) {
|
|
16303
17750
|
if (sink.releasedRequests.size > 0) {
|
|
16304
17751
|
await settledWithin([releasedRequestsEnded(sink)], deadline - now);
|
|
@@ -16328,7 +17775,7 @@ function recorderAt(page, testInfo, location, reason) {
|
|
|
16328
17775
|
return async (ended) => {
|
|
16329
17776
|
console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
|
|
16330
17777
|
const why = await recordFromHere({ page, testInfo, location, reason, ended });
|
|
16331
|
-
if (ended?.aborted) console.log("[piwi] The recording ended with the
|
|
17778
|
+
if (ended?.aborted) console.log("[piwi] The recording ended with the stop.");
|
|
16332
17779
|
else
|
|
16333
17780
|
console.log(
|
|
16334
17781
|
why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
|
|
@@ -16345,10 +17792,10 @@ async function pauseInPlace(page, playwrightPause, location) {
|
|
|
16345
17792
|
if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
|
|
16346
17793
|
return leaveToPlaywright(context, playwrightPause);
|
|
16347
17794
|
}
|
|
16348
|
-
sink.pause ??=
|
|
17795
|
+
sink.pause ??= pauseState(NO_BREAKPOINTS);
|
|
16349
17796
|
watchStops(context, onDebuggerStop);
|
|
16350
17797
|
await steerDebugger(context, "pause");
|
|
16351
|
-
PAGE_PAUSES.set(context, location);
|
|
17798
|
+
PAGE_PAUSES.set(context, { location, page });
|
|
16352
17799
|
try {
|
|
16353
17800
|
await playwrightPause({ __testHookKeepTestTimeout: true });
|
|
16354
17801
|
} finally {
|
|
@@ -16531,15 +17978,15 @@ function instrumentPage(page) {
|
|
|
16531
17978
|
if (typeof page.close === "function") {
|
|
16532
17979
|
const originalClose = page.close.bind(page);
|
|
16533
17980
|
page.close = async (...args) => {
|
|
16534
|
-
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
await
|
|
16539
|
-
await
|
|
16540
|
-
|
|
17981
|
+
let closed = () => {
|
|
17982
|
+
};
|
|
17983
|
+
CLOSING_PAGES.set(page, new Promise((resolve11) => closed = resolve11));
|
|
17984
|
+
try {
|
|
17985
|
+
await settleBeforeClose({ page });
|
|
17986
|
+
return await originalClose(...args);
|
|
17987
|
+
} finally {
|
|
17988
|
+
closed();
|
|
16541
17989
|
}
|
|
16542
|
-
return originalClose(...args);
|
|
16543
17990
|
};
|
|
16544
17991
|
}
|
|
16545
17992
|
const captureLocators = process.env.PIWI_CAPTURE_LOCATORS !== "false";
|
|
@@ -16584,7 +18031,7 @@ function instrumentPage(page) {
|
|
|
16584
18031
|
});
|
|
16585
18032
|
page.on("console", (msg) => {
|
|
16586
18033
|
const sink = currentSink;
|
|
16587
|
-
if (!sink) return;
|
|
18034
|
+
if (!sink || sink.frozenAt !== null) return;
|
|
16588
18035
|
const type = msg.type();
|
|
16589
18036
|
if (["warning", "error", "assert"].includes(type)) {
|
|
16590
18037
|
const location = msg.location();
|
|
@@ -16600,7 +18047,7 @@ function instrumentPage(page) {
|
|
|
16600
18047
|
try {
|
|
16601
18048
|
page.on("dialogclosed", (dialog) => {
|
|
16602
18049
|
const sink = currentSink;
|
|
16603
|
-
if (!sink) return;
|
|
18050
|
+
if (!sink || sink.frozenAt !== null) return;
|
|
16604
18051
|
try {
|
|
16605
18052
|
sink.dialogs.push({
|
|
16606
18053
|
type: typeof dialog.type === "function" ? dialog.type() : null,
|
|
@@ -16619,12 +18066,14 @@ function instrumentPage(page) {
|
|
|
16619
18066
|
if (!sink) return;
|
|
16620
18067
|
try {
|
|
16621
18068
|
const request3 = response.request();
|
|
18069
|
+
if (HELD_REQUESTS.has(request3)) return;
|
|
16622
18070
|
if (isTrackedRequest(request3.url(), request3.resourceType())) sink.openResponses.set(request3, response);
|
|
16623
18071
|
} catch {
|
|
16624
18072
|
}
|
|
16625
18073
|
});
|
|
16626
18074
|
page.on("request", (request3) => {
|
|
16627
18075
|
const sink = currentSink;
|
|
18076
|
+
if (sink && sink.frozenAt !== null) HELD_REQUESTS.add(request3);
|
|
16628
18077
|
if (!sink || sink.released.length === 0) return;
|
|
16629
18078
|
sink.releasedRequests.add(request3);
|
|
16630
18079
|
sink.releasedQuietSince = Date.now();
|
|
@@ -16635,16 +18084,18 @@ function instrumentPage(page) {
|
|
|
16635
18084
|
sink.releasedQuietSince = Date.now();
|
|
16636
18085
|
if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
|
|
16637
18086
|
}
|
|
16638
|
-
if (!sink || RECORDED_OPEN.has(request3)) return;
|
|
18087
|
+
if (!sink || RECORDED_OPEN.has(request3) || HELD_REQUESTS.has(request3)) return;
|
|
16639
18088
|
sink.lastActivePage = page;
|
|
16640
18089
|
const response = sink.openResponses.get(request3) ?? null;
|
|
16641
18090
|
sink.openResponses.delete(request3);
|
|
16642
18091
|
recordRequest(sink, request3, failed ? "failed" : "finished", response);
|
|
18092
|
+
if (!failed) void keepResponse(request3, response);
|
|
16643
18093
|
};
|
|
16644
18094
|
page.on("requestfinished", (request3) => onRequestEnd(request3, false));
|
|
16645
18095
|
page.on("requestfailed", (request3) => onRequestEnd(request3, true));
|
|
16646
18096
|
}
|
|
16647
18097
|
var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
|
|
18098
|
+
var HELD_REQUESTS = /* @__PURE__ */ new WeakSet();
|
|
16648
18099
|
function isTrackedRequest(url, resourceType) {
|
|
16649
18100
|
if (url.startsWith("data:") || url.startsWith("blob:")) return false;
|
|
16650
18101
|
return TRACKED_REQUEST_TYPES.includes(resourceType);
|
|
@@ -16706,6 +18157,7 @@ function readResponseHeaders(sink, entry, url, headers) {
|
|
|
16706
18157
|
}
|
|
16707
18158
|
}
|
|
16708
18159
|
function recordApiCall(sink, call) {
|
|
18160
|
+
if (sink.frozenAt !== null) return;
|
|
16709
18161
|
try {
|
|
16710
18162
|
const entry = apiCallEntry(call, sink.baseURL);
|
|
16711
18163
|
if (!entry) return;
|
|
@@ -16742,7 +18194,10 @@ function instrumentContext(context) {
|
|
|
16742
18194
|
INSTRUMENTED_CONTEXTS.add(context);
|
|
16743
18195
|
if (typeof context.once === "function") {
|
|
16744
18196
|
OPEN_CONTEXTS.add(context);
|
|
16745
|
-
context.once("close", () =>
|
|
18197
|
+
context.once("close", () => {
|
|
18198
|
+
OPEN_CONTEXTS.delete(context);
|
|
18199
|
+
CLOSING_CONTEXTS.add(context);
|
|
18200
|
+
});
|
|
16746
18201
|
}
|
|
16747
18202
|
observeOwnApiRequests(context);
|
|
16748
18203
|
if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
|
|
@@ -16760,14 +18215,7 @@ function instrumentContext(context) {
|
|
|
16760
18215
|
if (typeof context.close === "function") {
|
|
16761
18216
|
const originalClose = context.close.bind(context);
|
|
16762
18217
|
context.close = async (...args) => {
|
|
16763
|
-
await
|
|
16764
|
-
const sink = currentSink;
|
|
16765
|
-
if (sink) {
|
|
16766
|
-
await endShownStops(sink, "closed", { context });
|
|
16767
|
-
await stashPageState(sink, { context });
|
|
16768
|
-
await maybeOpenPicker(sink, { context });
|
|
16769
|
-
await stopSinkCodeReach(sink, { context });
|
|
16770
|
-
}
|
|
18218
|
+
await settleContextClose(context);
|
|
16771
18219
|
return originalClose(...args);
|
|
16772
18220
|
};
|
|
16773
18221
|
}
|
|
@@ -16780,7 +18228,7 @@ function patchBrowser(browser) {
|
|
|
16780
18228
|
browser.newPage = async (...args) => {
|
|
16781
18229
|
const page = await originalNewPage(...args);
|
|
16782
18230
|
instrumentPage(page);
|
|
16783
|
-
await
|
|
18231
|
+
await armStops([pageContext(page)]);
|
|
16784
18232
|
await awaitFlakeConditions();
|
|
16785
18233
|
await maybeStartCodeReach(page);
|
|
16786
18234
|
return page;
|
|
@@ -16789,13 +18237,16 @@ function patchBrowser(browser) {
|
|
|
16789
18237
|
browser.newContext = async (...args) => {
|
|
16790
18238
|
const context = await originalNewContext(...args);
|
|
16791
18239
|
instrumentContext(context);
|
|
16792
|
-
await
|
|
18240
|
+
await armStops([context]);
|
|
16793
18241
|
return context;
|
|
16794
18242
|
};
|
|
16795
18243
|
if (typeof browser.close === "function") {
|
|
16796
18244
|
const originalClose = browser.close.bind(browser);
|
|
16797
18245
|
browser.close = async (...args) => {
|
|
16798
18246
|
await drainPendingProbes(1e3);
|
|
18247
|
+
for (const context of typeof browser.contexts === "function" ? browser.contexts() : []) {
|
|
18248
|
+
await settleContextClose(context);
|
|
18249
|
+
}
|
|
16799
18250
|
return originalClose(...args);
|
|
16800
18251
|
};
|
|
16801
18252
|
}
|
|
@@ -16848,7 +18299,11 @@ async function flushSink(sink, testInfo) {
|
|
|
16848
18299
|
clearTimeout(drainDeadline);
|
|
16849
18300
|
const page = sink.lastActivePage;
|
|
16850
18301
|
const pageReadable = page !== null && !isPageClosed(page);
|
|
16851
|
-
if (pageReadable)
|
|
18302
|
+
if (pageReadable) {
|
|
18303
|
+
await holdFailure(sink);
|
|
18304
|
+
await maybeOpenPicker(sink);
|
|
18305
|
+
}
|
|
18306
|
+
const live = pageReadable && sink.frozenAt === null;
|
|
16852
18307
|
if (sink.stops.length > 0) {
|
|
16853
18308
|
await testInfo.attach(ATTACHMENT_NAMES.pauses, {
|
|
16854
18309
|
contentType: "application/json",
|
|
@@ -16916,13 +18371,13 @@ async function flushSink(sink, testInfo) {
|
|
|
16916
18371
|
}
|
|
16917
18372
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
|
|
16918
18373
|
try {
|
|
16919
|
-
const snapshot = (
|
|
18374
|
+
const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
16920
18375
|
if (snapshot) {
|
|
16921
18376
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
16922
|
-
contentType:
|
|
18377
|
+
contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
|
|
16923
18378
|
body: snapshot
|
|
16924
18379
|
});
|
|
16925
|
-
const snapshotJson = (
|
|
18380
|
+
const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
16926
18381
|
if (snapshotJson) {
|
|
16927
18382
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
16928
18383
|
contentType: "application/json",
|
|
@@ -16947,7 +18402,7 @@ async function flushSink(sink, testInfo) {
|
|
|
16947
18402
|
}
|
|
16948
18403
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
|
|
16949
18404
|
const option = sink.failureScreenshot;
|
|
16950
|
-
const shot = (
|
|
18405
|
+
const shot = (live ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
|
|
16951
18406
|
if (shot) {
|
|
16952
18407
|
try {
|
|
16953
18408
|
await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
|
|
@@ -16957,13 +18412,13 @@ async function flushSink(sink, testInfo) {
|
|
|
16957
18412
|
}
|
|
16958
18413
|
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
16959
18414
|
try {
|
|
16960
|
-
const snapshot = (
|
|
18415
|
+
const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
16961
18416
|
if (snapshot) {
|
|
16962
18417
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
16963
|
-
contentType:
|
|
18418
|
+
contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
|
|
16964
18419
|
body: snapshot
|
|
16965
18420
|
});
|
|
16966
|
-
const snapshotJson = (
|
|
18421
|
+
const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
16967
18422
|
if (snapshotJson) {
|
|
16968
18423
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
16969
18424
|
contentType: "application/json",
|
|
@@ -17009,7 +18464,7 @@ async function flushSink(sink, testInfo) {
|
|
|
17009
18464
|
body: Buffer.from(JSON.stringify(sink.networkRequests))
|
|
17010
18465
|
});
|
|
17011
18466
|
}
|
|
17012
|
-
const webVitals = (
|
|
18467
|
+
const webVitals = (live ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
|
|
17013
18468
|
if (webVitals) {
|
|
17014
18469
|
await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
|
|
17015
18470
|
contentType: "application/json",
|
|
@@ -17017,7 +18472,7 @@ async function flushSink(sink, testInfo) {
|
|
|
17017
18472
|
});
|
|
17018
18473
|
}
|
|
17019
18474
|
if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
|
|
17020
|
-
const pageState = (
|
|
18475
|
+
const pageState = (live ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
|
|
17021
18476
|
if (pageState) {
|
|
17022
18477
|
await testInfo.attach(ATTACHMENT_NAMES.pageState, {
|
|
17023
18478
|
contentType: "application/json",
|
|
@@ -17026,7 +18481,7 @@ async function flushSink(sink, testInfo) {
|
|
|
17026
18481
|
}
|
|
17027
18482
|
}
|
|
17028
18483
|
if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
17029
|
-
if (
|
|
18484
|
+
if (live) await recordPageWindow(sink, page);
|
|
17030
18485
|
const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
|
|
17031
18486
|
if (windows.length > 0) {
|
|
17032
18487
|
await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
|
|
@@ -17036,21 +18491,79 @@ async function flushSink(sink, testInfo) {
|
|
|
17036
18491
|
}
|
|
17037
18492
|
}
|
|
17038
18493
|
}
|
|
18494
|
+
var pauseNextTest = false;
|
|
18495
|
+
var runControl = {
|
|
18496
|
+
pause() {
|
|
18497
|
+
const sink = currentSink;
|
|
18498
|
+
if (!sink) {
|
|
18499
|
+
pauseNextTest = true;
|
|
18500
|
+
return;
|
|
18501
|
+
}
|
|
18502
|
+
if (!sink.pause) {
|
|
18503
|
+
const why = sink.testInfo ? pauseSkipReason({ ...pauseGateFromTestInfo(sink.testInfo), channel: true }) : null;
|
|
18504
|
+
tellHub({ type: "notice", message: `This test does not stop${why ? `: ${why}` : ""}.` });
|
|
18505
|
+
return;
|
|
18506
|
+
}
|
|
18507
|
+
sink.pause.pause = true;
|
|
18508
|
+
void armStops(OPEN_CONTEXTS);
|
|
18509
|
+
},
|
|
18510
|
+
breakpoints(places) {
|
|
18511
|
+
const set = replacePauseSet(places);
|
|
18512
|
+
const state = currentSink?.pause;
|
|
18513
|
+
if (!state) return;
|
|
18514
|
+
state.set = set;
|
|
18515
|
+
void armStops(OPEN_CONTEXTS);
|
|
18516
|
+
},
|
|
18517
|
+
runTo(place) {
|
|
18518
|
+
const state = currentSink?.pause;
|
|
18519
|
+
if (!state) return;
|
|
18520
|
+
state.runTo = placeLine(place);
|
|
18521
|
+
void armStops(OPEN_CONTEXTS);
|
|
18522
|
+
}
|
|
18523
|
+
};
|
|
18524
|
+
function runFile(file) {
|
|
18525
|
+
return path30.relative(process.cwd(), file).split(path30.sep).join("/") || file;
|
|
18526
|
+
}
|
|
18527
|
+
function testBegin(testInfo, cannotPause) {
|
|
18528
|
+
const title = testInfo.titlePath.slice(1);
|
|
18529
|
+
return {
|
|
18530
|
+
type: "test-begin",
|
|
18531
|
+
testId: testInfo.testId,
|
|
18532
|
+
title: title.length ? title : [testInfo.title],
|
|
18533
|
+
file: runFile(testInfo.file),
|
|
18534
|
+
line: Math.max(1, testInfo.line),
|
|
18535
|
+
project: testInfo.project.name,
|
|
18536
|
+
retry: testInfo.retry,
|
|
18537
|
+
cannotPause
|
|
18538
|
+
};
|
|
18539
|
+
}
|
|
18540
|
+
function testEnd(sink, testInfo) {
|
|
18541
|
+
return {
|
|
18542
|
+
type: "test-end",
|
|
18543
|
+
testId: testInfo.testId,
|
|
18544
|
+
status: testInfo.status ?? "passed",
|
|
18545
|
+
pausedMs: sink.stops.reduce((sum, stop) => sum + stop.duration, 0)
|
|
18546
|
+
};
|
|
18547
|
+
}
|
|
17039
18548
|
var piwiFixtures = {
|
|
17040
18549
|
// Auto, worker-scoped: the worker's resource ledger, and the API request
|
|
17041
18550
|
// contexts its tests create, handed out behind the call proxy. Set up before
|
|
17042
18551
|
// any hook (worker fixtures come first), so a `beforeAll` context is in the
|
|
17043
18552
|
// ledger and records the calls the tests make through it, and torn down after
|
|
17044
18553
|
// the browser closed, so its census sees what only the worker's shutdown
|
|
17045
|
-
// closed. Depends on `playwright` alone, which launches nothing.
|
|
18554
|
+
// closed. Depends on `playwright` alone, which launches nothing. The worker's
|
|
18555
|
+
// run connection to the editor (`PIWI_RUN_CHANNEL`) opens here, before the
|
|
18556
|
+
// first test, and closes as the worker ends.
|
|
17046
18557
|
piwiResources: [
|
|
17047
18558
|
async ({ playwright }, use, workerInfo) => {
|
|
17048
18559
|
boxCaptureFrames();
|
|
17049
18560
|
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
17050
18561
|
observeApiRequests(playwright, apiCallRecorder);
|
|
18562
|
+
await openRunChannel(workerInfo, runControl);
|
|
17051
18563
|
try {
|
|
17052
18564
|
await use();
|
|
17053
18565
|
} finally {
|
|
18566
|
+
closeRunChannel();
|
|
17054
18567
|
stopResourceLedger();
|
|
17055
18568
|
}
|
|
17056
18569
|
},
|
|
@@ -17070,10 +18583,12 @@ var piwiFixtures = {
|
|
|
17070
18583
|
// never double-wraps a page the browser patch already instrumented.
|
|
17071
18584
|
page: async ({ page }, use) => {
|
|
17072
18585
|
instrumentPage(page);
|
|
17073
|
-
await
|
|
18586
|
+
await armStops([pageContext(page)]);
|
|
17074
18587
|
await awaitFlakeConditions();
|
|
17075
18588
|
await maybeStartCodeReach(page);
|
|
17076
18589
|
await use(page);
|
|
18590
|
+
const sink = currentSink;
|
|
18591
|
+
if (sink) await holdFailure(sink, page);
|
|
17077
18592
|
},
|
|
17078
18593
|
// Auto, test-scoped: open a capture sink for the running test and flush it
|
|
17079
18594
|
// (attach the collected data) at teardown. Runs for every test without being
|
|
@@ -17088,12 +18603,16 @@ var piwiFixtures = {
|
|
|
17088
18603
|
sink.baseURL = baseURL ?? null;
|
|
17089
18604
|
if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
|
|
17090
18605
|
const pauseSet = currentPauseSet();
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
18606
|
+
const channel = runChannelOpen();
|
|
18607
|
+
const cannotPause = pauseSkipReason({ ...pauseGateFromTestInfo(testInfo), channel });
|
|
18608
|
+
if (pauseSet.size || channel) {
|
|
18609
|
+
if (!cannotPause) sink.pause = pauseState(pauseSet, pauseNextTest);
|
|
18610
|
+
else if (pauseSet.size) logPauseSkipOnce(cannotPause);
|
|
17095
18611
|
}
|
|
18612
|
+
pauseNextTest = false;
|
|
17096
18613
|
if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
|
|
18614
|
+
if (channel) tellHub(testBegin(testInfo, cannotPause));
|
|
18615
|
+
if (channel && sink.pause) startKeeping();
|
|
17097
18616
|
if (isFlakeMode()) {
|
|
17098
18617
|
const plan = loadFlakePlan();
|
|
17099
18618
|
const role = flakeRoleForTest(plan, {
|
|
@@ -17117,14 +18636,18 @@ var piwiFixtures = {
|
|
|
17117
18636
|
});
|
|
17118
18637
|
activeResourceLedger()?.testStarted(testInfo);
|
|
17119
18638
|
currentSink = sink;
|
|
18639
|
+
const showRunChannel = hideRunChannel();
|
|
17120
18640
|
try {
|
|
17121
|
-
await
|
|
18641
|
+
await armStops(OPEN_CONTEXTS);
|
|
17122
18642
|
await startCodeReachOnOpenPages();
|
|
17123
18643
|
await use();
|
|
17124
18644
|
} finally {
|
|
18645
|
+
sink.ending = true;
|
|
17125
18646
|
await endShownStops(sink, "ended");
|
|
17126
18647
|
await awaitReleasedCalls(sink);
|
|
18648
|
+
showRunChannel();
|
|
17127
18649
|
currentSink = null;
|
|
18650
|
+
if (sink.pause?.pause) pauseNextTest = true;
|
|
17128
18651
|
if (sink.probeItem) {
|
|
17129
18652
|
const applied = sink.probeInterception?.applied() ?? false;
|
|
17130
18653
|
const level = sink.probeItem.level ?? "client";
|
|
@@ -17147,7 +18670,12 @@ var piwiFixtures = {
|
|
|
17147
18670
|
});
|
|
17148
18671
|
}
|
|
17149
18672
|
if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
|
|
17150
|
-
|
|
18673
|
+
try {
|
|
18674
|
+
await flushSink(sink, testInfo);
|
|
18675
|
+
} finally {
|
|
18676
|
+
stopKeeping();
|
|
18677
|
+
if (runChannelOpen()) tellHub(testEnd(sink, testInfo));
|
|
18678
|
+
}
|
|
17151
18679
|
await recordResourceCensus(testInfo);
|
|
17152
18680
|
}
|
|
17153
18681
|
},
|
|
@@ -17328,7 +18856,7 @@ function withEntryLock(file, fn) {
|
|
|
17328
18856
|
}
|
|
17329
18857
|
|
|
17330
18858
|
// src/internal/ai/keys.ts
|
|
17331
|
-
var
|
|
18859
|
+
var crypto9 = __toESM(require("crypto"));
|
|
17332
18860
|
var path32 = __toESM(require("path"));
|
|
17333
18861
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
17334
18862
|
function normalizeTemplate(template) {
|
|
@@ -17336,7 +18864,7 @@ function normalizeTemplate(template) {
|
|
|
17336
18864
|
}
|
|
17337
18865
|
function hashTemplate(normalized, ordinal = 0) {
|
|
17338
18866
|
const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
|
|
17339
|
-
return
|
|
18867
|
+
return crypto9.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
17340
18868
|
}
|
|
17341
18869
|
function slug(text, maxLength = 40) {
|
|
17342
18870
|
const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|