@piwitests/reporter 0.60.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 +437 -229
- package/dist/global-setup-module.js +66 -9
- package/dist/index.d.ts +21 -2
- package/dist/index.js +2534 -371
- package/dist/internal/capture/attachments.d.ts +26 -1
- package/dist/internal/capture/attachments.js +23 -2
- package/dist/internal/capture/capture-fixtures.d.ts +114 -6
- package/dist/internal/capture/capture-fixtures.js +2028 -236
- 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
|
}
|
|
@@ -203,6 +203,7 @@ var PIWI_ENV_KEYS = {
|
|
|
203
203
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
204
204
|
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
205
205
|
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
206
|
+
coverageCss: "PIWI_COVERAGE_CSS",
|
|
206
207
|
externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
|
|
207
208
|
externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
|
|
208
209
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
@@ -357,10 +358,14 @@ function resolveOptions(raw, configDir) {
|
|
|
357
358
|
const coverage = { ...mergedRaw.coverage };
|
|
358
359
|
const coverageBrowser = env[PIWI_ENV_KEYS.coverageBrowser];
|
|
359
360
|
const coverageServer = env[PIWI_ENV_KEYS.coverageServer];
|
|
361
|
+
const coverageCss = env[PIWI_ENV_KEYS.coverageCss];
|
|
360
362
|
if (coverage.browser === void 0 && coverageBrowser)
|
|
361
363
|
coverage.browser = coverageBrowser;
|
|
362
364
|
if (coverage.server === void 0 && coverageServer) coverage.server = coverageServer;
|
|
363
|
-
if (coverage.
|
|
365
|
+
if (coverage.css === void 0 && coverageCss) coverage.css = coverageCss;
|
|
366
|
+
if (coverage.browser !== void 0 || coverage.server !== void 0 || coverage.css !== void 0) {
|
|
367
|
+
mergedRaw.coverage = coverage;
|
|
368
|
+
}
|
|
364
369
|
const externalId = { ...mergedRaw.externalId };
|
|
365
370
|
const externalIdPattern = env[PIWI_ENV_KEYS.externalIdPattern];
|
|
366
371
|
const externalIdOnDuplicate = env[PIWI_ENV_KEYS.externalIdOnDuplicate];
|
|
@@ -406,9 +411,11 @@ function applyOptionsToEnv(options) {
|
|
|
406
411
|
if (options.collectPerformanceMetrics === false) {
|
|
407
412
|
env[PIWI_ENV_KEYS.coverageBrowser] = "off";
|
|
408
413
|
env[PIWI_ENV_KEYS.coverageServer] = "off";
|
|
414
|
+
env[PIWI_ENV_KEYS.coverageCss] = "off";
|
|
409
415
|
} else {
|
|
410
416
|
if (options.coverage?.browser !== void 0) env[PIWI_ENV_KEYS.coverageBrowser] = options.coverage.browser;
|
|
411
417
|
if (options.coverage?.server !== void 0) env[PIWI_ENV_KEYS.coverageServer] = options.coverage.server;
|
|
418
|
+
if (options.coverage?.css !== void 0) env[PIWI_ENV_KEYS.coverageCss] = options.coverage.css;
|
|
412
419
|
}
|
|
413
420
|
if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
|
|
414
421
|
env[PIWI_ENV_KEYS.captureServerTraces] = "false";
|
|
@@ -1063,14 +1070,14 @@ var os3 = __toESM(require("os"));
|
|
|
1063
1070
|
var fs4 = __toESM(require("fs"));
|
|
1064
1071
|
|
|
1065
1072
|
// src/internal/support/instance-id.ts
|
|
1066
|
-
var
|
|
1073
|
+
var crypto2 = __toESM(require("crypto"));
|
|
1067
1074
|
var os2 = __toESM(require("os"));
|
|
1068
1075
|
function hashForProject(projectName) {
|
|
1069
|
-
return
|
|
1076
|
+
return crypto2.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
|
|
1070
1077
|
}
|
|
1071
1078
|
function computeInstanceId(projectName, runLabel) {
|
|
1072
1079
|
const seed = runLabel ? `${projectName}|${runLabel}` : `${os2.hostname()}|${projectName}`;
|
|
1073
|
-
return
|
|
1080
|
+
return crypto2.createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1074
1081
|
}
|
|
1075
1082
|
|
|
1076
1083
|
// src/internal/streaming/stream-buffer.ts
|
|
@@ -1132,7 +1139,7 @@ var StreamBuffer = class {
|
|
|
1132
1139
|
};
|
|
1133
1140
|
|
|
1134
1141
|
// src/internal/streaming/crash-recovery.ts
|
|
1135
|
-
var
|
|
1142
|
+
var crypto3 = __toESM(require("crypto"));
|
|
1136
1143
|
var fs5 = __toESM(require("fs"));
|
|
1137
1144
|
var os4 = __toESM(require("os"));
|
|
1138
1145
|
var path5 = __toESM(require("path"));
|
|
@@ -1148,7 +1155,7 @@ function recoveryServer(serverUrl) {
|
|
|
1148
1155
|
return serverUrl.replace(/\/+$/, "");
|
|
1149
1156
|
}
|
|
1150
1157
|
function recoveryFilePath(serverUrl, projectName) {
|
|
1151
|
-
const key =
|
|
1158
|
+
const key = crypto3.createHash("sha256").update(`${recoveryServer(serverUrl)}
|
|
1152
1159
|
${projectName}`).digest("hex").slice(0, 16);
|
|
1153
1160
|
return path5.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
|
|
1154
1161
|
}
|
|
@@ -1169,7 +1176,7 @@ var CrashRecovery = class {
|
|
|
1169
1176
|
/** Write the payload for a later retry, owner-only, replacing the file in one step. */
|
|
1170
1177
|
save(payload) {
|
|
1171
1178
|
const file = { v: 1, serverUrl: this.serverUrl, payload };
|
|
1172
|
-
const temp = `${this.filePath}.${process.pid}.${
|
|
1179
|
+
const temp = `${this.filePath}.${process.pid}.${crypto3.randomBytes(6).toString("hex")}.tmp`;
|
|
1173
1180
|
try {
|
|
1174
1181
|
fs5.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
|
|
1175
1182
|
fs5.renameSync(temp, this.filePath);
|
|
@@ -1240,7 +1247,7 @@ var CrashRecovery = class {
|
|
|
1240
1247
|
var fs7 = __toESM(require("fs"));
|
|
1241
1248
|
var os5 = __toESM(require("os"));
|
|
1242
1249
|
var path7 = __toESM(require("path"));
|
|
1243
|
-
var
|
|
1250
|
+
var crypto4 = __toESM(require("crypto"));
|
|
1244
1251
|
|
|
1245
1252
|
// src/internal/files/compression.ts
|
|
1246
1253
|
var fs6 = __toESM(require("fs"));
|
|
@@ -1312,6 +1319,8 @@ var ATTACHMENT_NAMES = {
|
|
|
1312
1319
|
codeReach: "piwi-code-reach",
|
|
1313
1320
|
browserCoverage: "piwi-browser-coverage",
|
|
1314
1321
|
browserFunctions: "piwi-browser-functions",
|
|
1322
|
+
cssCoverage: "piwi-css-coverage",
|
|
1323
|
+
cssRules: "piwi-css-rules",
|
|
1315
1324
|
serverCoverageRequests: "piwi-server-coverage-requests",
|
|
1316
1325
|
webVitals: "piwi-web-vitals",
|
|
1317
1326
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
@@ -1323,6 +1332,19 @@ var ATTACHMENT_NAMES = {
|
|
|
1323
1332
|
pauses: "piwi-pauses"
|
|
1324
1333
|
};
|
|
1325
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
|
+
}
|
|
1326
1348
|
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
1327
1349
|
var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
|
|
1328
1350
|
|
|
@@ -1441,18 +1463,18 @@ var FileHandler = class {
|
|
|
1441
1463
|
}
|
|
1442
1464
|
const contentType = a.contentType || "application/octet-stream";
|
|
1443
1465
|
const originalName = `${safeFileName(a.name || "attachment")}${BODY_EXTENSIONS[contentType] ?? ""}`;
|
|
1444
|
-
let
|
|
1445
|
-
if (!
|
|
1466
|
+
let filePath3 = this.bodyFiles.get(a);
|
|
1467
|
+
if (!filePath3) {
|
|
1446
1468
|
try {
|
|
1447
|
-
|
|
1448
|
-
fs7.writeFileSync(
|
|
1449
|
-
this.bodyFiles.set(a,
|
|
1469
|
+
filePath3 = path7.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
|
|
1470
|
+
fs7.writeFileSync(filePath3, body);
|
|
1471
|
+
this.bodyFiles.set(a, filePath3);
|
|
1450
1472
|
} catch (error) {
|
|
1451
1473
|
this.logger.warn(`Could not stage attachment "${a.name}" for upload: ${errorMessage(error)}`);
|
|
1452
1474
|
return null;
|
|
1453
1475
|
}
|
|
1454
1476
|
}
|
|
1455
|
-
return { name: a.name || "attachment", path:
|
|
1477
|
+
return { name: a.name || "attachment", path: filePath3, contentType, originalName };
|
|
1456
1478
|
}
|
|
1457
1479
|
bodyAttachmentDir() {
|
|
1458
1480
|
if (!this.bodyDir) this.bodyDir = fs7.mkdtempSync(path7.join(os5.tmpdir(), "piwi-dashboard-attachments-"));
|
|
@@ -1477,11 +1499,11 @@ var FileHandler = class {
|
|
|
1477
1499
|
}
|
|
1478
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 */
|
|
1479
1501
|
parsePerformanceAttachments(testCase, attachments) {
|
|
1480
|
-
const find = (name) => attachments
|
|
1481
|
-
const
|
|
1482
|
-
if (
|
|
1502
|
+
const find = (name) => findPiwiAttachment(attachments, name);
|
|
1503
|
+
const net5 = find(ATTACHMENT_NAMES.network);
|
|
1504
|
+
if (net5?.body) {
|
|
1483
1505
|
try {
|
|
1484
|
-
testCase.networkRequests = JSON.parse(
|
|
1506
|
+
testCase.networkRequests = JSON.parse(net5.body.toString());
|
|
1485
1507
|
} catch {
|
|
1486
1508
|
}
|
|
1487
1509
|
}
|
|
@@ -1540,7 +1562,7 @@ var FileHandler = class {
|
|
|
1540
1562
|
if (fs7.existsSync(tp)) lastPath = tp;
|
|
1541
1563
|
}
|
|
1542
1564
|
if (!lastPath) return null;
|
|
1543
|
-
const hash =
|
|
1565
|
+
const hash = crypto4.createHash("sha256");
|
|
1544
1566
|
await new Promise((resolve11, reject) => {
|
|
1545
1567
|
fs7.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve11).on("error", reject);
|
|
1546
1568
|
});
|
|
@@ -2119,6 +2141,8 @@ var StreamManager = class {
|
|
|
2119
2141
|
this._token = null;
|
|
2120
2142
|
this._auth = null;
|
|
2121
2143
|
this._startPromise = null;
|
|
2144
|
+
/** Called when the dashboard says someone opened the run. */
|
|
2145
|
+
this.onWatched = null;
|
|
2122
2146
|
const maxBytes = this.options.maxStreamBufferBytes ?? 0;
|
|
2123
2147
|
this.pendingEvents = new BoundedEventQueue(maxBytes);
|
|
2124
2148
|
this.pendingBeginEvents = new BoundedEventQueue(maxBytes);
|
|
@@ -2143,6 +2167,10 @@ var StreamManager = class {
|
|
|
2143
2167
|
get startPromise() {
|
|
2144
2168
|
return this._startPromise;
|
|
2145
2169
|
}
|
|
2170
|
+
/** Whether anyone has the run open live, as the dashboard last said; `null` until it says. */
|
|
2171
|
+
get isWatched() {
|
|
2172
|
+
return this.watched;
|
|
2173
|
+
}
|
|
2146
2174
|
/**
|
|
2147
2175
|
* Whether a test-result (`complete`) event never reached the server through
|
|
2148
2176
|
* the live stream: shed under buffer pressure, too large for any request the
|
|
@@ -2393,8 +2421,11 @@ var StreamManager = class {
|
|
|
2393
2421
|
if (this.pendingEvents.length > 0) this.flush();
|
|
2394
2422
|
}, delay);
|
|
2395
2423
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2424
|
+
/**
|
|
2425
|
+
* Take the dashboard's answer on whether anyone watches the run, from any
|
|
2426
|
+
* request of the stream. An answer without the flag (an older dashboard)
|
|
2427
|
+
* keeps step events flowing.
|
|
2428
|
+
*/
|
|
2398
2429
|
noteWatched(response) {
|
|
2399
2430
|
const watched = response?.watched;
|
|
2400
2431
|
const next = typeof watched === "boolean" ? watched : null;
|
|
@@ -2403,7 +2434,9 @@ var StreamManager = class {
|
|
|
2403
2434
|
next ? "The run is open on the dashboard: streaming live steps." : "Nobody is watching the run: live steps are not sent."
|
|
2404
2435
|
);
|
|
2405
2436
|
}
|
|
2437
|
+
const opened = next === true && this.watched !== true;
|
|
2406
2438
|
this.watched = next;
|
|
2439
|
+
if (opened) this.onWatched?.();
|
|
2407
2440
|
}
|
|
2408
2441
|
// Idle time after which a heartbeat goes out.
|
|
2409
2442
|
heartbeatDelay() {
|
|
@@ -3367,6 +3400,258 @@ function maskUrlFragment(hash) {
|
|
|
3367
3400
|
const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
|
|
3368
3401
|
return hash.startsWith("#") ? `#${out}` : out;
|
|
3369
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
|
+
}
|
|
3370
3655
|
|
|
3371
3656
|
// ../core/src/locator-break.ts
|
|
3372
3657
|
var filePath = (path34) => path34.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -3903,7 +4188,7 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
|
|
|
3903
4188
|
const maxFrames = opts.maxFrames ?? 4;
|
|
3904
4189
|
const picked = [];
|
|
3905
4190
|
const seen = /* @__PURE__ */ new Set();
|
|
3906
|
-
const
|
|
4191
|
+
const add2 = (absFile, line) => {
|
|
3907
4192
|
const key = `${absFile}:${line}`;
|
|
3908
4193
|
if (seen.has(key)) return;
|
|
3909
4194
|
seen.add(key);
|
|
@@ -3920,12 +4205,12 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
|
|
|
3920
4205
|
}
|
|
3921
4206
|
const rel = path11.relative(projectRoot, abs);
|
|
3922
4207
|
if (rel.startsWith("..") || path11.isAbsolute(rel)) continue;
|
|
3923
|
-
if (!isNaN(frame.line))
|
|
4208
|
+
if (!isNaN(frame.line)) add2(abs, frame.line);
|
|
3924
4209
|
}
|
|
3925
4210
|
}
|
|
3926
4211
|
const absTest = path11.resolve(testFile2);
|
|
3927
4212
|
if (!picked.some((p) => p.absFile === absTest)) {
|
|
3928
|
-
|
|
4213
|
+
add2(absTest, extractFailingLine(errorText, testFile2, declLine));
|
|
3929
4214
|
}
|
|
3930
4215
|
const frames = [];
|
|
3931
4216
|
for (const p of picked.slice(0, maxFrames)) {
|
|
@@ -4040,8 +4325,8 @@ var fs15 = __toESM(require("fs"));
|
|
|
4040
4325
|
var path12 = __toESM(require("path"));
|
|
4041
4326
|
var os7 = __toESM(require("os"));
|
|
4042
4327
|
var fs12 = __toESM(require("fs"));
|
|
4043
|
-
function ariaSampleIdentity(
|
|
4044
|
-
return `${
|
|
4328
|
+
function ariaSampleIdentity(filePath3, title) {
|
|
4329
|
+
return `${filePath3}\0${title}`;
|
|
4045
4330
|
}
|
|
4046
4331
|
function getAriaSampleFilePath(projectName) {
|
|
4047
4332
|
return path12.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
|
|
@@ -4085,6 +4370,7 @@ function isDueForAriaSample(testInfo) {
|
|
|
4085
4370
|
}
|
|
4086
4371
|
|
|
4087
4372
|
// src/internal/support/base-url-check.ts
|
|
4373
|
+
var net = __toESM(require("net"));
|
|
4088
4374
|
var BASE_URL_CHECK_ATTEMPTS = 3;
|
|
4089
4375
|
var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
|
|
4090
4376
|
var BASE_URL_CHECK_DELAY_MS = 2e3;
|
|
@@ -4106,9 +4392,9 @@ function cliProjectNames(argv = process.argv) {
|
|
|
4106
4392
|
}
|
|
4107
4393
|
return names;
|
|
4108
4394
|
}
|
|
4109
|
-
function wildcard(pattern) {
|
|
4395
|
+
function wildcard(pattern, flags = "i") {
|
|
4110
4396
|
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
4111
|
-
return new RegExp(`^${escaped.join(".*")}$`,
|
|
4397
|
+
return new RegExp(`^${escaped.join(".*")}$`, flags);
|
|
4112
4398
|
}
|
|
4113
4399
|
function selectedProjects(projects, argv = process.argv, filtered) {
|
|
4114
4400
|
let chosen;
|
|
@@ -4118,7 +4404,7 @@ function selectedProjects(projects, argv = process.argv, filtered) {
|
|
|
4118
4404
|
} else {
|
|
4119
4405
|
const names = cliProjectNames(argv);
|
|
4120
4406
|
if (!names || names.length === 0) return projects;
|
|
4121
|
-
const patterns = names.map(wildcard);
|
|
4407
|
+
const patterns = names.map((name) => wildcard(name));
|
|
4122
4408
|
chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
|
|
4123
4409
|
}
|
|
4124
4410
|
if (!argv.includes("--no-deps")) {
|
|
@@ -4140,23 +4426,58 @@ function asProxy(value) {
|
|
|
4140
4426
|
const proxy = value;
|
|
4141
4427
|
return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
|
|
4142
4428
|
}
|
|
4143
|
-
function
|
|
4429
|
+
function exposeNetworkCovers(rule, url) {
|
|
4430
|
+
if (!rule) return false;
|
|
4431
|
+
let parsed2;
|
|
4432
|
+
try {
|
|
4433
|
+
parsed2 = new URL(url);
|
|
4434
|
+
} catch {
|
|
4435
|
+
return false;
|
|
4436
|
+
}
|
|
4437
|
+
const host = parsed2.hostname;
|
|
4438
|
+
const port = parsed2.port ? Number(parsed2.port) : parsed2.protocol === "https:" ? 443 : 80;
|
|
4439
|
+
return rule.split(",").some((token) => {
|
|
4440
|
+
const match = token.match(/^(.*?)(?::(\d+))?$/);
|
|
4441
|
+
if (!match) return false;
|
|
4442
|
+
if (match[2] !== void 0 && Number(match[2]) !== port) return false;
|
|
4443
|
+
const tokenHost = match[1];
|
|
4444
|
+
if (tokenHost === "<loopback>") {
|
|
4445
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "[::1]";
|
|
4446
|
+
}
|
|
4447
|
+
if (tokenHost === "*") return true;
|
|
4448
|
+
if (net.isIP(tokenHost)) return host === tokenHost;
|
|
4449
|
+
if (net.isIP(host)) return false;
|
|
4450
|
+
return wildcard(tokenHost.startsWith(".") ? `*${tokenHost}` : tokenHost, "").test(host);
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
function reachedRemotely(use, url, env) {
|
|
4454
|
+
if (use?.connectOptions === void 0) {
|
|
4455
|
+
return !!env.PW_TEST_CONNECT_WS_ENDPOINT && !exposeNetworkCovers(env.PW_TEST_CONNECT_EXPOSE_NETWORK, url);
|
|
4456
|
+
}
|
|
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);
|
|
4460
|
+
}
|
|
4461
|
+
function baseUrlTargets(config, argv = process.argv, env = process.env) {
|
|
4144
4462
|
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
4145
4463
|
const filtered = config.projects?.length ? config.filteredProjects : void 0;
|
|
4146
4464
|
const targets = /* @__PURE__ */ new Map();
|
|
4147
4465
|
for (const project of selectedProjects(projects, argv, filtered)) {
|
|
4148
4466
|
const url = project.use?.baseURL;
|
|
4149
4467
|
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
|
|
4468
|
+
const remoteBrowser = reachedRemotely(project.use, url, env);
|
|
4150
4469
|
const target = targets.get(url);
|
|
4151
4470
|
if (target) {
|
|
4152
4471
|
if (project.name) target.projects.push(project.name);
|
|
4472
|
+
target.remoteBrowser ||= remoteBrowser;
|
|
4153
4473
|
continue;
|
|
4154
4474
|
}
|
|
4155
4475
|
targets.set(url, {
|
|
4156
4476
|
url,
|
|
4157
4477
|
projects: project.name ? [project.name] : [],
|
|
4158
4478
|
ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
|
|
4159
|
-
proxy: asProxy(project.use?.proxy)
|
|
4479
|
+
proxy: asProxy(project.use?.proxy),
|
|
4480
|
+
remoteBrowser
|
|
4160
4481
|
});
|
|
4161
4482
|
}
|
|
4162
4483
|
return [...targets.values()];
|
|
@@ -4197,14 +4518,21 @@ async function checkBaseUrl(target, options = {}) {
|
|
|
4197
4518
|
}
|
|
4198
4519
|
return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
|
|
4199
4520
|
}
|
|
4521
|
+
function remoteBrowserNote(target) {
|
|
4522
|
+
if (!target.remoteBrowser) return null;
|
|
4523
|
+
const { host } = new URL(target.url);
|
|
4524
|
+
return `The check ran from this machine's network, but a project's remote browser reaches ${host} from its own, since its exposeNetwork rule does not cover that address.`;
|
|
4525
|
+
}
|
|
4200
4526
|
async function checkBaseUrls(config, options = {}) {
|
|
4201
|
-
const targets = baseUrlTargets(config, options.argv);
|
|
4527
|
+
const targets = baseUrlTargets(config, options.argv, options.env);
|
|
4202
4528
|
const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
|
|
4203
4529
|
const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
|
|
4204
4530
|
if (down.length === 0) return targets;
|
|
4205
4531
|
const lines = down.map(({ target, reason }) => {
|
|
4206
4532
|
const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
|
|
4207
|
-
|
|
4533
|
+
const note = remoteBrowserNote(target);
|
|
4534
|
+
return ` ${target.url}${used}: ${reason}${note ? `
|
|
4535
|
+
${note}` : ""}`;
|
|
4208
4536
|
});
|
|
4209
4537
|
const message = [
|
|
4210
4538
|
`[Piwi Dashboard] The app under test did not answer, so no test ran:`,
|
|
@@ -4545,7 +4873,7 @@ function reduceCoverage(capture, granularity) {
|
|
|
4545
4873
|
}
|
|
4546
4874
|
|
|
4547
4875
|
// src/internal/collect/source-checksums.ts
|
|
4548
|
-
var
|
|
4876
|
+
var crypto5 = __toESM(require("crypto"));
|
|
4549
4877
|
var fs14 = __toESM(require("fs"));
|
|
4550
4878
|
var path14 = __toESM(require("path"));
|
|
4551
4879
|
var CHECKSUM = /^(sha1|sha256):[0-9a-f]{40,64}$/;
|
|
@@ -4563,7 +4891,7 @@ function contentChecksums(content) {
|
|
|
4563
4891
|
const out = /* @__PURE__ */ new Set();
|
|
4564
4892
|
for (const form of lineEndingForms(content)) {
|
|
4565
4893
|
for (const algorithm of ["sha1", "sha256"]) {
|
|
4566
|
-
out.add(`${algorithm}:${
|
|
4894
|
+
out.add(`${algorithm}:${crypto5.createHash(algorithm).update(form).digest("hex")}`);
|
|
4567
4895
|
}
|
|
4568
4896
|
}
|
|
4569
4897
|
return out;
|
|
@@ -4675,10 +5003,10 @@ function fileChecksums(files, into = /* @__PURE__ */ new Map()) {
|
|
|
4675
5003
|
if (!Array.isArray(files)) return into;
|
|
4676
5004
|
for (const item of files) {
|
|
4677
5005
|
if (!isRecord2(item)) continue;
|
|
4678
|
-
const
|
|
5006
|
+
const filePath3 = normalizeCoveragePath(item.path);
|
|
4679
5007
|
const checksums = checksumsOf(item.checksums);
|
|
4680
|
-
if (
|
|
4681
|
-
into.set(
|
|
5008
|
+
if (filePath3 === null || checksums.length === 0) continue;
|
|
5009
|
+
into.set(filePath3, [.../* @__PURE__ */ new Set([...into.get(filePath3) ?? [], ...checksums])]);
|
|
4682
5010
|
}
|
|
4683
5011
|
return into;
|
|
4684
5012
|
}
|
|
@@ -4687,9 +5015,9 @@ function runFilesOf(answer) {
|
|
|
4687
5015
|
const files = [];
|
|
4688
5016
|
for (const item of answer.files) {
|
|
4689
5017
|
if (!isRecord2(item) || item.runtime !== "server" && item.runtime !== "browser") continue;
|
|
4690
|
-
const
|
|
4691
|
-
if (
|
|
4692
|
-
const file = { path:
|
|
5018
|
+
const filePath3 = normalizeCoveragePath(item.path);
|
|
5019
|
+
if (filePath3 === null) continue;
|
|
5020
|
+
const file = { path: filePath3, runtime: item.runtime };
|
|
4693
5021
|
const functions = normalizeFunctionRanges(item.functions);
|
|
4694
5022
|
const lines = normalizeLineRanges(item.lines);
|
|
4695
5023
|
const executable = normalizeLineRanges(item.executable);
|
|
@@ -4709,7 +5037,7 @@ function instanceOf(answer) {
|
|
|
4709
5037
|
return { id: instance.id, startedAt };
|
|
4710
5038
|
}
|
|
4711
5039
|
function processKeyOf(id) {
|
|
4712
|
-
const dot = id.
|
|
5040
|
+
const dot = id.indexOf(".");
|
|
4713
5041
|
return dot > 0 ? id.slice(0, dot) : null;
|
|
4714
5042
|
}
|
|
4715
5043
|
function withTestCoverage(files, tests) {
|
|
@@ -5127,7 +5455,16 @@ function createGlobalSetup(options, userSetup) {
|
|
|
5127
5455
|
}
|
|
5128
5456
|
if (opts.checkBaseUrl === true) {
|
|
5129
5457
|
const checked = await checkBaseUrls(config ?? {});
|
|
5130
|
-
for (const target of checked)
|
|
5458
|
+
for (const target of checked) {
|
|
5459
|
+
const note = remoteBrowserNote(target);
|
|
5460
|
+
if (note) {
|
|
5461
|
+
logger.info(
|
|
5462
|
+
`Base URL answered: ${target.url}. ${note} A navigation failure from that browser can mean its machine cannot reach the app, not that the app is down.`
|
|
5463
|
+
);
|
|
5464
|
+
} else {
|
|
5465
|
+
logger.debug(`Base URL answered: ${target.url}`);
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5131
5468
|
}
|
|
5132
5469
|
if (opts.enabled === false || !opts.serverUrl) {
|
|
5133
5470
|
logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
@@ -5303,9 +5640,9 @@ function loadPlan() {
|
|
|
5303
5640
|
function probeItemForTest(test) {
|
|
5304
5641
|
const plan = loadPlan();
|
|
5305
5642
|
if (!plan) return null;
|
|
5306
|
-
const
|
|
5643
|
+
const filePath3 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
|
|
5307
5644
|
const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
|
|
5308
|
-
return matchProbeItem(plan, { title: test.title, filePath:
|
|
5645
|
+
return matchProbeItem(plan, { title: test.title, filePath: filePath3, suitePath });
|
|
5309
5646
|
}
|
|
5310
5647
|
function classifyProbeHandled(signals) {
|
|
5311
5648
|
const blank = signals.blankPage === true;
|
|
@@ -6300,9 +6637,9 @@ function duplicateTestKey(test) {
|
|
|
6300
6637
|
}
|
|
6301
6638
|
function readDuplicateTest(raw) {
|
|
6302
6639
|
if (!raw || typeof raw !== "object") return null;
|
|
6303
|
-
const { filePath:
|
|
6304
|
-
if (typeof
|
|
6305
|
-
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 };
|
|
6306
6643
|
if (Array.isArray(suitePath) && suitePath.every((s) => typeof s === "string") && suitePath.length > 0)
|
|
6307
6644
|
test.suitePath = suitePath;
|
|
6308
6645
|
if (typeof line === "number" && Number.isFinite(line)) test.line = line;
|
|
@@ -6859,7 +7196,9 @@ var RunSubmitter = class {
|
|
|
6859
7196
|
};
|
|
6860
7197
|
|
|
6861
7198
|
// src/internal/capture/resource-ledger.ts
|
|
7199
|
+
var childProcess = __toESM(require("child_process"));
|
|
6862
7200
|
var fs21 = __toESM(require("fs"));
|
|
7201
|
+
var net2 = __toESM(require("net"));
|
|
6863
7202
|
var path20 = __toESM(require("path"));
|
|
6864
7203
|
var import_node_module2 = require("module");
|
|
6865
7204
|
|
|
@@ -7237,6 +7576,8 @@ var WorkerMetrics = class {
|
|
|
7237
7576
|
constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
|
|
7238
7577
|
this.proc = proc;
|
|
7239
7578
|
this.pid = pid;
|
|
7579
|
+
/** The lowest heap read right after a full garbage collection in the current window. */
|
|
7580
|
+
this.heapAfterGc = null;
|
|
7240
7581
|
this.loop = null;
|
|
7241
7582
|
this.usage = null;
|
|
7242
7583
|
/** Each process's CPU and wait at the start of the current window; null before the first. */
|
|
@@ -7251,6 +7592,28 @@ var WorkerMetrics = class {
|
|
|
7251
7592
|
histogram = null;
|
|
7252
7593
|
}
|
|
7253
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
|
+
}
|
|
7254
7617
|
}
|
|
7255
7618
|
/** The worker's processes under it, keyed by pid and start time. */
|
|
7256
7619
|
tree() {
|
|
@@ -7284,6 +7647,7 @@ var WorkerMetrics = class {
|
|
|
7284
7647
|
start(processes = true) {
|
|
7285
7648
|
try {
|
|
7286
7649
|
this.histogram?.reset();
|
|
7650
|
+
this.heapAfterGc = null;
|
|
7287
7651
|
this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
|
|
7288
7652
|
this.usage = process.resourceUsage();
|
|
7289
7653
|
if (this.linux && processes && !this.subtree) this.snapshot();
|
|
@@ -7306,6 +7670,7 @@ var WorkerMetrics = class {
|
|
|
7306
7670
|
loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
|
|
7307
7671
|
loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
|
|
7308
7672
|
heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
|
|
7673
|
+
heapAfterGcMb: this.heapAfterGc === null ? null : round1(this.heapAfterGc / MB),
|
|
7309
7674
|
fds: this.linux ? this.proc.fdCount(this.pid) : null
|
|
7310
7675
|
}
|
|
7311
7676
|
};
|
|
@@ -7340,6 +7705,7 @@ var WorkerMetrics = class {
|
|
|
7340
7705
|
}
|
|
7341
7706
|
dispose() {
|
|
7342
7707
|
this.histogram?.disable();
|
|
7708
|
+
this.gcObserver?.disconnect();
|
|
7343
7709
|
}
|
|
7344
7710
|
};
|
|
7345
7711
|
var PageReader = class {
|
|
@@ -7361,12 +7727,36 @@ var PageReader = class {
|
|
|
7361
7727
|
try {
|
|
7362
7728
|
const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
|
|
7363
7729
|
const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
|
|
7364
|
-
|
|
7730
|
+
const main = {
|
|
7365
7731
|
cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
|
|
7366
7732
|
heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
|
|
7367
7733
|
nodes: values.get("Nodes") ?? 0,
|
|
7368
7734
|
listeners: values.get("JSEventListeners") ?? 0
|
|
7369
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;
|
|
7370
7760
|
} catch {
|
|
7371
7761
|
return null;
|
|
7372
7762
|
}
|
|
@@ -7375,6 +7765,9 @@ var PageReader = class {
|
|
|
7375
7765
|
|
|
7376
7766
|
// src/internal/capture/resource-ledger.ts
|
|
7377
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;
|
|
7378
7771
|
var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
|
|
7379
7772
|
var CLOSE_TIMEOUT_MS = 5e3;
|
|
7380
7773
|
var PAGE_READ_CAP = 50;
|
|
@@ -7450,20 +7843,41 @@ function playwrightTestInfoSource() {
|
|
|
7450
7843
|
function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
|
|
7451
7844
|
return value === "fail" || value === "close" ? value : "report";
|
|
7452
7845
|
}
|
|
7453
|
-
function countHandles() {
|
|
7846
|
+
function countHandles(types = TRACKED_HANDLE_TYPES) {
|
|
7454
7847
|
const counts = {};
|
|
7455
7848
|
const read = process.getActiveResourcesInfo;
|
|
7456
7849
|
if (typeof read !== "function") return counts;
|
|
7457
7850
|
for (const type of read.call(process)) {
|
|
7458
|
-
if (
|
|
7851
|
+
if (types.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
|
|
7459
7852
|
}
|
|
7460
7853
|
return counts;
|
|
7461
7854
|
}
|
|
7462
|
-
async function settledHandles(start) {
|
|
7463
|
-
const end = countHandles();
|
|
7855
|
+
async function settledHandles(start, types = TRACKED_HANDLE_TYPES) {
|
|
7856
|
+
const end = countHandles(types);
|
|
7464
7857
|
if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
|
|
7465
7858
|
await new Promise((resolve11) => setTimeout(resolve11, 0));
|
|
7466
|
-
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";
|
|
7467
7881
|
}
|
|
7468
7882
|
function settleWithin(promise, ms) {
|
|
7469
7883
|
let timer;
|
|
@@ -7475,7 +7889,7 @@ function settleWithin(promise, ms) {
|
|
|
7475
7889
|
]).finally(() => clearTimeout(timer));
|
|
7476
7890
|
}
|
|
7477
7891
|
var ResourceLedger = class {
|
|
7478
|
-
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) {
|
|
7479
7893
|
this.playwright = playwright;
|
|
7480
7894
|
this.workerIndex = workerIndex;
|
|
7481
7895
|
this.leakCheck = leakCheck;
|
|
@@ -7483,6 +7897,7 @@ var ResourceLedger = class {
|
|
|
7483
7897
|
this.testInfoSource = testInfoSource;
|
|
7484
7898
|
this.now = now;
|
|
7485
7899
|
this.readers = readers;
|
|
7900
|
+
this.nodeHandles = nodeHandles;
|
|
7486
7901
|
this.nextId = 1;
|
|
7487
7902
|
this.entries = /* @__PURE__ */ new Map();
|
|
7488
7903
|
this.byObject = /* @__PURE__ */ new WeakMap();
|
|
@@ -7499,6 +7914,11 @@ var ResourceLedger = class {
|
|
|
7499
7914
|
this.listener = null;
|
|
7500
7915
|
/** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
|
|
7501
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 = [];
|
|
7502
7922
|
}
|
|
7503
7923
|
/** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
|
|
7504
7924
|
install() {
|
|
@@ -7531,13 +7951,107 @@ var ResourceLedger = class {
|
|
|
7531
7951
|
for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
|
|
7532
7952
|
if (type) this.wrapBrowserType(type);
|
|
7533
7953
|
}
|
|
7954
|
+
if (this.nodeHandles) this.guard(() => this.wrapNodeHandles());
|
|
7534
7955
|
return true;
|
|
7535
7956
|
}
|
|
7536
|
-
/** 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. */
|
|
7537
7958
|
uninstall() {
|
|
7538
7959
|
const remove = this.playwright?._instrumentation?.removeListener;
|
|
7539
7960
|
if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
|
|
7540
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 } : {} }));
|
|
7541
8055
|
}
|
|
7542
8056
|
/** Mark a page used: a locator was built on it. */
|
|
7543
8057
|
notePageUse(page) {
|
|
@@ -7547,7 +8061,7 @@ var ResourceLedger = class {
|
|
|
7547
8061
|
/** Open the census window of a test. */
|
|
7548
8062
|
testStarted(info) {
|
|
7549
8063
|
this.window = testRef2(info);
|
|
7550
|
-
this.handlesAtStart = countHandles();
|
|
8064
|
+
this.handlesAtStart = countHandles(this.countedHandleTypes());
|
|
7551
8065
|
this.openAtStart = { contexts: 0, pages: 0 };
|
|
7552
8066
|
for (const entry of this.openEntries()) {
|
|
7553
8067
|
if (entry.birth.kind === "page") this.openAtStart.pages++;
|
|
@@ -7567,9 +8081,14 @@ var ResourceLedger = class {
|
|
|
7567
8081
|
if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
|
|
7568
8082
|
const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
|
|
7569
8083
|
const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
|
|
7570
|
-
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;
|
|
7571
8089
|
const census = this.census(ref, handles, await this.readPages());
|
|
7572
8090
|
if (metrics) census.metrics = metrics;
|
|
8091
|
+
if (running) census.running = running;
|
|
7573
8092
|
if (test) {
|
|
7574
8093
|
census.openAtStart = this.openAtStart;
|
|
7575
8094
|
if (!failed) census.leftOpen = leaks.length;
|
|
@@ -7608,6 +8127,10 @@ var ResourceLedger = class {
|
|
|
7608
8127
|
this.uninstall();
|
|
7609
8128
|
return census;
|
|
7610
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
|
+
}
|
|
7611
8134
|
guard(run) {
|
|
7612
8135
|
try {
|
|
7613
8136
|
run();
|
|
@@ -7830,6 +8353,7 @@ var ResourceLedger = class {
|
|
|
7830
8353
|
open
|
|
7831
8354
|
};
|
|
7832
8355
|
if (handles) census.handles = handles;
|
|
8356
|
+
for (const close of this.closed) this.entries.delete(close.id);
|
|
7833
8357
|
this.born = [];
|
|
7834
8358
|
this.closed = [];
|
|
7835
8359
|
return census;
|
|
@@ -7868,7 +8392,8 @@ function startResourceLedger(playwright, workerIndex) {
|
|
|
7868
8392
|
process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
|
|
7869
8393
|
playwrightTestInfoSource(),
|
|
7870
8394
|
Date.now,
|
|
7871
|
-
{ metrics, pages: new PageReader() }
|
|
8395
|
+
{ metrics, pages: new PageReader() },
|
|
8396
|
+
true
|
|
7872
8397
|
);
|
|
7873
8398
|
if (!ledger.install()) {
|
|
7874
8399
|
metrics.dispose();
|
|
@@ -8027,7 +8552,15 @@ function buildResourceReport(input) {
|
|
|
8027
8552
|
if (!entry) continue;
|
|
8028
8553
|
entry.lastOpenAt = census.at;
|
|
8029
8554
|
if (open.used !== void 0) entry.used = open.used;
|
|
8030
|
-
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
|
+
}
|
|
8031
8564
|
if (open.main) entry.mainCpu.push({ at: census.at, cpuMs: open.main.cpuMs });
|
|
8032
8565
|
}
|
|
8033
8566
|
}
|
|
@@ -8042,6 +8575,7 @@ function buildResourceReport(input) {
|
|
|
8042
8575
|
}
|
|
8043
8576
|
findings.push(...groupLeaks(leaked, all));
|
|
8044
8577
|
findings.push(...pilingFindings(all));
|
|
8578
|
+
findings.push(...workerPilingFindings(byWorker));
|
|
8045
8579
|
findings.push(...handleFindings(byWorker));
|
|
8046
8580
|
findings.push(...groupIdle(idle, input.fixturesByTest ?? /* @__PURE__ */ new Map()));
|
|
8047
8581
|
findings.push(...probableFindings(input.tallies ?? []));
|
|
@@ -8179,16 +8713,39 @@ function groupIdle(idle, fixturesByTest) {
|
|
|
8179
8713
|
}
|
|
8180
8714
|
return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => b.count - a.count);
|
|
8181
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
|
+
}
|
|
8182
8742
|
function pilingFindings(all) {
|
|
8183
8743
|
const out = [];
|
|
8184
8744
|
for (const entry of all) {
|
|
8185
|
-
for (const what of ["pages", "listeners", "routes"]) {
|
|
8745
|
+
for (const what of ["pages", "listeners", "routes", "elements"]) {
|
|
8186
8746
|
const values = entry.series.map((point) => point[what]).filter((v) => typeof v === "number");
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
const from = values[0];
|
|
8190
|
-
const to = values[values.length - 1];
|
|
8191
|
-
if (!rising || to - from < 2) continue;
|
|
8747
|
+
const growth = pileUp(values, what);
|
|
8748
|
+
if (!growth) continue;
|
|
8192
8749
|
out.push({
|
|
8193
8750
|
verdict: "piling",
|
|
8194
8751
|
kind: entry.birth.kind,
|
|
@@ -8196,34 +8753,91 @@ function pilingFindings(all) {
|
|
|
8196
8753
|
site: siteOf(entry.birth),
|
|
8197
8754
|
tests: values.length,
|
|
8198
8755
|
count: 1,
|
|
8199
|
-
growth: { what, from, to, tests: values.length }
|
|
8756
|
+
growth: { what, from: Math.round(growth.from), to: Math.round(growth.to), tests: values.length }
|
|
8200
8757
|
});
|
|
8201
8758
|
}
|
|
8202
8759
|
}
|
|
8203
8760
|
return out.sort((a, b) => b.growth.to - b.growth.from - (a.growth.to - a.growth.from));
|
|
8204
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
|
+
}
|
|
8205
8811
|
function handleFindings(byWorker) {
|
|
8206
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
|
+
};
|
|
8207
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
|
+
}
|
|
8208
8832
|
const ordered = censuses.filter((census) => census.test && census.handles).sort((a, b) => a.at - b.at);
|
|
8209
8833
|
for (let i = 0; i < ordered.length - 1; i++) {
|
|
8210
8834
|
const census = ordered[i];
|
|
8211
8835
|
const next = ordered[i + 1];
|
|
8212
8836
|
for (const [type, end] of Object.entries(census.handles.end)) {
|
|
8837
|
+
if (census.running && type === "TCPServerWrap") continue;
|
|
8213
8838
|
const grew = end - (census.handles.start[type] ?? 0);
|
|
8214
8839
|
if (grew <= 0 || (next.handles.start[type] ?? 0) < end) continue;
|
|
8215
|
-
|
|
8216
|
-
const key = `${test.id}|${type}`;
|
|
8217
|
-
const group = groups.get(key) ?? {
|
|
8218
|
-
verdict: "handle",
|
|
8219
|
-
kind: "handle",
|
|
8220
|
-
where: type,
|
|
8221
|
-
tests: 1,
|
|
8222
|
-
count: 0,
|
|
8223
|
-
detail: `"${test.title ?? ""}" (${test.file})`
|
|
8224
|
-
};
|
|
8225
|
-
group.count += grew;
|
|
8226
|
-
groups.set(key, group);
|
|
8840
|
+
add2(census.test, type, grew);
|
|
8227
8841
|
}
|
|
8228
8842
|
}
|
|
8229
8843
|
}
|
|
@@ -8268,19 +8882,37 @@ var KIND_LABEL = {
|
|
|
8268
8882
|
context: "context",
|
|
8269
8883
|
page: "page",
|
|
8270
8884
|
request: "API context",
|
|
8271
|
-
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"
|
|
8272
8903
|
};
|
|
8273
8904
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
8274
8905
|
var HANDLE_LABEL = {
|
|
8275
8906
|
TCPServerWrap: "server",
|
|
8276
8907
|
FSEventWrap: "file watcher",
|
|
8277
|
-
StatWatcher: "file watcher"
|
|
8908
|
+
StatWatcher: "file watcher",
|
|
8909
|
+
ChildProcess: "process"
|
|
8278
8910
|
};
|
|
8279
8911
|
function findingLine(finding) {
|
|
8280
8912
|
const verdict = finding.verdict.padEnd(8);
|
|
8281
|
-
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];
|
|
8282
8914
|
const kind = label.padEnd(11);
|
|
8283
|
-
const parts = finding.verdict === "handle" ? [] : [finding.where];
|
|
8915
|
+
const parts = finding.verdict === "handle" ? finding.site ? [finding.site] : [] : [finding.where];
|
|
8284
8916
|
switch (finding.verdict) {
|
|
8285
8917
|
case "leaked": {
|
|
8286
8918
|
if (finding.count > 1) parts.push(`${finding.count} ${KIND_LABEL[finding.kind]}s`);
|
|
@@ -8303,11 +8935,12 @@ function findingLine(finding) {
|
|
|
8303
8935
|
break;
|
|
8304
8936
|
case "piling": {
|
|
8305
8937
|
const growth = finding.growth;
|
|
8306
|
-
|
|
8307
|
-
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);
|
|
8308
8940
|
break;
|
|
8309
8941
|
}
|
|
8310
8942
|
case "handle":
|
|
8943
|
+
if (finding.count > 1) parts.push(`${finding.count} ${label}${label.endsWith("s") ? "es" : "s"}`);
|
|
8311
8944
|
parts.push(`left running in the worker by ${finding.detail ?? "a test"}`);
|
|
8312
8945
|
break;
|
|
8313
8946
|
case "probable":
|
|
@@ -8446,6 +9079,12 @@ async function dirBytes(dir, budget) {
|
|
|
8446
9079
|
}
|
|
8447
9080
|
return bytes;
|
|
8448
9081
|
}
|
|
9082
|
+
function memoryOf(container) {
|
|
9083
|
+
const totalMemory = os8.totalmem();
|
|
9084
|
+
const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
|
|
9085
|
+
const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
|
|
9086
|
+
return { memoryBytes: totalMemory, memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null };
|
|
9087
|
+
}
|
|
8449
9088
|
var RunSampler = class {
|
|
8450
9089
|
constructor(options = {}) {
|
|
8451
9090
|
this.timer = null;
|
|
@@ -8514,6 +9153,18 @@ var RunSampler = class {
|
|
|
8514
9153
|
}
|
|
8515
9154
|
return this.profile(await this.leftoverBytes());
|
|
8516
9155
|
}
|
|
9156
|
+
/**
|
|
9157
|
+
* What a live run's memory is drawn against: the machine's memory, or its
|
|
9158
|
+
* container's limit when lower, read once, and how the run's memory is
|
|
9159
|
+
* measured so far.
|
|
9160
|
+
*/
|
|
9161
|
+
liveMemory() {
|
|
9162
|
+
if (this.capacityBytes === void 0) {
|
|
9163
|
+
const machine = memoryOf(this.linux ? this.cgroup.read() : null);
|
|
9164
|
+
this.capacityBytes = machine.memoryLimitBytes ?? machine.memoryBytes;
|
|
9165
|
+
}
|
|
9166
|
+
return { capacityBytes: this.capacityBytes || null, kind: this.memoryKind };
|
|
9167
|
+
}
|
|
8517
9168
|
/** The series read so far, each point at the time it was read. */
|
|
8518
9169
|
timedSeries() {
|
|
8519
9170
|
return {
|
|
@@ -8810,9 +9461,6 @@ var RunSampler = class {
|
|
|
8810
9461
|
if (cpuPressure === null) notMeasured.push("CPU pressure");
|
|
8811
9462
|
const container = this.linux ? this.cgroup.read() : null;
|
|
8812
9463
|
const startContainer = this.containerStart;
|
|
8813
|
-
const totalMemory = os8.totalmem();
|
|
8814
|
-
const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
|
|
8815
|
-
const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
|
|
8816
9464
|
const raisedPeak = container?.memoryPeakBytes != null && startContainer?.memoryPeakBytes != null && container.memoryPeakBytes > startContainer.memoryPeakBytes ? container.memoryPeakBytes : null;
|
|
8817
9465
|
const delta = (to, from) => to != null && from != null ? Math.max(0, to - from) : null;
|
|
8818
9466
|
return {
|
|
@@ -8820,8 +9468,7 @@ var RunSampler = class {
|
|
|
8820
9468
|
wallMs,
|
|
8821
9469
|
machine: {
|
|
8822
9470
|
cores: typeof os8.availableParallelism === "function" ? os8.availableParallelism() : os8.cpus().length,
|
|
8823
|
-
|
|
8824
|
-
memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null,
|
|
9471
|
+
...memoryOf(container),
|
|
8825
9472
|
cpuQuotaCores: container?.cpuQuotaCores ?? null
|
|
8826
9473
|
},
|
|
8827
9474
|
cpu: {
|
|
@@ -9093,7 +9740,7 @@ function openPagesByWorker(censuses) {
|
|
|
9093
9740
|
}
|
|
9094
9741
|
return out;
|
|
9095
9742
|
}
|
|
9096
|
-
function pagesOverTime(censuses) {
|
|
9743
|
+
function pagesOverTime(censuses, live = false) {
|
|
9097
9744
|
const byWorker = /* @__PURE__ */ new Map();
|
|
9098
9745
|
for (const census of censuses) {
|
|
9099
9746
|
const list = byWorker.get(census.worker) ?? [];
|
|
@@ -9126,7 +9773,7 @@ function pagesOverTime(censuses) {
|
|
|
9126
9773
|
if (last && last[0] === at) last[1] = open;
|
|
9127
9774
|
else events.push([at, open]);
|
|
9128
9775
|
}
|
|
9129
|
-
if (open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
|
|
9776
|
+
if (!live && open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
|
|
9130
9777
|
out.push({ worker, events });
|
|
9131
9778
|
}
|
|
9132
9779
|
return out;
|
|
@@ -9158,7 +9805,7 @@ function resourceTimeline(samples, censuses) {
|
|
|
9158
9805
|
let startedAt = hasSamples ? samples.startedAt : Infinity;
|
|
9159
9806
|
for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
|
|
9160
9807
|
const shift = hasSamples ? samples.startedAt - startedAt : 0;
|
|
9161
|
-
const
|
|
9808
|
+
const relative12 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
|
|
9162
9809
|
const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
|
|
9163
9810
|
const perWorker = Math.max(
|
|
9164
9811
|
60,
|
|
@@ -9166,17 +9813,17 @@ function resourceTimeline(samples, censuses) {
|
|
|
9166
9813
|
);
|
|
9167
9814
|
return {
|
|
9168
9815
|
startedAt,
|
|
9169
|
-
cpuPct: hasSamples ? downsamplePoints(
|
|
9170
|
-
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") : [],
|
|
9171
9818
|
pages: workers.map(({ worker, events }) => {
|
|
9172
|
-
const points =
|
|
9819
|
+
const points = relative12(events, -startedAt);
|
|
9173
9820
|
const last = points.pop();
|
|
9174
9821
|
return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
|
|
9175
9822
|
}),
|
|
9176
9823
|
workers: trees.map((tree) => ({
|
|
9177
9824
|
worker: tree.worker,
|
|
9178
|
-
cpuCores: downsamplePoints(
|
|
9179
|
-
memoryBytes: downsamplePoints(
|
|
9825
|
+
cpuCores: downsamplePoints(relative12(tree.cpuCores, shift), perWorker, "mean"),
|
|
9826
|
+
memoryBytes: downsamplePoints(relative12(tree.memoryBytes, shift), perWorker, "max")
|
|
9180
9827
|
}))
|
|
9181
9828
|
};
|
|
9182
9829
|
}
|
|
@@ -9195,6 +9842,144 @@ function resourceReportWire(input) {
|
|
|
9195
9842
|
};
|
|
9196
9843
|
}
|
|
9197
9844
|
|
|
9845
|
+
// src/internal/streaming/live-resources.ts
|
|
9846
|
+
var LIVE_INTERVAL_MS = 5e3;
|
|
9847
|
+
function since(points, cursor, cap, merge) {
|
|
9848
|
+
const fresh = cursor === void 0 ? points : points.filter(([at]) => at > cursor);
|
|
9849
|
+
return downsamplePoints(fresh, cap, merge);
|
|
9850
|
+
}
|
|
9851
|
+
var LiveResources = class {
|
|
9852
|
+
constructor(stream, httpClient, options, logger = new Logger()) {
|
|
9853
|
+
this.stream = stream;
|
|
9854
|
+
this.httpClient = httpClient;
|
|
9855
|
+
this.options = options;
|
|
9856
|
+
this.logger = logger;
|
|
9857
|
+
this.timer = null;
|
|
9858
|
+
this.inFlight = null;
|
|
9859
|
+
this.stopped = false;
|
|
9860
|
+
/** Whether the next post must carry every point: the first one, and after the dashboard lost them. */
|
|
9861
|
+
this.reset = true;
|
|
9862
|
+
/** The time of the last point the dashboard took, per series. */
|
|
9863
|
+
this.sent = /* @__PURE__ */ new Map();
|
|
9864
|
+
/** How many censuses the open pages last posted came from. */
|
|
9865
|
+
this.censusCount = 0;
|
|
9866
|
+
}
|
|
9867
|
+
/** Post on a timer that never keeps the process alive. */
|
|
9868
|
+
start() {
|
|
9869
|
+
if (this.timer || this.stopped) return;
|
|
9870
|
+
this.timer = setInterval(() => this.tick(), this.options.intervalMs ?? LIVE_INTERVAL_MS);
|
|
9871
|
+
this.timer.unref?.();
|
|
9872
|
+
}
|
|
9873
|
+
/** Post nothing more, and wait for a post in flight. */
|
|
9874
|
+
async stop() {
|
|
9875
|
+
this.stopped = true;
|
|
9876
|
+
if (this.timer) clearInterval(this.timer);
|
|
9877
|
+
this.timer = null;
|
|
9878
|
+
await this.inFlight;
|
|
9879
|
+
}
|
|
9880
|
+
/** Post now, when the run is watched and no post is in flight: the dashboard says someone just opened it. */
|
|
9881
|
+
tick() {
|
|
9882
|
+
if (this.stopped || this.inFlight) return null;
|
|
9883
|
+
const { enabled, runId, token } = this.stream;
|
|
9884
|
+
if (!enabled || runId === null || !token || this.stream.isWatched !== true) return null;
|
|
9885
|
+
let draft;
|
|
9886
|
+
try {
|
|
9887
|
+
draft = this.draft();
|
|
9888
|
+
} catch (error) {
|
|
9889
|
+
this.logger.debug(`Live resources not read: ${errorMessage(error)}`);
|
|
9890
|
+
return null;
|
|
9891
|
+
}
|
|
9892
|
+
if (!draft) return null;
|
|
9893
|
+
this.inFlight = this.send(runId, token, draft).finally(() => {
|
|
9894
|
+
this.inFlight = null;
|
|
9895
|
+
});
|
|
9896
|
+
return this.inFlight;
|
|
9897
|
+
}
|
|
9898
|
+
/** What is new since the last post the dashboard took; null when nothing is. */
|
|
9899
|
+
draft() {
|
|
9900
|
+
const samples = this.options.sampler?.timedSeries() ?? null;
|
|
9901
|
+
const startedAt = samples?.startedAt || this.options.runStartedAt;
|
|
9902
|
+
const censuses = this.options.censuses();
|
|
9903
|
+
const cursors = this.reset ? /* @__PURE__ */ new Map() : this.sent;
|
|
9904
|
+
const sent2 = new Map(cursors);
|
|
9905
|
+
let fresh = false;
|
|
9906
|
+
const take = (key, points, cap, merge) => {
|
|
9907
|
+
const out = since(points, cursors.get(key), cap, merge);
|
|
9908
|
+
const last = points[points.length - 1];
|
|
9909
|
+
if (out.length > 0 && last) {
|
|
9910
|
+
sent2.set(key, last[0]);
|
|
9911
|
+
fresh = true;
|
|
9912
|
+
}
|
|
9913
|
+
return out;
|
|
9914
|
+
};
|
|
9915
|
+
const timeline = { startedAt, cpuPct: [], memoryBytes: [], pages: [], workers: [] };
|
|
9916
|
+
if (samples) {
|
|
9917
|
+
timeline.cpuPct = take("cpu", samples.cpuPct, TIMELINE_SERIES_CAP, "mean");
|
|
9918
|
+
timeline.memoryBytes = take("memory", samples.memoryBytes, TIMELINE_SERIES_CAP, "max");
|
|
9919
|
+
for (const tree of workerTreesByIndex(samples, censuses)) {
|
|
9920
|
+
const cpuCores = take(`cores:${tree.worker}`, tree.cpuCores, TIMELINE_WORKER_POINTS_CAP, "mean");
|
|
9921
|
+
const memoryBytes = take(`bytes:${tree.worker}`, tree.memoryBytes, TIMELINE_WORKER_POINTS_CAP, "max");
|
|
9922
|
+
if (cpuCores.length > 0 || memoryBytes.length > 0) {
|
|
9923
|
+
timeline.workers.push({ worker: tree.worker, cpuCores, memoryBytes });
|
|
9924
|
+
}
|
|
9925
|
+
}
|
|
9926
|
+
}
|
|
9927
|
+
if (this.reset || censuses.length !== this.censusCount) {
|
|
9928
|
+
for (const { worker, events } of pagesOverTime(censuses, true)) {
|
|
9929
|
+
const points = events.map(([at, open]) => [at - startedAt, open]);
|
|
9930
|
+
const last = points.pop();
|
|
9931
|
+
timeline.pages.push({
|
|
9932
|
+
worker,
|
|
9933
|
+
points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last]
|
|
9934
|
+
});
|
|
9935
|
+
}
|
|
9936
|
+
fresh ||= timeline.pages.length > 0;
|
|
9937
|
+
}
|
|
9938
|
+
if (!fresh) return null;
|
|
9939
|
+
const memory = this.options.sampler?.liveMemory() ?? { capacityBytes: null, kind: null };
|
|
9940
|
+
return {
|
|
9941
|
+
post: {
|
|
9942
|
+
shardIndex: this.options.shardIndex,
|
|
9943
|
+
reset: this.reset,
|
|
9944
|
+
timeline,
|
|
9945
|
+
memoryCapacityBytes: memory.capacityBytes,
|
|
9946
|
+
memoryKind: memory.kind
|
|
9947
|
+
},
|
|
9948
|
+
sent: sent2,
|
|
9949
|
+
censusCount: censuses.length
|
|
9950
|
+
};
|
|
9951
|
+
}
|
|
9952
|
+
async send(runId, token, draft) {
|
|
9953
|
+
try {
|
|
9954
|
+
const answer = await this.httpClient.postJSON(
|
|
9955
|
+
`/api/test-runs/${runId}/live-resources`,
|
|
9956
|
+
{ streamToken: token, ...draft.post },
|
|
9957
|
+
this.stream.auth
|
|
9958
|
+
);
|
|
9959
|
+
if (answer?.declined) {
|
|
9960
|
+
this.logger.debug("The project declined resources: live resources are not sent.");
|
|
9961
|
+
this.stopped = true;
|
|
9962
|
+
return;
|
|
9963
|
+
}
|
|
9964
|
+
this.stream.noteWatched(answer);
|
|
9965
|
+
if (answer?.resync) {
|
|
9966
|
+
this.reset = true;
|
|
9967
|
+
return;
|
|
9968
|
+
}
|
|
9969
|
+
this.sent = draft.sent;
|
|
9970
|
+
this.censusCount = draft.censusCount;
|
|
9971
|
+
this.reset = false;
|
|
9972
|
+
} catch (error) {
|
|
9973
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
9974
|
+
this.logger.debug("The dashboard takes no live resources.");
|
|
9975
|
+
this.stopped = true;
|
|
9976
|
+
return;
|
|
9977
|
+
}
|
|
9978
|
+
this.logger.debug(`Live resources not sent: ${errorMessage(error)}`);
|
|
9979
|
+
}
|
|
9980
|
+
}
|
|
9981
|
+
};
|
|
9982
|
+
|
|
9198
9983
|
// ../core/src/describe-failure.ts
|
|
9199
9984
|
var HEADLINE_MAX_CHARS = 120;
|
|
9200
9985
|
var VALUE_MAX_CHARS = 40;
|
|
@@ -9592,8 +10377,8 @@ function detailOf(parsed2, headline) {
|
|
|
9592
10377
|
case "assertion-timeout": {
|
|
9593
10378
|
const received = parsed2.received;
|
|
9594
10379
|
if (received && !/element\(s\) not found/.test(received)) {
|
|
9595
|
-
const
|
|
9596
|
-
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);
|
|
9597
10382
|
}
|
|
9598
10383
|
const state = parsed2.lastStateLine;
|
|
9599
10384
|
if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
|
|
@@ -9724,10 +10509,14 @@ function isServerGranularity(value) {
|
|
|
9724
10509
|
return typeof value === "string" && SERVER_VALUES.has(value);
|
|
9725
10510
|
}
|
|
9726
10511
|
function coverageSettings(options) {
|
|
9727
|
-
if (options.collectPerformanceMetrics === false) return { browser: "off", server: "off" };
|
|
10512
|
+
if (options.collectPerformanceMetrics === false) return { browser: "off", server: "off", css: "off" };
|
|
9728
10513
|
const browser = options.coverage?.browser === "functions" ? "functions" : options.coverage?.browser === "files" || options.captureCodeReach === true ? "files" : "off";
|
|
9729
10514
|
const server = isServerGranularity(options.coverage?.server) ? options.coverage.server : "off";
|
|
9730
|
-
|
|
10515
|
+
const css = options.coverage?.css === "rules" ? "rules" : "off";
|
|
10516
|
+
return { browser, server, css };
|
|
10517
|
+
}
|
|
10518
|
+
function cssCoverageInWorker() {
|
|
10519
|
+
return process.env[PIWI_ENV_KEYS.coverageCss] === "rules";
|
|
9731
10520
|
}
|
|
9732
10521
|
function browserCoverageInWorker() {
|
|
9733
10522
|
const browser = process.env[PIWI_ENV_KEYS.coverageBrowser];
|
|
@@ -9742,7 +10531,7 @@ function browserFunctionCoverage(files) {
|
|
|
9742
10531
|
files: [...files].map(([path34, functions]) => ({ path: path34, runtime: "browser", functions }))
|
|
9743
10532
|
});
|
|
9744
10533
|
}
|
|
9745
|
-
function newKnownFunctions(known,
|
|
10534
|
+
function newKnownFunctions(known, sent2) {
|
|
9746
10535
|
const parts = [];
|
|
9747
10536
|
let files = [];
|
|
9748
10537
|
let budget = MAX_COVERAGE_RANGES;
|
|
@@ -9754,10 +10543,10 @@ function newKnownFunctions(known, sent) {
|
|
|
9754
10543
|
for (const [raw, ranges] of known) {
|
|
9755
10544
|
const path34 = normalizeCoveragePath(raw);
|
|
9756
10545
|
if (path34 === null) continue;
|
|
9757
|
-
let named =
|
|
10546
|
+
let named = sent2.get(path34);
|
|
9758
10547
|
let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
|
|
9759
10548
|
if (fresh.length === 0) continue;
|
|
9760
|
-
if (!named)
|
|
10549
|
+
if (!named) sent2.set(path34, named = /* @__PURE__ */ new Set());
|
|
9761
10550
|
for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
|
|
9762
10551
|
while (fresh.length > 0) {
|
|
9763
10552
|
if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
|
|
@@ -9775,7 +10564,7 @@ function serverCoverageInWorker() {
|
|
|
9775
10564
|
}
|
|
9776
10565
|
|
|
9777
10566
|
// src/internal/capture/code-reach.ts
|
|
9778
|
-
var
|
|
10567
|
+
var crypto6 = __toESM(require("crypto"));
|
|
9779
10568
|
var fs23 = __toESM(require("fs"));
|
|
9780
10569
|
var path23 = __toESM(require("path"));
|
|
9781
10570
|
var import_node_child_process3 = require("child_process");
|
|
@@ -9847,10 +10636,10 @@ function joinRoot(root, source) {
|
|
|
9847
10636
|
function decodeLines(raw) {
|
|
9848
10637
|
const sources = [];
|
|
9849
10638
|
const decoded = [];
|
|
9850
|
-
const
|
|
10639
|
+
const add2 = (map, lineOffset, columnOffset) => {
|
|
9851
10640
|
if (map.sections) {
|
|
9852
10641
|
for (const section of map.sections) {
|
|
9853
|
-
if (!
|
|
10642
|
+
if (!add2(section.map, section.offset.line, section.offset.column)) return false;
|
|
9854
10643
|
}
|
|
9855
10644
|
return true;
|
|
9856
10645
|
}
|
|
@@ -9871,7 +10660,7 @@ function decodeLines(raw) {
|
|
|
9871
10660
|
});
|
|
9872
10661
|
return true;
|
|
9873
10662
|
};
|
|
9874
|
-
return
|
|
10663
|
+
return add2(raw, 0, 0) ? { sources, lines: decoded } : null;
|
|
9875
10664
|
}
|
|
9876
10665
|
function compactSegments(lines) {
|
|
9877
10666
|
let total = 0;
|
|
@@ -10047,7 +10836,7 @@ function functionLines(entry, map, text, all) {
|
|
|
10047
10836
|
const read = textOf(entry, text);
|
|
10048
10837
|
const ranges = executedFunctionRanges(entry, read?.length, all);
|
|
10049
10838
|
if (!ranges.length || !read) return out;
|
|
10050
|
-
const
|
|
10839
|
+
const add2 = (source, range) => {
|
|
10051
10840
|
const list = out.get(source);
|
|
10052
10841
|
if (list) list.push(range);
|
|
10053
10842
|
else out.set(source, [range]);
|
|
@@ -10057,7 +10846,7 @@ function functionLines(entry, map, text, all) {
|
|
|
10057
10846
|
const start = offsetToPosition(starts, range.start);
|
|
10058
10847
|
if (!map) {
|
|
10059
10848
|
const last = offsetToPosition(starts, Math.max(range.start, range.end - 1));
|
|
10060
|
-
|
|
10849
|
+
add2(-1, [start.line + 1, last.line + 1]);
|
|
10061
10850
|
continue;
|
|
10062
10851
|
}
|
|
10063
10852
|
const at = map.originalAt(start.line, start.column);
|
|
@@ -10067,7 +10856,7 @@ function functionLines(entry, map, text, all) {
|
|
|
10067
10856
|
if (first === null) continue;
|
|
10068
10857
|
const header = first === at.line || first === at.line + 1 ? at.line : first;
|
|
10069
10858
|
const lines = map.originalLinesBetween(at.source, start, end, header);
|
|
10070
|
-
if (lines)
|
|
10859
|
+
if (lines) add2(at.source, [header + 1, lines[1] + 1]);
|
|
10071
10860
|
}
|
|
10072
10861
|
return out;
|
|
10073
10862
|
}
|
|
@@ -10165,9 +10954,9 @@ function resolveSourcePath(source, roots) {
|
|
|
10165
10954
|
const normalized = normalizeSourcePath(source);
|
|
10166
10955
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
10167
10956
|
if (path23.isAbsolute(normalized) && isFile(normalized)) return path23.resolve(normalized);
|
|
10168
|
-
const
|
|
10957
|
+
const relative12 = normalized.replace(/^\/+/, "");
|
|
10169
10958
|
for (const root of roots) {
|
|
10170
|
-
const candidate = path23.resolve(root,
|
|
10959
|
+
const candidate = path23.resolve(root, relative12);
|
|
10171
10960
|
if (isFile(candidate)) return candidate;
|
|
10172
10961
|
}
|
|
10173
10962
|
return null;
|
|
@@ -10202,7 +10991,7 @@ function relativeToRepo(file, repoRoot) {
|
|
|
10202
10991
|
return path23.relative(repoRoot, file).split(path23.sep).join("/");
|
|
10203
10992
|
}
|
|
10204
10993
|
function scriptVersion(entry) {
|
|
10205
|
-
return entry.hash ??
|
|
10994
|
+
return entry.hash ?? crypto6.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
10206
10995
|
}
|
|
10207
10996
|
async function loadMap(entry, fetchMap) {
|
|
10208
10997
|
const ref = entry.source ? sourceMappingUrl(entry.source) : null;
|
|
@@ -10261,7 +11050,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
10261
11050
|
if (listed) listed.push(...functions);
|
|
10262
11051
|
else into.set(path34, [...functions]);
|
|
10263
11052
|
};
|
|
10264
|
-
const
|
|
11053
|
+
const add2 = (file, functions) => addTo(files, file, functions);
|
|
10265
11054
|
const bases = viteBases(entries);
|
|
10266
11055
|
const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
|
|
10267
11056
|
const cached = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
|
|
@@ -10288,14 +11077,14 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
|
|
|
10288
11077
|
const text = textOf2(covered, resolution);
|
|
10289
11078
|
if (resolution.lookup) {
|
|
10290
11079
|
const reached = reachedFunctionLines(covered, resolution.lookup, text);
|
|
10291
|
-
if (reached.size > 0)
|
|
11080
|
+
if (reached.size > 0) add2(resolution.file, [...reached.values()].flat());
|
|
10292
11081
|
} else if (scriptRan(covered, text)) {
|
|
10293
|
-
|
|
11082
|
+
add2(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
|
|
10294
11083
|
}
|
|
10295
11084
|
} else if (resolution.kind === "map") {
|
|
10296
11085
|
for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf2(covered, resolution))) {
|
|
10297
11086
|
const file = mapFile(resolution, index);
|
|
10298
|
-
if (file)
|
|
11087
|
+
if (file) add2(file, functions);
|
|
10299
11088
|
}
|
|
10300
11089
|
}
|
|
10301
11090
|
if (!withKnown || resolution.kind === "none") continue;
|
|
@@ -10374,10 +11163,13 @@ async function markCurrentDocument(page, cdp) {
|
|
|
10374
11163
|
}
|
|
10375
11164
|
function onPaused(page, capture, event) {
|
|
10376
11165
|
const leaving = event.reason === "EventListener" && event.data?.eventName === "listener:beforeunload" && (event.callFrames?.[0]?.functionName ?? MARK_FUNCTION) === MARK_FUNCTION;
|
|
10377
|
-
const read = leaving ?
|
|
11166
|
+
const read = leaving ? Promise.all([
|
|
11167
|
+
capture.js ? readCoverage(page, capture.cdp) : Promise.resolve([]),
|
|
11168
|
+
capture.css ? readCssUsage(page, capture.cdp, capture.css) : Promise.resolve()
|
|
11169
|
+
]) : null;
|
|
10378
11170
|
if (read) {
|
|
10379
11171
|
const merging = read.then(
|
|
10380
|
-
(coverage) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
|
|
11172
|
+
([coverage]) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
|
|
10381
11173
|
);
|
|
10382
11174
|
capture.reading.add(merging);
|
|
10383
11175
|
void merging.finally(() => capture.reading.delete(merging));
|
|
@@ -10391,7 +11183,7 @@ function versionKey(url, hash) {
|
|
|
10391
11183
|
return `${hash} ${url}`;
|
|
10392
11184
|
}
|
|
10393
11185
|
function onScriptParsed(page, capture, event) {
|
|
10394
|
-
if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
|
|
11186
|
+
if (!capture.js || !event.url || REPLAYED_SCRIPT.test(event.url)) return;
|
|
10395
11187
|
capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash, pageUrl: capture.pageUrl });
|
|
10396
11188
|
if (NEVER_REACHED.test(event.url)) return;
|
|
10397
11189
|
const key = versionKey(event.url, event.hash);
|
|
@@ -10420,6 +11212,53 @@ function readCoverage(page, cdp) {
|
|
|
10420
11212
|
);
|
|
10421
11213
|
return Promise.race([taken, late]).finally(() => clearTimeout(timer));
|
|
10422
11214
|
}
|
|
11215
|
+
function onStyleSheetAdded(page, cdp, css, pageUrl, header) {
|
|
11216
|
+
if (header.isConstructed) return;
|
|
11217
|
+
const sheet = {
|
|
11218
|
+
sourceURL: header.sourceURL,
|
|
11219
|
+
...header.sourceMapURL ? { sourceMapURL: header.sourceMapURL } : {},
|
|
11220
|
+
isInline: header.isInline,
|
|
11221
|
+
startLine: header.startLine,
|
|
11222
|
+
pageUrl,
|
|
11223
|
+
used: /* @__PURE__ */ new Map()
|
|
11224
|
+
};
|
|
11225
|
+
css.sheets.set(header.styleSheetId, sheet);
|
|
11226
|
+
const reading = (async () => {
|
|
11227
|
+
try {
|
|
11228
|
+
if (header.ownerNode !== void 0 && !/\.css(?:[?#]|$)/i.test(header.sourceURL)) {
|
|
11229
|
+
const { node } = await internalCall(
|
|
11230
|
+
page,
|
|
11231
|
+
() => cdp.send("DOM.describeNode", { backendNodeId: header.ownerNode })
|
|
11232
|
+
);
|
|
11233
|
+
const attributes = node.attributes ?? [];
|
|
11234
|
+
const at = attributes.indexOf("data-vite-dev-id");
|
|
11235
|
+
if (at >= 0 && typeof attributes[at + 1] === "string") sheet.devId = attributes[at + 1];
|
|
11236
|
+
}
|
|
11237
|
+
if (/\/node_modules\//.test(sheet.devId ?? header.sourceURL)) return;
|
|
11238
|
+
const { text } = await internalCall(
|
|
11239
|
+
page,
|
|
11240
|
+
() => cdp.send("CSS.getStyleSheetText", { styleSheetId: header.styleSheetId })
|
|
11241
|
+
);
|
|
11242
|
+
sheet.text = text;
|
|
11243
|
+
} catch {
|
|
11244
|
+
}
|
|
11245
|
+
})();
|
|
11246
|
+
css.reading.add(reading);
|
|
11247
|
+
void reading.finally(() => css.reading.delete(reading));
|
|
11248
|
+
}
|
|
11249
|
+
async function readCssUsage(page, cdp, css) {
|
|
11250
|
+
try {
|
|
11251
|
+
const { coverage } = await internalCall(page, () => cdp.send("CSS.takeCoverageDelta"));
|
|
11252
|
+
for (const rule of coverage) {
|
|
11253
|
+
if (!rule.used) continue;
|
|
11254
|
+
css.sheets.get(rule.styleSheetId)?.used.set(`${rule.startOffset}:${rule.endOffset}`, {
|
|
11255
|
+
startOffset: rule.startOffset,
|
|
11256
|
+
endOffset: rule.endOffset
|
|
11257
|
+
});
|
|
11258
|
+
}
|
|
11259
|
+
} catch {
|
|
11260
|
+
}
|
|
11261
|
+
}
|
|
10423
11262
|
function mergeCoverageRead(ran, scripts, read, keepIdle = true) {
|
|
10424
11263
|
for (const covered of read) {
|
|
10425
11264
|
const script = scripts.get(covered.scriptId);
|
|
@@ -10454,11 +11293,15 @@ async function detach(page, cdp) {
|
|
|
10454
11293
|
}
|
|
10455
11294
|
async function startCodeReach(page) {
|
|
10456
11295
|
if (!isChromiumPage(page) || captures.has(page)) return false;
|
|
11296
|
+
const withCss = cssCoverageInWorker();
|
|
11297
|
+
const js = browserCoverageInWorker() || !withCss;
|
|
10457
11298
|
let cdp = null;
|
|
10458
11299
|
try {
|
|
10459
11300
|
cdp = await internalCall(page, () => page.context().newCDPSession(page));
|
|
10460
11301
|
const capture = {
|
|
10461
11302
|
cdp,
|
|
11303
|
+
js,
|
|
11304
|
+
css: withCss ? { sheets: /* @__PURE__ */ new Map(), reading: /* @__PURE__ */ new Set() } : null,
|
|
10462
11305
|
scripts: /* @__PURE__ */ new Map(),
|
|
10463
11306
|
sources: /* @__PURE__ */ new Map(),
|
|
10464
11307
|
ran: /* @__PURE__ */ new Map(),
|
|
@@ -10468,6 +11311,9 @@ async function startCodeReach(page) {
|
|
|
10468
11311
|
};
|
|
10469
11312
|
const marks = marksOf(page);
|
|
10470
11313
|
cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
|
|
11314
|
+
const css = capture.css;
|
|
11315
|
+
if (css)
|
|
11316
|
+
cdp.on("CSS.styleSheetAdded", (event) => onStyleSheetAdded(page, cdp, css, capture.pageUrl, event.header));
|
|
10471
11317
|
cdp.on("Debugger.paused", (event) => onPaused(page, capture, event));
|
|
10472
11318
|
cdp.on("Page.frameNavigated", (event) => {
|
|
10473
11319
|
if (event.frame.parentId) return;
|
|
@@ -10478,8 +11324,17 @@ async function startCodeReach(page) {
|
|
|
10478
11324
|
page,
|
|
10479
11325
|
() => Promise.all([
|
|
10480
11326
|
capture.cdp.send("Debugger.enable"),
|
|
10481
|
-
|
|
10482
|
-
|
|
11327
|
+
...js ? [
|
|
11328
|
+
capture.cdp.send("Profiler.enable"),
|
|
11329
|
+
capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
|
|
11330
|
+
] : [],
|
|
11331
|
+
// The style sheets' rule usage records nothing without the DOM and CSS domains. A failure there leaves the
|
|
11332
|
+
// scripts' coverage running.
|
|
11333
|
+
...css ? [
|
|
11334
|
+
capture.cdp.send("DOM.enable").then(() => capture.cdp.send("CSS.enable")).then(() => capture.cdp.send("CSS.startRuleUsageTracking")).catch(() => {
|
|
11335
|
+
capture.css = null;
|
|
11336
|
+
})
|
|
11337
|
+
] : [],
|
|
10483
11338
|
capture.cdp.send("Page.enable"),
|
|
10484
11339
|
capture.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: MARK_SOURCE, worldName: MARK_WORLD }),
|
|
10485
11340
|
capture.cdp.send("DOMDebugger.setEventListenerBreakpoint", { eventName: "beforeunload" })
|
|
@@ -10493,18 +11348,26 @@ async function startCodeReach(page) {
|
|
|
10493
11348
|
return false;
|
|
10494
11349
|
}
|
|
10495
11350
|
}
|
|
10496
|
-
async function
|
|
11351
|
+
async function stopPageCoverage(page) {
|
|
10497
11352
|
const capture = captures.get(page);
|
|
10498
|
-
if (!capture) return null;
|
|
11353
|
+
if (!capture) return { scripts: null, sheets: [] };
|
|
10499
11354
|
captures.delete(page);
|
|
10500
11355
|
const { cdp } = capture;
|
|
10501
|
-
|
|
11356
|
+
const sheets = async () => {
|
|
11357
|
+
if (!capture.css) return [];
|
|
11358
|
+
await readCssUsage(page, cdp, capture.css);
|
|
11359
|
+
await Promise.all(capture.css.reading);
|
|
11360
|
+
await internalCall(page, () => cdp.send("CSS.stopRuleUsageTracking")).catch(() => {
|
|
11361
|
+
});
|
|
11362
|
+
return [...capture.css.sheets.values()].filter((sheet) => sheet.used.size > 0 || sheet.text !== void 0);
|
|
11363
|
+
};
|
|
11364
|
+
const scripts = async () => {
|
|
11365
|
+
if (!capture.js) return null;
|
|
10502
11366
|
const last = await internalCall(page, async () => {
|
|
10503
11367
|
const taken = await cdp.send("Profiler.takePreciseCoverage");
|
|
10504
11368
|
await cdp.send("Profiler.stopPreciseCoverage");
|
|
10505
11369
|
return taken.result;
|
|
10506
11370
|
});
|
|
10507
|
-
await Promise.all(capture.reading);
|
|
10508
11371
|
mergeCoverageRead(capture.ran, capture.scripts, last, capture.keepIdle);
|
|
10509
11372
|
const entries = [];
|
|
10510
11373
|
for (const [key, { script, functions }] of capture.ran) {
|
|
@@ -10514,8 +11377,11 @@ async function stopCodeReach(page) {
|
|
|
10514
11377
|
entries.push(entry);
|
|
10515
11378
|
}
|
|
10516
11379
|
return entries;
|
|
10517
|
-
}
|
|
10518
|
-
|
|
11380
|
+
};
|
|
11381
|
+
try {
|
|
11382
|
+
await Promise.all(capture.reading);
|
|
11383
|
+
const [read, styles] = await Promise.all([scripts().catch(() => null), sheets().catch(() => [])]);
|
|
11384
|
+
return { scripts: read, sheets: styles };
|
|
10519
11385
|
} finally {
|
|
10520
11386
|
await detach(page, cdp);
|
|
10521
11387
|
}
|
|
@@ -10523,11 +11389,11 @@ async function stopCodeReach(page) {
|
|
|
10523
11389
|
|
|
10524
11390
|
// src/internal/collect/blob-ids.ts
|
|
10525
11391
|
var import_node_child_process4 = require("child_process");
|
|
10526
|
-
var
|
|
11392
|
+
var crypto7 = __toESM(require("crypto"));
|
|
10527
11393
|
var fs24 = __toESM(require("fs"));
|
|
10528
11394
|
var path24 = __toESM(require("path"));
|
|
10529
11395
|
function gitBlobId(content) {
|
|
10530
|
-
return
|
|
11396
|
+
return crypto7.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
|
|
10531
11397
|
}
|
|
10532
11398
|
function namesLines(file) {
|
|
10533
11399
|
return Boolean(file.functions?.length || file.lines?.length || file.executable?.length || file.allFunctions?.length);
|
|
@@ -10632,7 +11498,7 @@ var SETTLE_WAIT_MS = 15e3;
|
|
|
10632
11498
|
var RUN_COVERAGE_FILES_PER_REQUEST = 5e3;
|
|
10633
11499
|
var RUN_COVERAGE_BYTES_PER_REQUEST = 8 * 1024 * 1024;
|
|
10634
11500
|
var INCOMPLETE = { files: [], incomplete: true };
|
|
10635
|
-
function composeTestCoverage(server, browserFiles, settings) {
|
|
11501
|
+
function composeTestCoverage(server, browserFiles, settings, cssFiles) {
|
|
10636
11502
|
const serverCapture = settings.server !== "off" && server ? reduceCoverage(
|
|
10637
11503
|
{
|
|
10638
11504
|
v: 1,
|
|
@@ -10650,13 +11516,25 @@ function composeTestCoverage(server, browserFiles, settings) {
|
|
|
10650
11516
|
},
|
|
10651
11517
|
settings.browser
|
|
10652
11518
|
) : null;
|
|
10653
|
-
const
|
|
11519
|
+
const wholeFiles = new Set(
|
|
11520
|
+
settings.browser === "functions" ? [] : (browserFiles ?? []).map((file) => typeof file === "string" ? file : file.path)
|
|
11521
|
+
);
|
|
11522
|
+
const cssCapture = settings.css === "rules" && cssFiles?.length ? reduceCoverage(
|
|
11523
|
+
{
|
|
11524
|
+
v: 1,
|
|
11525
|
+
files: cssFiles.map(
|
|
11526
|
+
(file) => wholeFiles.has(file.path) ? { path: file.path, runtime: "browser" } : { ...file, runtime: "browser" }
|
|
11527
|
+
)
|
|
11528
|
+
},
|
|
11529
|
+
"functions"
|
|
11530
|
+
) : null;
|
|
11531
|
+
const merged = mergeCoverage(serverCapture, mergeCoverage(browserCapture, cssCapture));
|
|
10654
11532
|
return merged ? sanitizeCoverage(merged) : null;
|
|
10655
11533
|
}
|
|
10656
|
-
function addRanges(into,
|
|
11534
|
+
function addRanges(into, filePath3, ranges) {
|
|
10657
11535
|
if (ranges.length === 0) return;
|
|
10658
|
-
let known = into.get(
|
|
10659
|
-
if (!known) into.set(
|
|
11536
|
+
let known = into.get(filePath3);
|
|
11537
|
+
if (!known) into.set(filePath3, known = /* @__PURE__ */ new Map());
|
|
10660
11538
|
for (const range of ranges) known.set(`${range[0]}:${range[1]}`, range);
|
|
10661
11539
|
}
|
|
10662
11540
|
function chunkRunCoverage(files) {
|
|
@@ -10710,7 +11588,7 @@ var CoverageCollection = class {
|
|
|
10710
11588
|
this.browserKnown = /* @__PURE__ */ new Map();
|
|
10711
11589
|
this.logger = options.logger ?? new Logger();
|
|
10712
11590
|
this.waitMs = options.waitMs ?? TEST_COVERAGE_WAIT_MS;
|
|
10713
|
-
this.browserBlobs = options.settings.browser === "functions" ? new BlobIds(() => repositoryRoot(options.configDir)) : null;
|
|
11591
|
+
this.browserBlobs = options.settings.browser === "functions" || options.settings.css === "rules" ? new BlobIds(() => repositoryRoot(options.configDir)) : null;
|
|
10714
11592
|
const granularity = options.settings.server;
|
|
10715
11593
|
this.collector = granularity === "off" ? null : new ServerCoverageCollector({
|
|
10716
11594
|
granularity,
|
|
@@ -10727,28 +11605,41 @@ var CoverageCollection = class {
|
|
|
10727
11605
|
/**
|
|
10728
11606
|
* The browser side of a test's coverage: its code reach paths, or, at
|
|
10729
11607
|
* `functions` granularity, the files of its `piwi-browser-coverage` attachment
|
|
10730
|
-
* with their blob ids, once read.
|
|
11608
|
+
* with their blob ids, once read; with `coverage.css: 'rules'`, the style
|
|
11609
|
+
* sheet files of its `piwi-css-coverage` attachment too. Never rejects.
|
|
10731
11610
|
*/
|
|
10732
11611
|
browserCoverage(testCase, attachments) {
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
const
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
}
|
|
10741
|
-
|
|
11612
|
+
const { browser, css } = this.options.settings;
|
|
11613
|
+
if (browser === "off" && css !== "rules") return {};
|
|
11614
|
+
const paths = browser === "off" ? void 0 : testCase.codeReach;
|
|
11615
|
+
for (const known of attachments ?? []) {
|
|
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);
|
|
11619
|
+
}
|
|
11620
|
+
const scripts = browser === "functions" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.browserCoverage) : [];
|
|
11621
|
+
const sheets = css === "rules" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.cssCoverage) : [];
|
|
11622
|
+
const blobs = this.browserBlobs;
|
|
11623
|
+
if (!blobs || scripts.length === 0 && sheets.length === 0) return { files: paths };
|
|
11624
|
+
const withBlobs = (files) => blobs.withBlobs(files).catch(() => files);
|
|
11625
|
+
return Promise.all([
|
|
11626
|
+
scripts.length > 0 ? withBlobs(scripts) : Promise.resolve(null),
|
|
11627
|
+
sheets.length > 0 ? withBlobs(sheets) : Promise.resolve([])
|
|
11628
|
+
]).then(([files, rules]) => ({ files: files ?? paths, css: rules }));
|
|
11629
|
+
}
|
|
11630
|
+
/** The browser files of a test's attachment holding line ranges, added to the run's. */
|
|
11631
|
+
rangedFiles(attachments, name) {
|
|
11632
|
+
const attachment = findPiwiAttachment(attachments, name);
|
|
11633
|
+
if (!attachment?.body) return [];
|
|
10742
11634
|
let files = [];
|
|
10743
11635
|
try {
|
|
10744
11636
|
files = sanitizeCoverage(JSON.parse(attachment.body.toString("utf8")))?.files ?? [];
|
|
10745
11637
|
} catch {
|
|
10746
|
-
return
|
|
11638
|
+
return [];
|
|
10747
11639
|
}
|
|
10748
11640
|
const browser = files.filter((file) => file.runtime === "browser");
|
|
10749
|
-
if (browser.length === 0) return paths;
|
|
10750
11641
|
for (const file of browser) addRanges(this.browserRan, file.path, file.functions ?? []);
|
|
10751
|
-
return
|
|
11642
|
+
return browser;
|
|
10752
11643
|
}
|
|
10753
11644
|
/** Add the functions of a `piwi-browser-functions` attachment, one of a test's, to the run's. */
|
|
10754
11645
|
addKnownFunctions(attachment) {
|
|
@@ -10762,8 +11653,8 @@ var CoverageCollection = class {
|
|
|
10762
11653
|
const files = parsed2?.v === 1 ? parsed2.files : null;
|
|
10763
11654
|
if (!Array.isArray(files)) return;
|
|
10764
11655
|
for (const item of files) {
|
|
10765
|
-
const
|
|
10766
|
-
if (
|
|
11656
|
+
const filePath3 = normalizeCoveragePath(item?.path);
|
|
11657
|
+
if (filePath3 !== null) addRanges(this.browserKnown, filePath3, normalizeFunctionRanges(item.allFunctions));
|
|
10767
11658
|
}
|
|
10768
11659
|
}
|
|
10769
11660
|
/**
|
|
@@ -10774,13 +11665,13 @@ var CoverageCollection = class {
|
|
|
10774
11665
|
async browserRunFiles() {
|
|
10775
11666
|
if (!this.browserBlobs) return [];
|
|
10776
11667
|
const paths = [.../* @__PURE__ */ new Set([...this.browserKnown.keys(), ...this.browserRan.keys()])].sort();
|
|
10777
|
-
const files = paths.map((
|
|
10778
|
-
const functions = normalizeFunctionRanges([...this.browserRan.get(
|
|
11668
|
+
const files = paths.map((filePath3) => {
|
|
11669
|
+
const functions = normalizeFunctionRanges([...this.browserRan.get(filePath3)?.values() ?? []]);
|
|
10779
11670
|
const allFunctions = normalizeFunctionRanges([
|
|
10780
|
-
...this.browserKnown.get(
|
|
11671
|
+
...this.browserKnown.get(filePath3)?.values() ?? [],
|
|
10781
11672
|
...functions
|
|
10782
11673
|
]);
|
|
10783
|
-
const file = { path:
|
|
11674
|
+
const file = { path: filePath3, runtime: "browser" };
|
|
10784
11675
|
if (functions.length > 0) file.functions = functions;
|
|
10785
11676
|
if (allFunctions.length > 0) file.allFunctions = allFunctions;
|
|
10786
11677
|
return file;
|
|
@@ -10799,15 +11690,15 @@ var CoverageCollection = class {
|
|
|
10799
11690
|
*/
|
|
10800
11691
|
apply(testCase, attachments) {
|
|
10801
11692
|
const browser = this.browserCoverage(testCase, attachments);
|
|
10802
|
-
const attachment = this.collector ? attachments
|
|
11693
|
+
const attachment = this.collector ? findPiwiAttachment(attachments, ATTACHMENT_NAMES.serverCoverageRequests) : void 0;
|
|
10803
11694
|
const requests = attachment?.body ? parseServerCoverageRequests(attachment.body) : null;
|
|
10804
11695
|
if (!this.collector || !requests) {
|
|
10805
11696
|
if (!(browser instanceof Promise)) {
|
|
10806
|
-
this.assign(testCase, composeTestCoverage(null, browser, this.options.settings));
|
|
11697
|
+
this.assign(testCase, composeTestCoverage(null, browser.files, this.options.settings, browser.css));
|
|
10807
11698
|
return null;
|
|
10808
11699
|
}
|
|
10809
11700
|
const ready = browser.then(
|
|
10810
|
-
(
|
|
11701
|
+
(side) => this.assign(testCase, composeTestCoverage(null, side.files, this.options.settings, side.css))
|
|
10811
11702
|
);
|
|
10812
11703
|
this.browserPending.add(ready);
|
|
10813
11704
|
return ready.finally(() => this.browserPending.delete(ready));
|
|
@@ -10821,8 +11712,8 @@ var CoverageCollection = class {
|
|
|
10821
11712
|
settled = true;
|
|
10822
11713
|
clearTimeout(timer);
|
|
10823
11714
|
this.pending.delete(entry);
|
|
10824
|
-
void Promise.resolve(browser).then((
|
|
10825
|
-
this.assign(testCase, composeTestCoverage(server, files, this.options.settings));
|
|
11715
|
+
void Promise.resolve(browser).then((side) => {
|
|
11716
|
+
this.assign(testCase, composeTestCoverage(server, side.files, this.options.settings, side.css));
|
|
10826
11717
|
resolve11();
|
|
10827
11718
|
});
|
|
10828
11719
|
};
|
|
@@ -10959,6 +11850,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10959
11850
|
this.resourcesFile = null;
|
|
10960
11851
|
/** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
|
|
10961
11852
|
this.sampler = null;
|
|
11853
|
+
/** Posts what the sampler and the censuses add while someone has the run open on the dashboard. */
|
|
11854
|
+
this.liveResources = null;
|
|
10962
11855
|
/** Bytes of the files the tests attached, by kind. */
|
|
10963
11856
|
this.artifactBytes = emptyArtifacts();
|
|
10964
11857
|
/** The run's resource report for the dashboard, built in `onEnd`. */
|
|
@@ -11117,8 +12010,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11117
12010
|
);
|
|
11118
12011
|
}
|
|
11119
12012
|
if (coverage.server !== "off") this.lastTestWatch = new LastTestWatch(this.plannedTests.length);
|
|
11120
|
-
if (coverage.browser !== "off" || coverage.server !== "off") {
|
|
12013
|
+
if (coverage.browser !== "off" || coverage.server !== "off" || coverage.css === "rules") {
|
|
11121
12014
|
if (coverage.browser !== "off") process.env[PIWI_ENV_KEYS.coverageBrowser] = coverage.browser;
|
|
12015
|
+
if (coverage.css === "rules") process.env[PIWI_ENV_KEYS.coverageCss] = coverage.css;
|
|
11122
12016
|
if (coverage.server !== "off") {
|
|
11123
12017
|
process.env[PIWI_ENV_KEYS.coverageServer] = coverage.server;
|
|
11124
12018
|
}
|
|
@@ -11143,6 +12037,22 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11143
12037
|
// the dashboard shows the real total from the first render.
|
|
11144
12038
|
this.plannedTests.length
|
|
11145
12039
|
);
|
|
12040
|
+
if (this.streamManager && this.options.captureResources !== false) {
|
|
12041
|
+
const live = new LiveResources(
|
|
12042
|
+
this.streamManager,
|
|
12043
|
+
this.httpClient,
|
|
12044
|
+
{
|
|
12045
|
+
sampler: this.sampler,
|
|
12046
|
+
censuses: () => this.resourceCensuses,
|
|
12047
|
+
shardIndex: this.shardInfo?.current ?? null,
|
|
12048
|
+
runStartedAt: Date.now()
|
|
12049
|
+
},
|
|
12050
|
+
this.logger
|
|
12051
|
+
);
|
|
12052
|
+
this.streamManager.onWatched = () => void live.tick();
|
|
12053
|
+
live.start();
|
|
12054
|
+
this.liveResources = live;
|
|
12055
|
+
}
|
|
11146
12056
|
}
|
|
11147
12057
|
/**
|
|
11148
12058
|
* Settle each planned test's external ID before any test runs, name the IDs two tests share, and record them on
|
|
@@ -11326,7 +12236,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11326
12236
|
if (frames.length) testCase.testSourceFrames = frames;
|
|
11327
12237
|
}
|
|
11328
12238
|
if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
|
|
11329
|
-
const pauses = readPauses(result.attachments
|
|
12239
|
+
const pauses = readPauses(findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.pauses)?.body);
|
|
11330
12240
|
testCase.performanceMetrics = collectStepMetrics(result.steps, result.errors, pauses);
|
|
11331
12241
|
const stepEvents = extractTestStepEvents(result.steps, result.startTime);
|
|
11332
12242
|
const waitEvents = extractWaitEvents(result.steps);
|
|
@@ -11336,14 +12246,14 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11336
12246
|
if (this.options.collectPerformanceMetrics && result.attachments) {
|
|
11337
12247
|
this.fileHandler.parsePerformanceAttachments(testCase, result.attachments);
|
|
11338
12248
|
this.manifests?.note(testCase.networkRequests);
|
|
11339
|
-
const locatorAttachment = this.options.captureLocators !== false ? result.attachments
|
|
12249
|
+
const locatorAttachment = this.options.captureLocators !== false ? findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.locators) : void 0;
|
|
11340
12250
|
if (locatorAttachment?.body) {
|
|
11341
12251
|
try {
|
|
11342
12252
|
testCase.locatorSnapshots = JSON.parse(locatorAttachment.body.toString());
|
|
11343
12253
|
} catch {
|
|
11344
12254
|
}
|
|
11345
12255
|
}
|
|
11346
|
-
const pagesAttachment = this.options.captureLocators !== false ? result.attachments
|
|
12256
|
+
const pagesAttachment = this.options.captureLocators !== false ? findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.locatorPages) : void 0;
|
|
11347
12257
|
if (pagesAttachment?.body) {
|
|
11348
12258
|
try {
|
|
11349
12259
|
testCase.locatorPages = JSON.parse(pagesAttachment.body.toString());
|
|
@@ -11352,7 +12262,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11352
12262
|
}
|
|
11353
12263
|
}
|
|
11354
12264
|
if (this.enabled && this.options.captureResources !== false) this.collectResources(test, result, testCase);
|
|
11355
|
-
const reachAttachment = this.coverageSettings.browser !== "off" ? result.attachments
|
|
12265
|
+
const reachAttachment = this.coverageSettings.browser !== "off" ? findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.codeReach) : void 0;
|
|
11356
12266
|
if (reachAttachment?.body) {
|
|
11357
12267
|
try {
|
|
11358
12268
|
const files = JSON.parse(reachAttachment.body.toString());
|
|
@@ -11424,7 +12334,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11424
12334
|
} catch {
|
|
11425
12335
|
}
|
|
11426
12336
|
}
|
|
11427
|
-
const attachment = result.attachments
|
|
12337
|
+
const attachment = findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.resources);
|
|
11428
12338
|
const census = parseResourceCensus(attachment?.body);
|
|
11429
12339
|
if (census) {
|
|
11430
12340
|
this.resourceCensuses.push(census);
|
|
@@ -11445,6 +12355,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11445
12355
|
*/
|
|
11446
12356
|
async reportResources() {
|
|
11447
12357
|
if (this.options.captureResources === false) return;
|
|
12358
|
+
await this.liveResources?.stop();
|
|
12359
|
+
this.liveResources = null;
|
|
11448
12360
|
const profile = this.sampler ? await this.sampler.stop() : null;
|
|
11449
12361
|
const samples = this.sampler?.timedSeries() ?? null;
|
|
11450
12362
|
this.sampler = null;
|
|
@@ -12442,14 +13354,14 @@ function showAnchorPicker(arg) {
|
|
|
12442
13354
|
} catch {
|
|
12443
13355
|
shadowRoots.length = 0;
|
|
12444
13356
|
}
|
|
12445
|
-
const
|
|
13357
|
+
const within2 = (node2, scope) => {
|
|
12446
13358
|
if (scope === doc) return true;
|
|
12447
13359
|
for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
|
|
12448
13360
|
return false;
|
|
12449
13361
|
};
|
|
12450
13362
|
const queryAll = (scope, sel) => {
|
|
12451
13363
|
const found = Array.from(scope.querySelectorAll(sel));
|
|
12452
|
-
for (const sr of shadowRoots) if (
|
|
13364
|
+
for (const sr of shadowRoots) if (within2(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
|
|
12453
13365
|
return found;
|
|
12454
13366
|
};
|
|
12455
13367
|
const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
|
|
@@ -12530,8 +13442,8 @@ function showAnchorPicker(arg) {
|
|
|
12530
13442
|
return;
|
|
12531
13443
|
}
|
|
12532
13444
|
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
12533
|
-
const
|
|
12534
|
-
|
|
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);`;
|
|
12535
13447
|
const outlineLabel = doc.createElement("div");
|
|
12536
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);`;
|
|
12537
13449
|
const pickedOutline = doc.createElement("div");
|
|
@@ -12620,11 +13532,11 @@ function showAnchorPicker(arg) {
|
|
|
12620
13532
|
line.appendChild(text);
|
|
12621
13533
|
line.addEventListener("mouseenter", () => {
|
|
12622
13534
|
const r = row.node.getBoundingClientRect();
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
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";
|
|
12628
13540
|
outlineLabel.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
12629
13541
|
outlineLabel.style.display = "block";
|
|
12630
13542
|
const lr = outlineLabel.getBoundingClientRect();
|
|
@@ -12634,7 +13546,7 @@ function showAnchorPicker(arg) {
|
|
|
12634
13546
|
outlineLabel.style.top = top + "px";
|
|
12635
13547
|
});
|
|
12636
13548
|
line.addEventListener("mouseleave", () => {
|
|
12637
|
-
|
|
13549
|
+
outline2.style.display = "none";
|
|
12638
13550
|
outlineLabel.style.display = "none";
|
|
12639
13551
|
});
|
|
12640
13552
|
box.addEventListener("change", () => {
|
|
@@ -12648,7 +13560,7 @@ function showAnchorPicker(arg) {
|
|
|
12648
13560
|
const cleanup = () => {
|
|
12649
13561
|
doc.removeEventListener("keydown", onKey, true);
|
|
12650
13562
|
panel.remove();
|
|
12651
|
-
|
|
13563
|
+
outline2.remove();
|
|
12652
13564
|
outlineLabel.remove();
|
|
12653
13565
|
pickedOutline.remove();
|
|
12654
13566
|
if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
|
|
@@ -12713,7 +13625,7 @@ function showAnchorPicker(arg) {
|
|
|
12713
13625
|
);
|
|
12714
13626
|
}
|
|
12715
13627
|
};
|
|
12716
|
-
mount(pickedOutline,
|
|
13628
|
+
mount(pickedOutline, outline2, outlineLabel, panel);
|
|
12717
13629
|
}
|
|
12718
13630
|
|
|
12719
13631
|
// ../picker-dom/src/overlay-confirm.ts
|
|
@@ -12817,7 +13729,7 @@ function showPauseBar(arg) {
|
|
|
12817
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;`;
|
|
12818
13730
|
const where = doc.createElement("div");
|
|
12819
13731
|
where.style.cssText = "font-weight:600;white-space:nowrap;";
|
|
12820
|
-
where.textContent =
|
|
13732
|
+
where.textContent = `${arg.failure ? "Failed at" : "Paused at"} ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
|
|
12821
13733
|
const what = doc.createElement("code");
|
|
12822
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;";
|
|
12823
13735
|
what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
|
|
@@ -12840,13 +13752,13 @@ function showPauseBar(arg) {
|
|
|
12840
13752
|
delete g.__piwiPauseCleanup;
|
|
12841
13753
|
};
|
|
12842
13754
|
const buttons = [
|
|
12843
|
-
["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
|
|
12844
|
-
["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]],
|
|
12845
13757
|
["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
|
|
12846
13758
|
...arg.record ? [
|
|
12847
13759
|
["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
|
|
12848
13760
|
] : [],
|
|
12849
|
-
["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]]
|
|
12850
13762
|
];
|
|
12851
13763
|
for (const [label, choice, hint, primary] of buttons) {
|
|
12852
13764
|
const btn = doc.createElement("button");
|
|
@@ -13011,7 +13923,7 @@ var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "dat
|
|
|
13011
13923
|
function generateAlternatives(attrs, options = {}) {
|
|
13012
13924
|
const alts = [];
|
|
13013
13925
|
const seen = /* @__PURE__ */ new Set();
|
|
13014
|
-
const
|
|
13926
|
+
const add2 = (loc) => {
|
|
13015
13927
|
if (!seen.has(loc.locator)) {
|
|
13016
13928
|
seen.add(loc.locator);
|
|
13017
13929
|
alts.push(loc);
|
|
@@ -13028,7 +13940,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13028
13940
|
const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
|
|
13029
13941
|
const testId = attr(attrs, testIdAttribute);
|
|
13030
13942
|
if (testId && isUnique(counts?.testId)) {
|
|
13031
|
-
|
|
13943
|
+
add2({
|
|
13032
13944
|
locator: `getByTestId(${quoteJsString(testId)})`,
|
|
13033
13945
|
method: "getByTestId",
|
|
13034
13946
|
args: { testId },
|
|
@@ -13039,7 +13951,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13039
13951
|
const levelPart = level != null ? `, level: ${level}` : "";
|
|
13040
13952
|
const withLevel = (base) => level != null ? { ...base, level } : base;
|
|
13041
13953
|
if (role && accessibleName) {
|
|
13042
|
-
|
|
13954
|
+
add2({
|
|
13043
13955
|
locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
|
|
13044
13956
|
method: "getByRole",
|
|
13045
13957
|
args: withLevel({ role, name: accessibleName }),
|
|
@@ -13048,7 +13960,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13048
13960
|
}
|
|
13049
13961
|
const ariaLabel = attr(attrs, "aria-label");
|
|
13050
13962
|
if (role && ariaLabel && ariaLabel !== accessibleName) {
|
|
13051
|
-
|
|
13963
|
+
add2({
|
|
13052
13964
|
locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
|
|
13053
13965
|
method: "getByRole",
|
|
13054
13966
|
args: withLevel({ role, name: ariaLabel }),
|
|
@@ -13059,7 +13971,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13059
13971
|
const label = attr(attrs, "aria-label");
|
|
13060
13972
|
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
13061
13973
|
if (labelBacked) {
|
|
13062
|
-
|
|
13974
|
+
add2({
|
|
13063
13975
|
locator: `getByLabel(${quoteJsString(accessibleName)})`,
|
|
13064
13976
|
method: "getByLabel",
|
|
13065
13977
|
args: { label: accessibleName },
|
|
@@ -13069,7 +13981,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13069
13981
|
}
|
|
13070
13982
|
const placeholder = attr(attrs, "placeholder");
|
|
13071
13983
|
if (placeholder) {
|
|
13072
|
-
|
|
13984
|
+
add2({
|
|
13073
13985
|
locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
|
|
13074
13986
|
method: "getByPlaceholder",
|
|
13075
13987
|
args: { placeholder },
|
|
@@ -13077,7 +13989,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13077
13989
|
});
|
|
13078
13990
|
}
|
|
13079
13991
|
if (text && text.length < 80) {
|
|
13080
|
-
|
|
13992
|
+
add2({
|
|
13081
13993
|
locator: `getByText(${quoteJsString(text)})`,
|
|
13082
13994
|
method: "getByText",
|
|
13083
13995
|
args: { text },
|
|
@@ -13087,7 +13999,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13087
13999
|
const id = attr(attrs, "id");
|
|
13088
14000
|
if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
13089
14001
|
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
13090
|
-
|
|
14002
|
+
add2({
|
|
13091
14003
|
locator: `locator(${quoteJsString(selector)})`,
|
|
13092
14004
|
method: "locator",
|
|
13093
14005
|
args: { selector },
|
|
@@ -13097,7 +14009,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13097
14009
|
const name = attr(attrs, "name");
|
|
13098
14010
|
if (name && selectable("name") && isUnique(counts?.name)) {
|
|
13099
14011
|
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
13100
|
-
|
|
14012
|
+
add2({
|
|
13101
14013
|
locator: `locator(${quoteJsString(selector)})`,
|
|
13102
14014
|
method: "locator",
|
|
13103
14015
|
args: { selector },
|
|
@@ -13106,7 +14018,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13106
14018
|
}
|
|
13107
14019
|
const alt = attr(attrs, "alt");
|
|
13108
14020
|
if (alt) {
|
|
13109
|
-
|
|
14021
|
+
add2({
|
|
13110
14022
|
locator: `getByAltText(${quoteJsString(alt)})`,
|
|
13111
14023
|
method: "getByAltText",
|
|
13112
14024
|
args: { text: alt },
|
|
@@ -13115,7 +14027,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13115
14027
|
}
|
|
13116
14028
|
const title = attr(attrs, "title");
|
|
13117
14029
|
if (title) {
|
|
13118
|
-
|
|
14030
|
+
add2({
|
|
13119
14031
|
locator: `getByTitle(${quoteJsString(title)})`,
|
|
13120
14032
|
method: "getByTitle",
|
|
13121
14033
|
args: { title },
|
|
@@ -13133,7 +14045,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13133
14045
|
if (scopedCount(anc) !== 1) continue;
|
|
13134
14046
|
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
13135
14047
|
testIdAnchorDone = true;
|
|
13136
|
-
|
|
14048
|
+
add2({
|
|
13137
14049
|
locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
|
|
13138
14050
|
method: leaf.method,
|
|
13139
14051
|
args: { ...leaf.args, anchorTestId: anc.testId },
|
|
@@ -13143,7 +14055,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13143
14055
|
if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
13144
14056
|
idAnchorDone = true;
|
|
13145
14057
|
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
13146
|
-
|
|
14058
|
+
add2({
|
|
13147
14059
|
locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
|
|
13148
14060
|
method: leaf.method,
|
|
13149
14061
|
args: { ...leaf.args, anchorSelector },
|
|
@@ -13154,7 +14066,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13154
14066
|
dataAnchorDone = true;
|
|
13155
14067
|
const { name: name2, value } = anc.dataAttr;
|
|
13156
14068
|
const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
|
|
13157
|
-
|
|
14069
|
+
add2({
|
|
13158
14070
|
locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
|
|
13159
14071
|
method: leaf.method,
|
|
13160
14072
|
args: { ...leaf.args, anchorSelector },
|
|
@@ -13164,7 +14076,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13164
14076
|
const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
|
|
13165
14077
|
if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
|
|
13166
14078
|
roleAnchorDone = true;
|
|
13167
|
-
|
|
14079
|
+
add2({
|
|
13168
14080
|
locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
|
|
13169
14081
|
method: leaf.method,
|
|
13170
14082
|
args: { ...leaf.args, anchorRole: ancestorRole },
|
|
@@ -13173,7 +14085,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13173
14085
|
}
|
|
13174
14086
|
if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
|
|
13175
14087
|
filterAnchorDone = true;
|
|
13176
|
-
|
|
14088
|
+
add2({
|
|
13177
14089
|
locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
|
|
13178
14090
|
method: leaf.method,
|
|
13179
14091
|
args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
|
|
@@ -13192,7 +14104,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13192
14104
|
);
|
|
13193
14105
|
const pos = attrs.rolePosition;
|
|
13194
14106
|
if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
|
|
13195
|
-
|
|
14107
|
+
add2({
|
|
13196
14108
|
locator: `getByRole(${rolePart})`,
|
|
13197
14109
|
method: "getByRole",
|
|
13198
14110
|
args: leafArgs,
|
|
@@ -13211,7 +14123,7 @@ function generateAlternatives(attrs, options = {}) {
|
|
|
13211
14123
|
const countsClasses = counts?.classes;
|
|
13212
14124
|
const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
|
|
13213
14125
|
for (const { cls, score } of classes) {
|
|
13214
|
-
|
|
14126
|
+
add2({
|
|
13215
14127
|
locator: `locator(${quoteJsString(`.${cls}`)})`,
|
|
13216
14128
|
method: "locator",
|
|
13217
14129
|
args: { selector: `.${cls}` },
|
|
@@ -13268,7 +14180,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
13268
14180
|
const seen = /* @__PURE__ */ new Set();
|
|
13269
14181
|
const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
|
|
13270
14182
|
const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
|
|
13271
|
-
const
|
|
14183
|
+
const add2 = (l) => {
|
|
13272
14184
|
if (!seen.has(l.locator)) {
|
|
13273
14185
|
seen.add(l.locator);
|
|
13274
14186
|
out.push(l);
|
|
@@ -13278,7 +14190,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
13278
14190
|
if (anchor.scopedLeafCount !== 1) continue;
|
|
13279
14191
|
const seg = anchorSegment(anchor);
|
|
13280
14192
|
if (!seg) continue;
|
|
13281
|
-
|
|
14193
|
+
add2({
|
|
13282
14194
|
locator: `${seg.code}.getByRole(${rolePart})`,
|
|
13283
14195
|
method: "getByRole",
|
|
13284
14196
|
args: { ...leafArgs, ...seg.flatArgs },
|
|
@@ -13291,7 +14203,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
|
13291
14203
|
if (segments.every((s) => s !== null)) {
|
|
13292
14204
|
const innermost = segments[segments.length - 1];
|
|
13293
14205
|
const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
|
|
13294
|
-
|
|
14206
|
+
add2({
|
|
13295
14207
|
locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
|
|
13296
14208
|
method: "getByRole",
|
|
13297
14209
|
// Flat args describe the innermost anchor (what the healing fingerprint
|
|
@@ -14254,11 +15166,31 @@ function parsePauseAt(value, cwd = process.cwd()) {
|
|
|
14254
15166
|
return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
|
|
14255
15167
|
}
|
|
14256
15168
|
var parsed = null;
|
|
15169
|
+
var sent = null;
|
|
14257
15170
|
function currentPauseSet() {
|
|
15171
|
+
if (sent) return sent;
|
|
14258
15172
|
const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
|
|
14259
15173
|
if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
|
|
14260
15174
|
return parsed.set;
|
|
14261
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
|
+
}
|
|
14262
15194
|
function pauseGateFromTestInfo(testInfo) {
|
|
14263
15195
|
const use = testInfo.project?.use ?? {};
|
|
14264
15196
|
const debug = testInfo._configInternal?.configCLIOverrides?.debug;
|
|
@@ -14266,7 +15198,7 @@ function pauseGateFromTestInfo(testInfo) {
|
|
|
14266
15198
|
}
|
|
14267
15199
|
function pauseSkipReason(gate) {
|
|
14268
15200
|
if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
|
|
14269
|
-
if (gate.headless !== false)
|
|
15201
|
+
if (gate.headless !== false && !gate.channel)
|
|
14270
15202
|
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
14271
15203
|
if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
|
|
14272
15204
|
return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
|
|
@@ -14274,20 +15206,42 @@ function pauseSkipReason(gate) {
|
|
|
14274
15206
|
return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
|
|
14275
15207
|
return null;
|
|
14276
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
|
+
}
|
|
14277
15218
|
function atStop(state, callSite) {
|
|
14278
|
-
if (callSite
|
|
14279
|
-
|
|
14280
|
-
|
|
14281
|
-
|
|
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 };
|
|
14282
15224
|
}
|
|
14283
|
-
if (state.set.matches(callSite)) return { show: "breakpoint" };
|
|
14284
15225
|
}
|
|
14285
15226
|
return { go: goOn(state) };
|
|
14286
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
|
+
}
|
|
14287
15241
|
function goOn(state, answer) {
|
|
14288
15242
|
if (answer === "step") state.step = true;
|
|
14289
15243
|
else if (answer === "finish") state.finished = true;
|
|
14290
|
-
return
|
|
15244
|
+
return wantsStops(state) ? "next" : "resume";
|
|
14291
15245
|
}
|
|
14292
15246
|
function contextDebugger(context) {
|
|
14293
15247
|
const dbg = context.debugger;
|
|
@@ -14423,38 +15377,83 @@ function logPauseSkipOnce(reason) {
|
|
|
14423
15377
|
var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
|
|
14424
15378
|
var MAX_NAVIGATED = 10;
|
|
14425
15379
|
var HIGHLIGHT_MS = 2e3;
|
|
14426
|
-
|
|
14427
|
-
|
|
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;
|
|
14428
15425
|
try {
|
|
14429
15426
|
const highlight = point.target?.highlight;
|
|
14430
15427
|
if (typeof highlight === "function") {
|
|
14431
|
-
|
|
14432
|
-
|
|
14433
|
-
Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
|
|
14434
|
-
new Promise((resolve11) => {
|
|
14435
|
-
timer = setTimeout(() => resolve11(null), HIGHLIGHT_MS);
|
|
14436
|
-
})
|
|
14437
|
-
]);
|
|
14438
|
-
clearTimeout(timer);
|
|
15428
|
+
const highlighted = Promise.resolve().then(() => highlight.call(point.target)).catch(() => null);
|
|
15429
|
+
shown2 = await within(highlighted, HIGHLIGHT_MS) ?? null;
|
|
14439
15430
|
}
|
|
14440
15431
|
const arg = {
|
|
14441
15432
|
place: pausePlace(point.location),
|
|
14442
15433
|
attempt: point.attempt,
|
|
14443
15434
|
action: point.call,
|
|
14444
15435
|
locator: null,
|
|
14445
|
-
record: !!onRecord
|
|
15436
|
+
record: !!onRecord,
|
|
15437
|
+
...point.failure ? { failure: true } : {}
|
|
14446
15438
|
};
|
|
14447
15439
|
console.log(
|
|
14448
|
-
`[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).`
|
|
14449
15441
|
);
|
|
14450
15442
|
let navigated = 0;
|
|
14451
15443
|
for (; ; ) {
|
|
14452
15444
|
try {
|
|
14453
15445
|
if (ended?.aborted) return "resume";
|
|
14454
|
-
await page.evaluate(showPauseBar, arg);
|
|
14455
|
-
|
|
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);
|
|
14456
15456
|
if (ended?.aborted) return "resume";
|
|
14457
|
-
const choice = await page.evaluate(takePauseState);
|
|
14458
15457
|
navigated = 0;
|
|
14459
15458
|
if (choice === "resume" || choice === "step" || choice === "finish") return choice;
|
|
14460
15459
|
if (choice === "record" && onRecord) {
|
|
@@ -14464,31 +15463,290 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
|
|
|
14464
15463
|
);
|
|
14465
15464
|
continue;
|
|
14466
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
|
+
}
|
|
14467
15473
|
if (choice !== "pick") continue;
|
|
14468
15474
|
const picked = await pickLocator(page, probe, null, ended).catch(() => null);
|
|
14469
15475
|
if (picked && !ended?.aborted) await onPick(picked);
|
|
14470
15476
|
} catch (error) {
|
|
14471
15477
|
if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
|
|
14472
|
-
await
|
|
15478
|
+
await unlessEnded(
|
|
15479
|
+
page.waitForLoadState("domcontentloaded").catch(() => void 0),
|
|
15480
|
+
ended
|
|
15481
|
+
);
|
|
14473
15482
|
}
|
|
14474
15483
|
}
|
|
14475
15484
|
} catch {
|
|
14476
15485
|
return "resume";
|
|
14477
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('"')) {
|
|
14478
15574
|
try {
|
|
14479
|
-
|
|
14480
|
-
const dispose = shown?.dispose;
|
|
14481
|
-
const hide = page.hideHighlight;
|
|
14482
|
-
if (typeof dispose === "function") await dispose.call(shown);
|
|
14483
|
-
else if (typeof hide === "function") await hide.call(page);
|
|
15575
|
+
return String(JSON.parse(value));
|
|
14484
15576
|
} catch {
|
|
15577
|
+
return value;
|
|
14485
15578
|
}
|
|
14486
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;
|
|
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
|
+
};
|
|
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 };
|
|
14487
15745
|
}
|
|
14488
15746
|
|
|
14489
15747
|
// src/internal/capture/record-handoff.ts
|
|
14490
15748
|
var fs26 = __toESM(require("fs"));
|
|
14491
|
-
var
|
|
15749
|
+
var net3 = __toESM(require("net"));
|
|
14492
15750
|
var path29 = __toESM(require("path"));
|
|
14493
15751
|
|
|
14494
15752
|
// ../core/src/ide-recorder.ts
|
|
@@ -14504,7 +15762,7 @@ function parseRecordChannel(value) {
|
|
|
14504
15762
|
const path34 = text.slice(0, hash);
|
|
14505
15763
|
return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path34.includes("\0") ? { path: path34, token } : null;
|
|
14506
15764
|
}
|
|
14507
|
-
function
|
|
15765
|
+
function isObject3(value) {
|
|
14508
15766
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14509
15767
|
}
|
|
14510
15768
|
function parseHandoffToTest(line) {
|
|
@@ -14514,7 +15772,7 @@ function parseHandoffToTest(line) {
|
|
|
14514
15772
|
} catch {
|
|
14515
15773
|
return null;
|
|
14516
15774
|
}
|
|
14517
|
-
if (!
|
|
15775
|
+
if (!isObject3(value)) return null;
|
|
14518
15776
|
switch (value.type) {
|
|
14519
15777
|
case "refused":
|
|
14520
15778
|
return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
|
|
@@ -14577,8 +15835,8 @@ async function recordFromHere(options) {
|
|
|
14577
15835
|
const use = testInfo.project?.use ?? {};
|
|
14578
15836
|
const context = page.context();
|
|
14579
15837
|
return new Promise((resolve11) => {
|
|
14580
|
-
const
|
|
14581
|
-
|
|
15838
|
+
const socket2 = net3.connect(channel.path);
|
|
15839
|
+
socket2.setEncoding("utf8");
|
|
14582
15840
|
const answers = /* @__PURE__ */ new Map();
|
|
14583
15841
|
let calls = 0;
|
|
14584
15842
|
let attached = false;
|
|
@@ -14586,7 +15844,7 @@ async function recordFromHere(options) {
|
|
|
14586
15844
|
let settled = false;
|
|
14587
15845
|
let initScript;
|
|
14588
15846
|
const send = (message) => {
|
|
14589
|
-
if (!
|
|
15847
|
+
if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
|
|
14590
15848
|
`);
|
|
14591
15849
|
};
|
|
14592
15850
|
const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
|
|
@@ -14636,7 +15894,7 @@ async function recordFromHere(options) {
|
|
|
14636
15894
|
page.off("close", onGone);
|
|
14637
15895
|
page.off("crash", onGone);
|
|
14638
15896
|
ended?.removeEventListener("abort", onEnded);
|
|
14639
|
-
|
|
15897
|
+
socket2.end();
|
|
14640
15898
|
resolve11(recording ? null : said);
|
|
14641
15899
|
};
|
|
14642
15900
|
const relayed = (source, payload) => new Promise((answer) => {
|
|
@@ -14676,7 +15934,7 @@ async function recordFromHere(options) {
|
|
|
14676
15934
|
context.on("page", onPage);
|
|
14677
15935
|
send({ type: "loaded" });
|
|
14678
15936
|
};
|
|
14679
|
-
|
|
15937
|
+
socket2.on("connect", () => {
|
|
14680
15938
|
attached = true;
|
|
14681
15939
|
send({
|
|
14682
15940
|
type: "attach",
|
|
@@ -14685,10 +15943,11 @@ async function recordFromHere(options) {
|
|
|
14685
15943
|
line: place.line,
|
|
14686
15944
|
reason: options.reason,
|
|
14687
15945
|
testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
|
|
14688
|
-
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 } : {}
|
|
14689
15948
|
});
|
|
14690
15949
|
});
|
|
14691
|
-
|
|
15950
|
+
socket2.on(
|
|
14692
15951
|
"data",
|
|
14693
15952
|
lineReader(
|
|
14694
15953
|
(line) => {
|
|
@@ -14721,11 +15980,516 @@ async function recordFromHere(options) {
|
|
|
14721
15980
|
() => finish("the editor sent a message too long to read.")
|
|
14722
15981
|
)
|
|
14723
15982
|
);
|
|
14724
|
-
|
|
14725
|
-
|
|
15983
|
+
socket2.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
|
|
15984
|
+
socket2.on("close", () => finish("the editor closed the recording."));
|
|
14726
15985
|
});
|
|
14727
15986
|
}
|
|
14728
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
|
+
|
|
16232
|
+
// src/internal/capture/css-coverage.ts
|
|
16233
|
+
var crypto8 = __toESM(require("crypto"));
|
|
16234
|
+
var fs27 = __toESM(require("fs"));
|
|
16235
|
+
|
|
16236
|
+
// ../core/src/css-rules.ts
|
|
16237
|
+
var GROUPING = /* @__PURE__ */ new Set(["media", "supports", "layer", "container", "scope", "starting-style", "document"]);
|
|
16238
|
+
function parseCssRules(text) {
|
|
16239
|
+
const rules = [];
|
|
16240
|
+
const open = [];
|
|
16241
|
+
let preludeStart = -1;
|
|
16242
|
+
for (let i = 0; i < text.length; i++) {
|
|
16243
|
+
const c = text[i];
|
|
16244
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
16245
|
+
const end = text.indexOf("*/", i + 2);
|
|
16246
|
+
i = end < 0 ? text.length : end + 1;
|
|
16247
|
+
continue;
|
|
16248
|
+
}
|
|
16249
|
+
if (c === '"' || c === "'") {
|
|
16250
|
+
if (preludeStart < 0) preludeStart = i;
|
|
16251
|
+
for (i++; i < text.length && text[i] !== c && text[i] !== "\n"; i++) if (text[i] === "\\") i++;
|
|
16252
|
+
continue;
|
|
16253
|
+
}
|
|
16254
|
+
if (c === "\\") {
|
|
16255
|
+
if (preludeStart < 0) preludeStart = i;
|
|
16256
|
+
i++;
|
|
16257
|
+
continue;
|
|
16258
|
+
}
|
|
16259
|
+
if ((c === "u" || c === "U") && /^url\(/i.test(text.slice(i, i + 4)) && !/[\w-]/.test(text[i - 1] ?? "")) {
|
|
16260
|
+
let j = i + 4;
|
|
16261
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
16262
|
+
if (text[j] !== '"' && text[j] !== "'") {
|
|
16263
|
+
if (preludeStart < 0) preludeStart = i;
|
|
16264
|
+
for (; j < text.length && text[j] !== ")"; j++) if (text[j] === "\\") j++;
|
|
16265
|
+
i = j;
|
|
16266
|
+
continue;
|
|
16267
|
+
}
|
|
16268
|
+
}
|
|
16269
|
+
if (text.startsWith("<!--", i) || text.startsWith("-->", i)) {
|
|
16270
|
+
i += text.startsWith("<!--", i) ? 3 : 2;
|
|
16271
|
+
continue;
|
|
16272
|
+
}
|
|
16273
|
+
if (c === "{") {
|
|
16274
|
+
const start = preludeStart < 0 ? i : preludeStart;
|
|
16275
|
+
const atRule = /^@([\w-]+)/.exec(text.slice(start, i));
|
|
16276
|
+
const parent = open[open.length - 1];
|
|
16277
|
+
const kind = atRule ? GROUPING.has(atRule[1].toLowerCase()) ? "group" : "other" : parent?.kind === "other" ? "other" : "style";
|
|
16278
|
+
open.push({ kind, start, open: i });
|
|
16279
|
+
preludeStart = -1;
|
|
16280
|
+
continue;
|
|
16281
|
+
}
|
|
16282
|
+
if (c === "}") {
|
|
16283
|
+
const block = open.pop();
|
|
16284
|
+
if (block && block.kind !== "other")
|
|
16285
|
+
rules.push({ kind: block.kind, start: block.start, open: block.open, end: i + 1 });
|
|
16286
|
+
preludeStart = -1;
|
|
16287
|
+
continue;
|
|
16288
|
+
}
|
|
16289
|
+
if (c === ";") {
|
|
16290
|
+
preludeStart = -1;
|
|
16291
|
+
continue;
|
|
16292
|
+
}
|
|
16293
|
+
if (preludeStart < 0 && !/\s/.test(c)) preludeStart = i;
|
|
16294
|
+
}
|
|
16295
|
+
for (const block of open) {
|
|
16296
|
+
if (block.kind !== "other")
|
|
16297
|
+
rules.push({ kind: block.kind, start: block.start, open: block.open, end: text.length });
|
|
16298
|
+
}
|
|
16299
|
+
return rules.sort((a, b) => a.start - b.start);
|
|
16300
|
+
}
|
|
16301
|
+
function usedCssRules(rules, used) {
|
|
16302
|
+
const styleAt = /* @__PURE__ */ new Map();
|
|
16303
|
+
const groups = [];
|
|
16304
|
+
for (const rule of rules) {
|
|
16305
|
+
if (rule.kind === "style") styleAt.set(rule.start, rule);
|
|
16306
|
+
else groups.push(rule);
|
|
16307
|
+
}
|
|
16308
|
+
const out = /* @__PURE__ */ new Map();
|
|
16309
|
+
for (const range of used) {
|
|
16310
|
+
const style = styleAt.get(range.startOffset);
|
|
16311
|
+
if (style) {
|
|
16312
|
+
out.set(`${style.start}`, [style.start, style.end]);
|
|
16313
|
+
continue;
|
|
16314
|
+
}
|
|
16315
|
+
const group = groups.find((rule) => rule.start <= range.startOffset && range.startOffset <= rule.open);
|
|
16316
|
+
if (group) out.set(`${group.start}`, [group.start, group.start + 1]);
|
|
16317
|
+
}
|
|
16318
|
+
return [...out.values()].sort((a, b) => a[0] - b[0]);
|
|
16319
|
+
}
|
|
16320
|
+
function allCssRules(rules) {
|
|
16321
|
+
return rules.map((rule) => rule.kind === "style" ? [rule.start, rule.end] : [rule.start, rule.start + 1]);
|
|
16322
|
+
}
|
|
16323
|
+
function cssRuleLines(text, offsets, options = {}) {
|
|
16324
|
+
const starts = lineStarts(text);
|
|
16325
|
+
const lineOffset = options.lineOffset ?? 0;
|
|
16326
|
+
const out = /* @__PURE__ */ new Map();
|
|
16327
|
+
const add2 = (source, range) => {
|
|
16328
|
+
const ranges = out.get(source);
|
|
16329
|
+
if (ranges) ranges.push(range);
|
|
16330
|
+
else out.set(source, [range]);
|
|
16331
|
+
};
|
|
16332
|
+
for (const [startOffset, endOffset] of offsets) {
|
|
16333
|
+
const start = offsetToPosition(starts, startOffset);
|
|
16334
|
+
const end = offsetToPosition(starts, Math.max(startOffset, endOffset - 1));
|
|
16335
|
+
if (!options.lookup) {
|
|
16336
|
+
add2(-1, [start.line + lineOffset + 1, end.line + lineOffset + 1]);
|
|
16337
|
+
continue;
|
|
16338
|
+
}
|
|
16339
|
+
const first = options.lookup.originalAt(start.line, start.column);
|
|
16340
|
+
if (!first) continue;
|
|
16341
|
+
const last = options.lookup.originalAt(end.line, end.column);
|
|
16342
|
+
const lastLine = last && last.source === first.source && last.line >= first.line ? last.line : first.line;
|
|
16343
|
+
add2(first.source, [first.line + 1, lastLine + 1]);
|
|
16344
|
+
}
|
|
16345
|
+
return out;
|
|
16346
|
+
}
|
|
16347
|
+
|
|
16348
|
+
// src/internal/capture/css-coverage.ts
|
|
16349
|
+
var STYLE_SOURCES = /\.(?:css|scss|sass|less|styl|stylus|pcss|postcss|vue|svelte|astro|html?)$/i;
|
|
16350
|
+
var BUILD_DIRS2 = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit)\//;
|
|
16351
|
+
var resolutions = /* @__PURE__ */ new Map();
|
|
16352
|
+
var MAX_CACHED_SHEETS = 2e3;
|
|
16353
|
+
function fileForStyleUrl(url, roots) {
|
|
16354
|
+
let pathname;
|
|
16355
|
+
try {
|
|
16356
|
+
const parsed2 = new URL(url);
|
|
16357
|
+
if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
|
|
16358
|
+
pathname = parsed2.pathname;
|
|
16359
|
+
} catch {
|
|
16360
|
+
return null;
|
|
16361
|
+
}
|
|
16362
|
+
return sourceFile(decodeURIComponent(pathname), roots);
|
|
16363
|
+
}
|
|
16364
|
+
function sourceFile(source, roots) {
|
|
16365
|
+
const bare = source.replace(/[?#].*$/, "");
|
|
16366
|
+
if (!STYLE_SOURCES.test(bare) || /(?:^|\/)node_modules\//.test(bare)) return null;
|
|
16367
|
+
const file = resolveSourcePath(bare, roots);
|
|
16368
|
+
if (!file || BUILD_DIRS2.test(file.replace(/\\/g, "/")) || /(?:^|[\\/])node_modules[\\/]/.test(file)) return null;
|
|
16369
|
+
return file;
|
|
16370
|
+
}
|
|
16371
|
+
function styleBlockLine(file, index) {
|
|
16372
|
+
let text;
|
|
16373
|
+
try {
|
|
16374
|
+
text = fs27.readFileSync(file, "utf-8");
|
|
16375
|
+
} catch {
|
|
16376
|
+
return 0;
|
|
16377
|
+
}
|
|
16378
|
+
const blocks = [...text.matchAll(/^<style\b[^>]*>/gim)];
|
|
16379
|
+
const block = blocks[index];
|
|
16380
|
+
if (!block) return 0;
|
|
16381
|
+
return text.slice(0, block.index + block[0].length).split("\n").length - 1;
|
|
16382
|
+
}
|
|
16383
|
+
async function sheetMap(sheet, text, fetchMap) {
|
|
16384
|
+
const ref = sourceMappingUrl(text) ?? sheet.sourceMapURL ?? null;
|
|
16385
|
+
if (!ref) return null;
|
|
16386
|
+
let raw;
|
|
16387
|
+
if (ref.startsWith("data:")) raw = decodeDataUrl(ref);
|
|
16388
|
+
else {
|
|
16389
|
+
try {
|
|
16390
|
+
raw = await fetchMap(new URL(ref, sheet.sourceURL).href);
|
|
16391
|
+
} catch {
|
|
16392
|
+
raw = null;
|
|
16393
|
+
}
|
|
16394
|
+
}
|
|
16395
|
+
if (!raw) return null;
|
|
16396
|
+
try {
|
|
16397
|
+
return decodeSourceMap(JSON.parse(raw));
|
|
16398
|
+
} catch {
|
|
16399
|
+
return null;
|
|
16400
|
+
}
|
|
16401
|
+
}
|
|
16402
|
+
function sameLines(text, rules, file, lineOffset) {
|
|
16403
|
+
let source;
|
|
16404
|
+
try {
|
|
16405
|
+
source = fs27.readFileSync(file, "utf-8");
|
|
16406
|
+
} catch {
|
|
16407
|
+
return false;
|
|
16408
|
+
}
|
|
16409
|
+
const fileLines = source.split(/\r?\n/);
|
|
16410
|
+
const sheetLineStarts = [0];
|
|
16411
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") sheetLineStarts.push(i + 1);
|
|
16412
|
+
let low = 0;
|
|
16413
|
+
for (const rule of rules) {
|
|
16414
|
+
while (low + 1 < sheetLineStarts.length && sheetLineStarts[low + 1] <= rule.start) low++;
|
|
16415
|
+
const head = text.slice(rule.start, rule.open).split("\n")[0].trim();
|
|
16416
|
+
if (head && !(fileLines[low + lineOffset] ?? "").includes(head)) return false;
|
|
16417
|
+
}
|
|
16418
|
+
return true;
|
|
16419
|
+
}
|
|
16420
|
+
async function resolveSheet(sheet, text, dirs, fetchMap) {
|
|
16421
|
+
const lookup = await sheetMap(sheet, text, fetchMap);
|
|
16422
|
+
const rules = parseCssRules(text);
|
|
16423
|
+
if (lookup) {
|
|
16424
|
+
const sources = lookup.sources.map((source) => {
|
|
16425
|
+
const direct = sourceFile(source, dirs);
|
|
16426
|
+
if (direct) return direct;
|
|
16427
|
+
try {
|
|
16428
|
+
return /^[a-z][\w+.-]*:/i.test(source) ? null : fileForStyleUrl(new URL(source, sheet.sourceURL).href, dirs);
|
|
16429
|
+
} catch {
|
|
16430
|
+
return null;
|
|
16431
|
+
}
|
|
16432
|
+
});
|
|
16433
|
+
return { rules, lookup, file: null, lineOffset: 0, sources, exact: true };
|
|
16434
|
+
}
|
|
16435
|
+
if (sheet.devId) {
|
|
16436
|
+
const file2 = sourceFile(sheet.devId, dirs);
|
|
16437
|
+
if (!file2) return null;
|
|
16438
|
+
const index = /[?&]index=(\d+)/.exec(sheet.devId);
|
|
16439
|
+
const lineOffset2 = /\.(?:vue|svelte|astro)$/i.test(file2) && index ? styleBlockLine(file2, Number(index[1])) : 0;
|
|
16440
|
+
return { rules, lookup: null, file: file2, lineOffset: lineOffset2, sources: [], exact: sameLines(text, rules, file2, lineOffset2) };
|
|
16441
|
+
}
|
|
16442
|
+
const file = fileForStyleUrl(sheet.sourceURL, dirs);
|
|
16443
|
+
if (!file) return null;
|
|
16444
|
+
const lineOffset = sheet.isInline ? sheet.startLine : 0;
|
|
16445
|
+
return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
|
|
16446
|
+
}
|
|
16447
|
+
function cachedResolution(sheet, text, dirs, fetchMap) {
|
|
16448
|
+
const version = crypto8.createHash("sha1").update(text).digest("hex");
|
|
16449
|
+
const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
|
|
16450
|
+
" "
|
|
16451
|
+
);
|
|
16452
|
+
let pending = resolutions.get(key);
|
|
16453
|
+
if (!pending) {
|
|
16454
|
+
pending = resolveSheet(sheet, text, dirs, fetchMap).catch(() => null);
|
|
16455
|
+
resolutions.set(key, pending);
|
|
16456
|
+
if (resolutions.size > MAX_CACHED_SHEETS) resolutions.delete(resolutions.keys().next().value);
|
|
16457
|
+
}
|
|
16458
|
+
return pending;
|
|
16459
|
+
}
|
|
16460
|
+
function add(into, file, ranges, repoRoot, fileLevel = false) {
|
|
16461
|
+
if (ranges.length === 0 && !fileLevel) return;
|
|
16462
|
+
const key = relativeToRepo(file, repoRoot);
|
|
16463
|
+
into.set(key, [...into.get(key) ?? [], ...ranges]);
|
|
16464
|
+
}
|
|
16465
|
+
async function resolveCssCoverage(sheets, roots, fetchMap) {
|
|
16466
|
+
const reached = /* @__PURE__ */ new Map();
|
|
16467
|
+
const known = /* @__PURE__ */ new Map();
|
|
16468
|
+
for (const sheet of sheets) {
|
|
16469
|
+
if (sheet.text === void 0) continue;
|
|
16470
|
+
const dirs = scriptRoots(roots, sheet.sourceURL, sheet.pageUrl);
|
|
16471
|
+
const resolution = await cachedResolution(sheet, sheet.text, dirs, fetchMap);
|
|
16472
|
+
if (!resolution) continue;
|
|
16473
|
+
const used = usedCssRules(resolution.rules, [...sheet.used.values()]);
|
|
16474
|
+
if (!resolution.exact) {
|
|
16475
|
+
if (resolution.file && used.length > 0) add(reached, resolution.file, [], roots.repoRoot, true);
|
|
16476
|
+
continue;
|
|
16477
|
+
}
|
|
16478
|
+
const options = { lookup: resolution.lookup, lineOffset: resolution.lineOffset };
|
|
16479
|
+
const fileOf2 = (source) => source === -1 ? resolution.file : resolution.sources[source] ?? null;
|
|
16480
|
+
const matched = cssRuleLines(sheet.text, used, options);
|
|
16481
|
+
for (const [source, ranges] of matched) {
|
|
16482
|
+
const file = fileOf2(source);
|
|
16483
|
+
if (file) add(reached, file, ranges, roots.repoRoot);
|
|
16484
|
+
}
|
|
16485
|
+
for (const [source, ranges] of cssRuleLines(sheet.text, allCssRules(resolution.rules), options)) {
|
|
16486
|
+
const file = fileOf2(source);
|
|
16487
|
+
if (file) add(known, file, ranges, roots.repoRoot);
|
|
16488
|
+
}
|
|
16489
|
+
}
|
|
16490
|
+
return { reached, known };
|
|
16491
|
+
}
|
|
16492
|
+
|
|
14729
16493
|
// src/internal/capture/page-interaction.ts
|
|
14730
16494
|
function readPageInteraction() {
|
|
14731
16495
|
const doc = globalThis.document;
|
|
@@ -15024,21 +16788,29 @@ function createSink() {
|
|
|
15024
16788
|
locatorCalls: /* @__PURE__ */ new Map(),
|
|
15025
16789
|
stops: [],
|
|
15026
16790
|
shownStops: /* @__PURE__ */ new Map(),
|
|
16791
|
+
failureHeld: false,
|
|
16792
|
+
frozenAt: null,
|
|
16793
|
+
stopEnds: 0,
|
|
16794
|
+
ending: false,
|
|
15027
16795
|
released: [],
|
|
15028
16796
|
releasedRequests: /* @__PURE__ */ new Set(),
|
|
15029
16797
|
onReleasedRequestsEnded: null,
|
|
15030
16798
|
releasedQuietSince: 0,
|
|
16799
|
+
releasedUntil: null,
|
|
15031
16800
|
locatorPages: new LocatorPageLog(),
|
|
15032
|
-
codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
|
|
16801
|
+
codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
|
|
15033
16802
|
serverCoverage: /* @__PURE__ */ new Map(),
|
|
15034
16803
|
baseURL: null
|
|
15035
16804
|
};
|
|
15036
16805
|
}
|
|
15037
16806
|
var MAX_CODE_REACH_PAGES = 16;
|
|
15038
16807
|
var CODE_REACH_START_WAIT_MS = 2e3;
|
|
16808
|
+
function pageCoverageInWorker() {
|
|
16809
|
+
return browserCoverageInWorker() || cssCoverageInWorker();
|
|
16810
|
+
}
|
|
15039
16811
|
function maybeStartCodeReach(page, alreadyOpen = false) {
|
|
15040
16812
|
const sink = currentSink;
|
|
15041
|
-
if (!sink || !
|
|
16813
|
+
if (!sink || !pageCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
|
|
15042
16814
|
const known = sink.codeReach.sessions.get(page);
|
|
15043
16815
|
if (known) return known.started;
|
|
15044
16816
|
if (alreadyOpen) {
|
|
@@ -15051,7 +16823,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
|
|
|
15051
16823
|
return started;
|
|
15052
16824
|
}
|
|
15053
16825
|
async function startCodeReachOnOpenPages() {
|
|
15054
|
-
if (!
|
|
16826
|
+
if (!pageCoverageInWorker() || OPEN_PAGES.size === 0) return;
|
|
15055
16827
|
const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
|
|
15056
16828
|
await Promise.race([started, new Promise((resolve11) => setTimeout(resolve11, CODE_REACH_START_WAIT_MS).unref())]);
|
|
15057
16829
|
}
|
|
@@ -15094,22 +16866,28 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
15094
16866
|
async function stopPageCodeReach(sink, page, session, roots) {
|
|
15095
16867
|
try {
|
|
15096
16868
|
if (!await session.started || isPageClosed(page)) return;
|
|
15097
|
-
const
|
|
15098
|
-
|
|
15099
|
-
const
|
|
16869
|
+
const { scripts, sheets } = await stopPageCoverage(page);
|
|
16870
|
+
const fetchMap = pageMapFetcher(page);
|
|
16871
|
+
const append = (into, from) => {
|
|
16872
|
+
for (const [file, ranges] of from) into.set(file, [...into.get(file) ?? [], ...ranges]);
|
|
16873
|
+
};
|
|
16874
|
+
if (sheets.length > 0) {
|
|
16875
|
+
const rules = await resolveCssCoverage(sheets, roots, fetchMap);
|
|
16876
|
+
append(sink.codeReach.cssFiles, rules.reached);
|
|
16877
|
+
append(sink.codeReach.cssKnown, rules.known);
|
|
16878
|
+
}
|
|
16879
|
+
if (!scripts) return;
|
|
16880
|
+
const { reached, known } = await resolveCodeReachFunctions(scripts, roots, fetchMap, page.url(), {
|
|
15100
16881
|
known: browserFunctionsInWorker()
|
|
15101
16882
|
});
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
}
|
|
15105
|
-
for (const [file, functions] of known) {
|
|
15106
|
-
sink.codeReach.known.set(file, [...sink.codeReach.known.get(file) ?? [], ...functions]);
|
|
15107
|
-
}
|
|
16883
|
+
append(sink.codeReach.files, reached);
|
|
16884
|
+
append(sink.codeReach.known, known);
|
|
15108
16885
|
} catch {
|
|
15109
16886
|
}
|
|
15110
16887
|
}
|
|
15111
16888
|
var currentSink = null;
|
|
15112
16889
|
var KNOWN_FUNCTIONS_SENT = /* @__PURE__ */ new Map();
|
|
16890
|
+
var KNOWN_CSS_RULES_SENT = /* @__PURE__ */ new Map();
|
|
15113
16891
|
var PENDING_PROBES = /* @__PURE__ */ new Set();
|
|
15114
16892
|
async function drainPendingProbes(capMs) {
|
|
15115
16893
|
if (PENDING_PROBES.size === 0) return;
|
|
@@ -15122,6 +16900,25 @@ async function drainPendingProbes(capMs) {
|
|
|
15122
16900
|
]);
|
|
15123
16901
|
clearTimeout(cap);
|
|
15124
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
|
+
}
|
|
15125
16922
|
function isPageClosed(page) {
|
|
15126
16923
|
try {
|
|
15127
16924
|
return typeof page.isClosed === "function" ? page.isClosed() : false;
|
|
@@ -15244,7 +17041,7 @@ function buildPageState(raw, cookies, interaction) {
|
|
|
15244
17041
|
}
|
|
15245
17042
|
async function readPageState(page) {
|
|
15246
17043
|
try {
|
|
15247
|
-
const
|
|
17044
|
+
const readRaw = page.evaluate(() => {
|
|
15248
17045
|
const listStorage = (s) => {
|
|
15249
17046
|
const out = [];
|
|
15250
17047
|
try {
|
|
@@ -15270,14 +17067,16 @@ async function readPageState(page) {
|
|
|
15270
17067
|
sessionStorage: listStorage(globalThis.sessionStorage)
|
|
15271
17068
|
};
|
|
15272
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]);
|
|
15273
17079
|
if (!raw) return null;
|
|
15274
|
-
let cookies = null;
|
|
15275
|
-
try {
|
|
15276
|
-
cookies = await pageContext(page)?.cookies() ?? null;
|
|
15277
|
-
} catch {
|
|
15278
|
-
cookies = null;
|
|
15279
|
-
}
|
|
15280
|
-
const interaction = await page.evaluate(readPageInteraction).catch(() => null);
|
|
15281
17080
|
return buildPageState(raw, cookies, interaction);
|
|
15282
17081
|
} catch {
|
|
15283
17082
|
return null;
|
|
@@ -15341,7 +17140,7 @@ async function recordPageWindow(sink, page) {
|
|
|
15341
17140
|
}
|
|
15342
17141
|
async function stashPageState(sink, closing) {
|
|
15343
17142
|
const page = sink.lastActivePage;
|
|
15344
|
-
if (!page || isPageClosed(page)) return;
|
|
17143
|
+
if (!page || isPageClosed(page) || sink.frozenAt !== null) return;
|
|
15345
17144
|
const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
|
|
15346
17145
|
if (!belongsToClosing) return;
|
|
15347
17146
|
await internalCall(page, () => readPageBeforeClose(sink, page));
|
|
@@ -15353,13 +17152,12 @@ async function readBlankPage(page) {
|
|
|
15353
17152
|
return aria != null && aria.trim() === "";
|
|
15354
17153
|
}
|
|
15355
17154
|
async function readPageBeforeClose(sink, page) {
|
|
15356
|
-
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]);
|
|
15357
17158
|
if (vitals) sink.stashedWebVitals = vitals;
|
|
15358
|
-
if (
|
|
15359
|
-
if (
|
|
15360
|
-
const pageState = await readPageState(page);
|
|
15361
|
-
if (pageState) sink.stashedPageState = pageState;
|
|
15362
|
-
}
|
|
17159
|
+
if (blankPage !== null) sink.probeBlankPage = blankPage;
|
|
17160
|
+
if (pageState) sink.stashedPageState = pageState;
|
|
15363
17161
|
const status = sink.testInfo?.status;
|
|
15364
17162
|
const sampleAria = async () => {
|
|
15365
17163
|
const root = page.locator(":root");
|
|
@@ -15553,16 +17351,35 @@ function locatorSource(target, method, args) {
|
|
|
15553
17351
|
return renderFailing({ method, args });
|
|
15554
17352
|
}
|
|
15555
17353
|
var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
|
|
17354
|
+
var CLOSING_CONTEXTS = /* @__PURE__ */ new WeakSet();
|
|
17355
|
+
var CLOSING_PAGES = /* @__PURE__ */ new WeakMap();
|
|
15556
17356
|
var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
|
|
15557
|
-
|
|
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) {
|
|
15558
17374
|
const state = currentSink?.pause;
|
|
15559
|
-
if (!state || state
|
|
17375
|
+
if (!state || !wantsStops(state)) return;
|
|
15560
17376
|
await Promise.all(
|
|
15561
17377
|
[...contexts].map(
|
|
15562
17378
|
(context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
|
|
15563
17379
|
)
|
|
15564
17380
|
);
|
|
15565
17381
|
}
|
|
17382
|
+
var stopTurn = Promise.resolve();
|
|
15566
17383
|
function onDebuggerStop(context) {
|
|
15567
17384
|
const details = contextDebugger(context)?.pausedDetails();
|
|
15568
17385
|
if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
|
|
@@ -15573,13 +17390,38 @@ function onDebuggerStop(context) {
|
|
|
15573
17390
|
try {
|
|
15574
17391
|
if (!sink || !state) return;
|
|
15575
17392
|
const callSite = stopSite(sink, details.location);
|
|
17393
|
+
const call = callSite ? sink.locatorCalls.get(callSiteLine(callSite)) : void 0;
|
|
15576
17394
|
const pagePause = PAGE_PAUSES.get(context);
|
|
17395
|
+
const asked = askedOnce(state);
|
|
15577
17396
|
const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
|
|
15578
17397
|
if ("go" in decided) {
|
|
15579
17398
|
how = decided.go;
|
|
15580
17399
|
return;
|
|
15581
17400
|
}
|
|
15582
|
-
|
|
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);
|
|
15583
17425
|
} catch {
|
|
15584
17426
|
if (state) how = goOn(state);
|
|
15585
17427
|
} finally {
|
|
@@ -15587,22 +17429,30 @@ function onDebuggerStop(context) {
|
|
|
15587
17429
|
}
|
|
15588
17430
|
})();
|
|
15589
17431
|
}
|
|
17432
|
+
function endedWaiting(sink, call) {
|
|
17433
|
+
releaseCall(sink, call?.settled ?? Promise.resolve());
|
|
17434
|
+
return "finish";
|
|
17435
|
+
}
|
|
15590
17436
|
function stopSite(sink, location) {
|
|
15591
17437
|
if (path30.dirname(path30.resolve(location.file)) !== __dirname) return stopCallSite(location);
|
|
15592
17438
|
return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
|
|
15593
17439
|
}
|
|
15594
|
-
function
|
|
15595
|
-
|
|
15596
|
-
|
|
15597
|
-
|
|
15598
|
-
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);
|
|
15599
17445
|
return open[open.length - 1] ?? null;
|
|
15600
17446
|
}
|
|
15601
|
-
async function showStop(sink, context, title, location, reason) {
|
|
17447
|
+
async function showStop(sink, context, title, location, reason, call, of, failure = null) {
|
|
15602
17448
|
const testInfo = sink.testInfo;
|
|
15603
|
-
const
|
|
15604
|
-
const
|
|
15605
|
-
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;
|
|
15606
17456
|
const onPick = async (picked) => {
|
|
15607
17457
|
if (call) {
|
|
15608
17458
|
call.picked = true;
|
|
@@ -15613,16 +17463,57 @@ async function showStop(sink, context, title, location, reason) {
|
|
|
15613
17463
|
...picked
|
|
15614
17464
|
});
|
|
15615
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);
|
|
15616
17473
|
}
|
|
15617
17474
|
console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
|
|
15618
|
-
await postPickToEditor(
|
|
17475
|
+
await postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location));
|
|
15619
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;
|
|
15620
17480
|
const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
|
|
15621
17481
|
sink.stops.push(record);
|
|
15622
17482
|
const end = new AbortController();
|
|
15623
|
-
const
|
|
15624
|
-
|
|
15625
|
-
|
|
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(
|
|
15626
17517
|
page,
|
|
15627
17518
|
() => pauseHere(
|
|
15628
17519
|
page,
|
|
@@ -15630,45 +17521,221 @@ async function showStop(sink, context, title, location, reason) {
|
|
|
15630
17521
|
location,
|
|
15631
17522
|
call: title,
|
|
15632
17523
|
attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
|
|
15633
|
-
target: call?.page === page ? call.target : null
|
|
17524
|
+
target: call?.page === page ? call.target : null,
|
|
17525
|
+
...failure ? { failure: true } : {}
|
|
15634
17526
|
},
|
|
15635
17527
|
{ fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
|
|
15636
17528
|
onPick,
|
|
15637
|
-
|
|
15638
|
-
|
|
17529
|
+
onRecord,
|
|
17530
|
+
over.signal,
|
|
17531
|
+
intents ?? void 0,
|
|
17532
|
+
onIntent
|
|
15639
17533
|
)
|
|
15640
|
-
)
|
|
15641
|
-
).finally(() => {
|
|
17534
|
+
);
|
|
17535
|
+
}).finally(async () => {
|
|
15642
17536
|
if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
|
|
15643
17537
|
else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
|
|
15644
17538
|
sink.shownStops.delete(end);
|
|
17539
|
+
await held2?.release();
|
|
15645
17540
|
});
|
|
15646
17541
|
const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
|
|
15647
|
-
|
|
15648
|
-
|
|
15649
|
-
|
|
15650
|
-
|
|
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
|
+
});
|
|
15651
17710
|
}
|
|
15652
17711
|
var END_STOP_MS = 2e3;
|
|
15653
17712
|
var RELEASED_CALL_MS = 2e3;
|
|
15654
17713
|
var RELEASED_QUIET_MS = 200;
|
|
15655
17714
|
async function endShownStops(sink, why, closing) {
|
|
15656
17715
|
const ending = [];
|
|
17716
|
+
if (why === "ended") sink.stopEnds++;
|
|
15657
17717
|
let heldAt = Infinity;
|
|
15658
|
-
for (const [end,
|
|
15659
|
-
if (closing &&
|
|
17718
|
+
for (const [end, stop] of sink.shownStops) {
|
|
17719
|
+
if (closing && !closesStop(closing, stop)) continue;
|
|
15660
17720
|
if (!end.signal.aborted) {
|
|
15661
|
-
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;
|
|
15662
17723
|
end.abort(why);
|
|
15663
|
-
if (why === "ended" &&
|
|
17724
|
+
if (why === "ended" && stop.heldAt !== null) heldAt = Math.min(heldAt, stop.heldAt);
|
|
15664
17725
|
}
|
|
15665
|
-
ending.push(done);
|
|
17726
|
+
ending.push(stop.done);
|
|
15666
17727
|
}
|
|
15667
17728
|
if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
|
|
15668
17729
|
await settledWithin(ending, END_STOP_MS);
|
|
15669
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
|
+
}
|
|
15670
17736
|
function releaseCall(sink, settled) {
|
|
15671
17737
|
sink.releasedQuietSince = Date.now();
|
|
17738
|
+
sink.releasedUntil = null;
|
|
15672
17739
|
sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
|
|
15673
17740
|
}
|
|
15674
17741
|
function releasedRequestsEnded(sink) {
|
|
@@ -15677,8 +17744,8 @@ function releasedRequestsEnded(sink) {
|
|
|
15677
17744
|
}
|
|
15678
17745
|
async function awaitReleasedCalls(sink) {
|
|
15679
17746
|
if (sink.released.length === 0) return;
|
|
15680
|
-
const deadline = Date.now() + RELEASED_CALL_MS;
|
|
15681
|
-
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()));
|
|
15682
17749
|
for (let now = Date.now(); now < deadline; now = Date.now()) {
|
|
15683
17750
|
if (sink.releasedRequests.size > 0) {
|
|
15684
17751
|
await settledWithin([releasedRequestsEnded(sink)], deadline - now);
|
|
@@ -15708,7 +17775,7 @@ function recorderAt(page, testInfo, location, reason) {
|
|
|
15708
17775
|
return async (ended) => {
|
|
15709
17776
|
console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
|
|
15710
17777
|
const why = await recordFromHere({ page, testInfo, location, reason, ended });
|
|
15711
|
-
if (ended?.aborted) console.log("[piwi] The recording ended with the
|
|
17778
|
+
if (ended?.aborted) console.log("[piwi] The recording ended with the stop.");
|
|
15712
17779
|
else
|
|
15713
17780
|
console.log(
|
|
15714
17781
|
why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
|
|
@@ -15725,10 +17792,10 @@ async function pauseInPlace(page, playwrightPause, location) {
|
|
|
15725
17792
|
if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
|
|
15726
17793
|
return leaveToPlaywright(context, playwrightPause);
|
|
15727
17794
|
}
|
|
15728
|
-
sink.pause ??=
|
|
17795
|
+
sink.pause ??= pauseState(NO_BREAKPOINTS);
|
|
15729
17796
|
watchStops(context, onDebuggerStop);
|
|
15730
17797
|
await steerDebugger(context, "pause");
|
|
15731
|
-
PAGE_PAUSES.set(context, location);
|
|
17798
|
+
PAGE_PAUSES.set(context, { location, page });
|
|
15732
17799
|
try {
|
|
15733
17800
|
await playwrightPause({ __testHookKeepTestTimeout: true });
|
|
15734
17801
|
} finally {
|
|
@@ -15911,15 +17978,15 @@ function instrumentPage(page) {
|
|
|
15911
17978
|
if (typeof page.close === "function") {
|
|
15912
17979
|
const originalClose = page.close.bind(page);
|
|
15913
17980
|
page.close = async (...args) => {
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
await
|
|
15919
|
-
await
|
|
15920
|
-
|
|
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();
|
|
15921
17989
|
}
|
|
15922
|
-
return originalClose(...args);
|
|
15923
17990
|
};
|
|
15924
17991
|
}
|
|
15925
17992
|
const captureLocators = process.env.PIWI_CAPTURE_LOCATORS !== "false";
|
|
@@ -15964,7 +18031,7 @@ function instrumentPage(page) {
|
|
|
15964
18031
|
});
|
|
15965
18032
|
page.on("console", (msg) => {
|
|
15966
18033
|
const sink = currentSink;
|
|
15967
|
-
if (!sink) return;
|
|
18034
|
+
if (!sink || sink.frozenAt !== null) return;
|
|
15968
18035
|
const type = msg.type();
|
|
15969
18036
|
if (["warning", "error", "assert"].includes(type)) {
|
|
15970
18037
|
const location = msg.location();
|
|
@@ -15980,7 +18047,7 @@ function instrumentPage(page) {
|
|
|
15980
18047
|
try {
|
|
15981
18048
|
page.on("dialogclosed", (dialog) => {
|
|
15982
18049
|
const sink = currentSink;
|
|
15983
|
-
if (!sink) return;
|
|
18050
|
+
if (!sink || sink.frozenAt !== null) return;
|
|
15984
18051
|
try {
|
|
15985
18052
|
sink.dialogs.push({
|
|
15986
18053
|
type: typeof dialog.type === "function" ? dialog.type() : null,
|
|
@@ -15999,12 +18066,14 @@ function instrumentPage(page) {
|
|
|
15999
18066
|
if (!sink) return;
|
|
16000
18067
|
try {
|
|
16001
18068
|
const request3 = response.request();
|
|
18069
|
+
if (HELD_REQUESTS.has(request3)) return;
|
|
16002
18070
|
if (isTrackedRequest(request3.url(), request3.resourceType())) sink.openResponses.set(request3, response);
|
|
16003
18071
|
} catch {
|
|
16004
18072
|
}
|
|
16005
18073
|
});
|
|
16006
18074
|
page.on("request", (request3) => {
|
|
16007
18075
|
const sink = currentSink;
|
|
18076
|
+
if (sink && sink.frozenAt !== null) HELD_REQUESTS.add(request3);
|
|
16008
18077
|
if (!sink || sink.released.length === 0) return;
|
|
16009
18078
|
sink.releasedRequests.add(request3);
|
|
16010
18079
|
sink.releasedQuietSince = Date.now();
|
|
@@ -16015,16 +18084,18 @@ function instrumentPage(page) {
|
|
|
16015
18084
|
sink.releasedQuietSince = Date.now();
|
|
16016
18085
|
if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
|
|
16017
18086
|
}
|
|
16018
|
-
if (!sink || RECORDED_OPEN.has(request3)) return;
|
|
18087
|
+
if (!sink || RECORDED_OPEN.has(request3) || HELD_REQUESTS.has(request3)) return;
|
|
16019
18088
|
sink.lastActivePage = page;
|
|
16020
18089
|
const response = sink.openResponses.get(request3) ?? null;
|
|
16021
18090
|
sink.openResponses.delete(request3);
|
|
16022
18091
|
recordRequest(sink, request3, failed ? "failed" : "finished", response);
|
|
18092
|
+
if (!failed) void keepResponse(request3, response);
|
|
16023
18093
|
};
|
|
16024
18094
|
page.on("requestfinished", (request3) => onRequestEnd(request3, false));
|
|
16025
18095
|
page.on("requestfailed", (request3) => onRequestEnd(request3, true));
|
|
16026
18096
|
}
|
|
16027
18097
|
var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
|
|
18098
|
+
var HELD_REQUESTS = /* @__PURE__ */ new WeakSet();
|
|
16028
18099
|
function isTrackedRequest(url, resourceType) {
|
|
16029
18100
|
if (url.startsWith("data:") || url.startsWith("blob:")) return false;
|
|
16030
18101
|
return TRACKED_REQUEST_TYPES.includes(resourceType);
|
|
@@ -16086,6 +18157,7 @@ function readResponseHeaders(sink, entry, url, headers) {
|
|
|
16086
18157
|
}
|
|
16087
18158
|
}
|
|
16088
18159
|
function recordApiCall(sink, call) {
|
|
18160
|
+
if (sink.frozenAt !== null) return;
|
|
16089
18161
|
try {
|
|
16090
18162
|
const entry = apiCallEntry(call, sink.baseURL);
|
|
16091
18163
|
if (!entry) return;
|
|
@@ -16122,7 +18194,10 @@ function instrumentContext(context) {
|
|
|
16122
18194
|
INSTRUMENTED_CONTEXTS.add(context);
|
|
16123
18195
|
if (typeof context.once === "function") {
|
|
16124
18196
|
OPEN_CONTEXTS.add(context);
|
|
16125
|
-
context.once("close", () =>
|
|
18197
|
+
context.once("close", () => {
|
|
18198
|
+
OPEN_CONTEXTS.delete(context);
|
|
18199
|
+
CLOSING_CONTEXTS.add(context);
|
|
18200
|
+
});
|
|
16126
18201
|
}
|
|
16127
18202
|
observeOwnApiRequests(context);
|
|
16128
18203
|
if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
|
|
@@ -16140,14 +18215,7 @@ function instrumentContext(context) {
|
|
|
16140
18215
|
if (typeof context.close === "function") {
|
|
16141
18216
|
const originalClose = context.close.bind(context);
|
|
16142
18217
|
context.close = async (...args) => {
|
|
16143
|
-
await
|
|
16144
|
-
const sink = currentSink;
|
|
16145
|
-
if (sink) {
|
|
16146
|
-
await endShownStops(sink, "closed", { context });
|
|
16147
|
-
await stashPageState(sink, { context });
|
|
16148
|
-
await maybeOpenPicker(sink, { context });
|
|
16149
|
-
await stopSinkCodeReach(sink, { context });
|
|
16150
|
-
}
|
|
18218
|
+
await settleContextClose(context);
|
|
16151
18219
|
return originalClose(...args);
|
|
16152
18220
|
};
|
|
16153
18221
|
}
|
|
@@ -16160,7 +18228,7 @@ function patchBrowser(browser) {
|
|
|
16160
18228
|
browser.newPage = async (...args) => {
|
|
16161
18229
|
const page = await originalNewPage(...args);
|
|
16162
18230
|
instrumentPage(page);
|
|
16163
|
-
await
|
|
18231
|
+
await armStops([pageContext(page)]);
|
|
16164
18232
|
await awaitFlakeConditions();
|
|
16165
18233
|
await maybeStartCodeReach(page);
|
|
16166
18234
|
return page;
|
|
@@ -16169,13 +18237,16 @@ function patchBrowser(browser) {
|
|
|
16169
18237
|
browser.newContext = async (...args) => {
|
|
16170
18238
|
const context = await originalNewContext(...args);
|
|
16171
18239
|
instrumentContext(context);
|
|
16172
|
-
await
|
|
18240
|
+
await armStops([context]);
|
|
16173
18241
|
return context;
|
|
16174
18242
|
};
|
|
16175
18243
|
if (typeof browser.close === "function") {
|
|
16176
18244
|
const originalClose = browser.close.bind(browser);
|
|
16177
18245
|
browser.close = async (...args) => {
|
|
16178
18246
|
await drainPendingProbes(1e3);
|
|
18247
|
+
for (const context of typeof browser.contexts === "function" ? browser.contexts() : []) {
|
|
18248
|
+
await settleContextClose(context);
|
|
18249
|
+
}
|
|
16179
18250
|
return originalClose(...args);
|
|
16180
18251
|
};
|
|
16181
18252
|
}
|
|
@@ -16228,7 +18299,11 @@ async function flushSink(sink, testInfo) {
|
|
|
16228
18299
|
clearTimeout(drainDeadline);
|
|
16229
18300
|
const page = sink.lastActivePage;
|
|
16230
18301
|
const pageReadable = page !== null && !isPageClosed(page);
|
|
16231
|
-
if (pageReadable)
|
|
18302
|
+
if (pageReadable) {
|
|
18303
|
+
await holdFailure(sink);
|
|
18304
|
+
await maybeOpenPicker(sink);
|
|
18305
|
+
}
|
|
18306
|
+
const live = pageReadable && sink.frozenAt === null;
|
|
16232
18307
|
if (sink.stops.length > 0) {
|
|
16233
18308
|
await testInfo.attach(ATTACHMENT_NAMES.pauses, {
|
|
16234
18309
|
contentType: "application/json",
|
|
@@ -16265,6 +18340,21 @@ async function flushSink(sink, testInfo) {
|
|
|
16265
18340
|
body: Buffer.from(JSON.stringify(part))
|
|
16266
18341
|
});
|
|
16267
18342
|
}
|
|
18343
|
+
if (cssCoverageInWorker()) {
|
|
18344
|
+
const rules = sink.codeReach.cssFiles.size > 0 ? browserFunctionCoverage(sink.codeReach.cssFiles) : null;
|
|
18345
|
+
if (rules) {
|
|
18346
|
+
await testInfo.attach(ATTACHMENT_NAMES.cssCoverage, {
|
|
18347
|
+
contentType: "application/json",
|
|
18348
|
+
body: Buffer.from(JSON.stringify(rules))
|
|
18349
|
+
});
|
|
18350
|
+
}
|
|
18351
|
+
for (const part of newKnownFunctions(sink.codeReach.cssKnown, KNOWN_CSS_RULES_SENT)) {
|
|
18352
|
+
await testInfo.attach(ATTACHMENT_NAMES.cssRules, {
|
|
18353
|
+
contentType: "application/json",
|
|
18354
|
+
body: Buffer.from(JSON.stringify(part))
|
|
18355
|
+
});
|
|
18356
|
+
}
|
|
18357
|
+
}
|
|
16268
18358
|
if (sink.serverCoverage.size > 0) {
|
|
16269
18359
|
const requests = {};
|
|
16270
18360
|
for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
|
|
@@ -16281,13 +18371,13 @@ async function flushSink(sink, testInfo) {
|
|
|
16281
18371
|
}
|
|
16282
18372
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
|
|
16283
18373
|
try {
|
|
16284
|
-
const snapshot = (
|
|
18374
|
+
const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
16285
18375
|
if (snapshot) {
|
|
16286
18376
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
16287
|
-
contentType:
|
|
18377
|
+
contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
|
|
16288
18378
|
body: snapshot
|
|
16289
18379
|
});
|
|
16290
|
-
const snapshotJson = (
|
|
18380
|
+
const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
16291
18381
|
if (snapshotJson) {
|
|
16292
18382
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
16293
18383
|
contentType: "application/json",
|
|
@@ -16312,7 +18402,7 @@ async function flushSink(sink, testInfo) {
|
|
|
16312
18402
|
}
|
|
16313
18403
|
if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
|
|
16314
18404
|
const option = sink.failureScreenshot;
|
|
16315
|
-
const shot = (
|
|
18405
|
+
const shot = (live ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
|
|
16316
18406
|
if (shot) {
|
|
16317
18407
|
try {
|
|
16318
18408
|
await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
|
|
@@ -16322,13 +18412,13 @@ async function flushSink(sink, testInfo) {
|
|
|
16322
18412
|
}
|
|
16323
18413
|
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
16324
18414
|
try {
|
|
16325
|
-
const snapshot = (
|
|
18415
|
+
const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
16326
18416
|
if (snapshot) {
|
|
16327
18417
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
16328
|
-
contentType:
|
|
18418
|
+
contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
|
|
16329
18419
|
body: snapshot
|
|
16330
18420
|
});
|
|
16331
|
-
const snapshotJson = (
|
|
18421
|
+
const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
|
|
16332
18422
|
if (snapshotJson) {
|
|
16333
18423
|
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
16334
18424
|
contentType: "application/json",
|
|
@@ -16374,7 +18464,7 @@ async function flushSink(sink, testInfo) {
|
|
|
16374
18464
|
body: Buffer.from(JSON.stringify(sink.networkRequests))
|
|
16375
18465
|
});
|
|
16376
18466
|
}
|
|
16377
|
-
const webVitals = (
|
|
18467
|
+
const webVitals = (live ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
|
|
16378
18468
|
if (webVitals) {
|
|
16379
18469
|
await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
|
|
16380
18470
|
contentType: "application/json",
|
|
@@ -16382,7 +18472,7 @@ async function flushSink(sink, testInfo) {
|
|
|
16382
18472
|
});
|
|
16383
18473
|
}
|
|
16384
18474
|
if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
|
|
16385
|
-
const pageState = (
|
|
18475
|
+
const pageState = (live ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
|
|
16386
18476
|
if (pageState) {
|
|
16387
18477
|
await testInfo.attach(ATTACHMENT_NAMES.pageState, {
|
|
16388
18478
|
contentType: "application/json",
|
|
@@ -16391,7 +18481,7 @@ async function flushSink(sink, testInfo) {
|
|
|
16391
18481
|
}
|
|
16392
18482
|
}
|
|
16393
18483
|
if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
|
|
16394
|
-
if (
|
|
18484
|
+
if (live) await recordPageWindow(sink, page);
|
|
16395
18485
|
const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
|
|
16396
18486
|
if (windows.length > 0) {
|
|
16397
18487
|
await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
|
|
@@ -16401,21 +18491,79 @@ async function flushSink(sink, testInfo) {
|
|
|
16401
18491
|
}
|
|
16402
18492
|
}
|
|
16403
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
|
+
}
|
|
16404
18548
|
var piwiFixtures = {
|
|
16405
18549
|
// Auto, worker-scoped: the worker's resource ledger, and the API request
|
|
16406
18550
|
// contexts its tests create, handed out behind the call proxy. Set up before
|
|
16407
18551
|
// any hook (worker fixtures come first), so a `beforeAll` context is in the
|
|
16408
18552
|
// ledger and records the calls the tests make through it, and torn down after
|
|
16409
18553
|
// the browser closed, so its census sees what only the worker's shutdown
|
|
16410
|
-
// 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.
|
|
16411
18557
|
piwiResources: [
|
|
16412
18558
|
async ({ playwright }, use, workerInfo) => {
|
|
16413
18559
|
boxCaptureFrames();
|
|
16414
18560
|
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
16415
18561
|
observeApiRequests(playwright, apiCallRecorder);
|
|
18562
|
+
await openRunChannel(workerInfo, runControl);
|
|
16416
18563
|
try {
|
|
16417
18564
|
await use();
|
|
16418
18565
|
} finally {
|
|
18566
|
+
closeRunChannel();
|
|
16419
18567
|
stopResourceLedger();
|
|
16420
18568
|
}
|
|
16421
18569
|
},
|
|
@@ -16435,10 +18583,12 @@ var piwiFixtures = {
|
|
|
16435
18583
|
// never double-wraps a page the browser patch already instrumented.
|
|
16436
18584
|
page: async ({ page }, use) => {
|
|
16437
18585
|
instrumentPage(page);
|
|
16438
|
-
await
|
|
18586
|
+
await armStops([pageContext(page)]);
|
|
16439
18587
|
await awaitFlakeConditions();
|
|
16440
18588
|
await maybeStartCodeReach(page);
|
|
16441
18589
|
await use(page);
|
|
18590
|
+
const sink = currentSink;
|
|
18591
|
+
if (sink) await holdFailure(sink, page);
|
|
16442
18592
|
},
|
|
16443
18593
|
// Auto, test-scoped: open a capture sink for the running test and flush it
|
|
16444
18594
|
// (attach the collected data) at teardown. Runs for every test without being
|
|
@@ -16453,12 +18603,16 @@ var piwiFixtures = {
|
|
|
16453
18603
|
sink.baseURL = baseURL ?? null;
|
|
16454
18604
|
if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
|
|
16455
18605
|
const pauseSet = currentPauseSet();
|
|
16456
|
-
|
|
16457
|
-
|
|
16458
|
-
|
|
16459
|
-
|
|
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);
|
|
16460
18611
|
}
|
|
18612
|
+
pauseNextTest = false;
|
|
16461
18613
|
if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
|
|
18614
|
+
if (channel) tellHub(testBegin(testInfo, cannotPause));
|
|
18615
|
+
if (channel && sink.pause) startKeeping();
|
|
16462
18616
|
if (isFlakeMode()) {
|
|
16463
18617
|
const plan = loadFlakePlan();
|
|
16464
18618
|
const role = flakeRoleForTest(plan, {
|
|
@@ -16482,14 +18636,18 @@ var piwiFixtures = {
|
|
|
16482
18636
|
});
|
|
16483
18637
|
activeResourceLedger()?.testStarted(testInfo);
|
|
16484
18638
|
currentSink = sink;
|
|
18639
|
+
const showRunChannel = hideRunChannel();
|
|
16485
18640
|
try {
|
|
16486
|
-
await
|
|
18641
|
+
await armStops(OPEN_CONTEXTS);
|
|
16487
18642
|
await startCodeReachOnOpenPages();
|
|
16488
18643
|
await use();
|
|
16489
18644
|
} finally {
|
|
18645
|
+
sink.ending = true;
|
|
16490
18646
|
await endShownStops(sink, "ended");
|
|
16491
18647
|
await awaitReleasedCalls(sink);
|
|
18648
|
+
showRunChannel();
|
|
16492
18649
|
currentSink = null;
|
|
18650
|
+
if (sink.pause?.pause) pauseNextTest = true;
|
|
16493
18651
|
if (sink.probeItem) {
|
|
16494
18652
|
const applied = sink.probeInterception?.applied() ?? false;
|
|
16495
18653
|
const level = sink.probeItem.level ?? "client";
|
|
@@ -16512,7 +18670,12 @@ var piwiFixtures = {
|
|
|
16512
18670
|
});
|
|
16513
18671
|
}
|
|
16514
18672
|
if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
|
|
16515
|
-
|
|
18673
|
+
try {
|
|
18674
|
+
await flushSink(sink, testInfo);
|
|
18675
|
+
} finally {
|
|
18676
|
+
stopKeeping();
|
|
18677
|
+
if (runChannelOpen()) tellHub(testEnd(sink, testInfo));
|
|
18678
|
+
}
|
|
16516
18679
|
await recordResourceCensus(testInfo);
|
|
16517
18680
|
}
|
|
16518
18681
|
},
|
|
@@ -16546,12 +18709,12 @@ function extendPiwiFixtures(test) {
|
|
|
16546
18709
|
}
|
|
16547
18710
|
|
|
16548
18711
|
// src/internal/ai/ai-fixtures.ts
|
|
16549
|
-
var
|
|
18712
|
+
var fs29 = __toESM(require("fs"));
|
|
16550
18713
|
var path33 = __toESM(require("path"));
|
|
16551
18714
|
var import_test = require("@playwright/test");
|
|
16552
18715
|
|
|
16553
18716
|
// src/internal/ai/artifact.ts
|
|
16554
|
-
var
|
|
18717
|
+
var fs28 = __toESM(require("fs"));
|
|
16555
18718
|
var path31 = __toESM(require("path"));
|
|
16556
18719
|
var ARTIFACT_VERSION = 1;
|
|
16557
18720
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
@@ -16645,7 +18808,7 @@ function parseEntry(text) {
|
|
|
16645
18808
|
function readEntry(file) {
|
|
16646
18809
|
let text;
|
|
16647
18810
|
try {
|
|
16648
|
-
text =
|
|
18811
|
+
text = fs28.readFileSync(file, "utf8");
|
|
16649
18812
|
} catch (error) {
|
|
16650
18813
|
if (error.code === "ENOENT") return null;
|
|
16651
18814
|
throw error;
|
|
@@ -16655,28 +18818,28 @@ function readEntry(file) {
|
|
|
16655
18818
|
function writeEntry(file, entry) {
|
|
16656
18819
|
const canonical = serializeEntry(entry);
|
|
16657
18820
|
return withEntryLock(file, () => {
|
|
16658
|
-
if (
|
|
16659
|
-
|
|
18821
|
+
if (fs28.existsSync(file) && fs28.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
18822
|
+
fs28.mkdirSync(path31.dirname(file), { recursive: true });
|
|
16660
18823
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
16661
|
-
|
|
16662
|
-
|
|
18824
|
+
fs28.writeFileSync(tmp, canonical);
|
|
18825
|
+
fs28.renameSync(tmp, file);
|
|
16663
18826
|
return { written: true };
|
|
16664
18827
|
});
|
|
16665
18828
|
}
|
|
16666
18829
|
function withEntryLock(file, fn) {
|
|
16667
18830
|
const lock = `${file}.lock`;
|
|
16668
|
-
|
|
18831
|
+
fs28.mkdirSync(path31.dirname(file), { recursive: true });
|
|
16669
18832
|
const deadline = Date.now() + 5e3;
|
|
16670
18833
|
for (; ; ) {
|
|
16671
18834
|
try {
|
|
16672
|
-
const fd =
|
|
16673
|
-
|
|
18835
|
+
const fd = fs28.openSync(lock, "wx");
|
|
18836
|
+
fs28.closeSync(fd);
|
|
16674
18837
|
break;
|
|
16675
18838
|
} catch (error) {
|
|
16676
18839
|
if (error.code !== "EEXIST") throw error;
|
|
16677
18840
|
if (Date.now() > deadline) {
|
|
16678
18841
|
try {
|
|
16679
|
-
|
|
18842
|
+
fs28.unlinkSync(lock);
|
|
16680
18843
|
} catch {
|
|
16681
18844
|
}
|
|
16682
18845
|
}
|
|
@@ -16686,14 +18849,14 @@ function withEntryLock(file, fn) {
|
|
|
16686
18849
|
return fn();
|
|
16687
18850
|
} finally {
|
|
16688
18851
|
try {
|
|
16689
|
-
|
|
18852
|
+
fs28.unlinkSync(lock);
|
|
16690
18853
|
} catch {
|
|
16691
18854
|
}
|
|
16692
18855
|
}
|
|
16693
18856
|
}
|
|
16694
18857
|
|
|
16695
18858
|
// src/internal/ai/keys.ts
|
|
16696
|
-
var
|
|
18859
|
+
var crypto9 = __toESM(require("crypto"));
|
|
16697
18860
|
var path32 = __toESM(require("path"));
|
|
16698
18861
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
16699
18862
|
function normalizeTemplate(template) {
|
|
@@ -16701,7 +18864,7 @@ function normalizeTemplate(template) {
|
|
|
16701
18864
|
}
|
|
16702
18865
|
function hashTemplate(normalized, ordinal = 0) {
|
|
16703
18866
|
const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
|
|
16704
|
-
return
|
|
18867
|
+
return crypto9.createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
16705
18868
|
}
|
|
16706
18869
|
function slug(text, maxLength = 40) {
|
|
16707
18870
|
const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -17320,15 +19483,15 @@ function relativeToCwd(file) {
|
|
|
17320
19483
|
return path33.relative(process.cwd(), file).split(path33.sep).join("/");
|
|
17321
19484
|
}
|
|
17322
19485
|
function recordIntents(intents, entry) {
|
|
17323
|
-
const
|
|
19486
|
+
const add2 = (locator, kind) => {
|
|
17324
19487
|
intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
|
|
17325
19488
|
};
|
|
17326
19489
|
if (entry.kind === "locator") {
|
|
17327
|
-
|
|
19490
|
+
add2(locatorSource2(entry.locator), "locator");
|
|
17328
19491
|
return;
|
|
17329
19492
|
}
|
|
17330
|
-
for (const step of entry.steps)
|
|
17331
|
-
if (entry.postcondition.locator)
|
|
19493
|
+
for (const step of entry.steps) add2(locatorSource2(step.locator), "run");
|
|
19494
|
+
if (entry.postcondition.locator) add2(locatorSource2(entry.postcondition.locator), "run");
|
|
17332
19495
|
}
|
|
17333
19496
|
function missMessage(template, testTitle, relPath) {
|
|
17334
19497
|
return [
|
|
@@ -17344,7 +19507,7 @@ function createAiApi(page, testInfo, config, used, intents) {
|
|
|
17344
19507
|
const readSource = () => {
|
|
17345
19508
|
if (source === null) {
|
|
17346
19509
|
try {
|
|
17347
|
-
source =
|
|
19510
|
+
source = fs29.readFileSync(testInfo.file, "utf8");
|
|
17348
19511
|
} catch {
|
|
17349
19512
|
source = "";
|
|
17350
19513
|
}
|