@piwitests/reporter 0.44.0 → 0.46.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 +539 -244
- package/dist/global-setup-module.js +138 -0
- package/dist/index.d.ts +116 -2
- package/dist/index.js +3124 -377
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +2 -1
- package/dist/internal/capture/capture-fixtures.d.ts +374 -5
- package/dist/internal/capture/capture-fixtures.js +1136 -113
- package/dist/internal/capture/locator-healing.js +9 -7
- package/dist/internal/capture/pick-on-failure.js +130 -37
- package/package.json +1 -1
- package/templates/skills/apply-locator-healing/SKILL.md +2 -0
- package/templates/skills/investigate-failure/SKILL.md +5 -1
- package/templates/skills/write-the-missing-test/SKILL.md +9 -2
package/dist/index.js
CHANGED
|
@@ -43,7 +43,9 @@ __export(index_exports, {
|
|
|
43
43
|
module.exports = __toCommonJS(index_exports);
|
|
44
44
|
|
|
45
45
|
// src/public/reporter.ts
|
|
46
|
-
var
|
|
46
|
+
var fs21 = __toESM(require("fs"));
|
|
47
|
+
var os9 = __toESM(require("os"));
|
|
48
|
+
var path23 = __toESM(require("path"));
|
|
47
49
|
|
|
48
50
|
// src/internal/config/desktop.ts
|
|
49
51
|
var fs = __toESM(require("fs"));
|
|
@@ -82,9 +84,12 @@ var DEFAULTS = {
|
|
|
82
84
|
// Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
|
|
83
85
|
captureCodeReach: false,
|
|
84
86
|
captureServerTraces: true,
|
|
87
|
+
captureResources: true,
|
|
88
|
+
leakCheck: "report",
|
|
85
89
|
sampleAriaOnPass: true,
|
|
86
90
|
uploadManifest: true,
|
|
87
91
|
defaultCapture: true,
|
|
92
|
+
checkBaseUrl: false,
|
|
88
93
|
streaming: true,
|
|
89
94
|
streamingBatchSize: 5,
|
|
90
95
|
streamingBatchDelay: 2e3,
|
|
@@ -122,9 +127,12 @@ var PIWI_ENV_KEYS = {
|
|
|
122
127
|
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
123
128
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
124
129
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
130
|
+
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
131
|
+
leakCheck: "PIWI_LEAK_CHECK",
|
|
125
132
|
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
126
133
|
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
127
134
|
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
135
|
+
checkBaseUrl: "PIWI_CHECK_BASE_URL",
|
|
128
136
|
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
129
137
|
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
130
138
|
outputFile: "PIWI_OUTPUT_FILE",
|
|
@@ -145,6 +153,10 @@ var PIWI_SELECTION_ENV = {
|
|
|
145
153
|
hash: "PIWI_SELECTION_HASH",
|
|
146
154
|
count: "PIWI_SELECTION_COUNT"
|
|
147
155
|
};
|
|
156
|
+
var PIWI_ORIGIN_ENV = {
|
|
157
|
+
kind: "PIWI_ORIGIN",
|
|
158
|
+
ref: "PIWI_ORIGIN_REF"
|
|
159
|
+
};
|
|
148
160
|
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
149
161
|
var PIWI_PROBE_ENV = {
|
|
150
162
|
flag: "PIWI_PROBE",
|
|
@@ -156,6 +168,7 @@ var PIWI_FLAKE_ENV = {
|
|
|
156
168
|
plan: "PIWI_FLAKE_PLAN",
|
|
157
169
|
results: "PIWI_FLAKE_RESULTS"
|
|
158
170
|
};
|
|
171
|
+
var PIWI_RESOURCES_RESULTS_ENV = "PIWI_RESOURCES_RESULTS";
|
|
159
172
|
function readBool(val) {
|
|
160
173
|
if (val === void 0) return void 0;
|
|
161
174
|
return val === "true";
|
|
@@ -184,9 +197,12 @@ var ENV_FALLBACK_SPECS = [
|
|
|
184
197
|
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
185
198
|
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
186
199
|
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
200
|
+
{ option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
|
|
201
|
+
{ option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
|
|
187
202
|
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
188
203
|
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
189
204
|
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
205
|
+
{ option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
|
|
190
206
|
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
191
207
|
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
192
208
|
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
@@ -234,6 +250,7 @@ function applyOptionsToEnv(options) {
|
|
|
234
250
|
if (options.label) env[PIWI_ENV_KEYS.label] = options.label;
|
|
235
251
|
if (options.runLabel) env[PIWI_ENV_KEYS.runLabel] = options.runLabel;
|
|
236
252
|
if (options.keep) env[PIWI_ENV_KEYS.keep] = "true";
|
|
253
|
+
if (options.checkBaseUrl !== void 0) env[PIWI_ENV_KEYS.checkBaseUrl] = String(options.checkBaseUrl);
|
|
237
254
|
if (options.captureLocators === false || options.collectPerformanceMetrics === false)
|
|
238
255
|
env[PIWI_ENV_KEYS.captureLocators] = "false";
|
|
239
256
|
else if (options.captureLocators === true) env[PIWI_ENV_KEYS.captureLocators] = "true";
|
|
@@ -250,6 +267,8 @@ function applyOptionsToEnv(options) {
|
|
|
250
267
|
if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
|
|
251
268
|
env[PIWI_ENV_KEYS.captureServerTraces] = "false";
|
|
252
269
|
else if (options.captureServerTraces === true) env[PIWI_ENV_KEYS.captureServerTraces] = "true";
|
|
270
|
+
if (options.captureResources !== void 0) env[PIWI_ENV_KEYS.captureResources] = String(options.captureResources);
|
|
271
|
+
if (options.leakCheck !== void 0) env[PIWI_ENV_KEYS.leakCheck] = options.leakCheck;
|
|
253
272
|
if (options.sampleAriaOnPass === false) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "false";
|
|
254
273
|
else if (options.sampleAriaOnPass === true) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "true";
|
|
255
274
|
if (options.inspectOnFailure !== void 0) env[PIWI_ENV_KEYS.inspectOnFailure] = String(options.inspectOnFailure);
|
|
@@ -490,7 +509,7 @@ var HttpClient = class {
|
|
|
490
509
|
}
|
|
491
510
|
/** Unified request core: transport, headers, auth, response accumulation, timeout. */
|
|
492
511
|
request(method, pathname, opts) {
|
|
493
|
-
return new Promise((
|
|
512
|
+
return new Promise((resolve7, reject) => {
|
|
494
513
|
if (this.closedReason !== null) {
|
|
495
514
|
reject(new Error(this.closedReason));
|
|
496
515
|
return;
|
|
@@ -513,7 +532,7 @@ var HttpClient = class {
|
|
|
513
532
|
data += chunk;
|
|
514
533
|
});
|
|
515
534
|
res.on("end", () => {
|
|
516
|
-
|
|
535
|
+
resolve7({ status: res.statusCode ?? 0, text: data, headers: res.headers });
|
|
517
536
|
});
|
|
518
537
|
res.on("error", reject);
|
|
519
538
|
res.on("close", () => {
|
|
@@ -608,6 +627,7 @@ function toWireTestCase(tc) {
|
|
|
608
627
|
locatorSnapshots: rest.locatorSnapshots || null,
|
|
609
628
|
locatorPages: rest.locatorPages || null,
|
|
610
629
|
codeReach: rest.codeReach || null,
|
|
630
|
+
resources: rest.resources ?? null,
|
|
611
631
|
didNotRunReason: rest.didNotRunReason ?? null,
|
|
612
632
|
expectedStatus: rest.expectedStatus ?? null,
|
|
613
633
|
blockedBy: rest.blockedBy ?? null
|
|
@@ -674,6 +694,7 @@ function serializeRun(payload, opts) {
|
|
|
674
694
|
isFullRun: payload.isFullRun ?? true,
|
|
675
695
|
filterDetails: payload.filterDetails ?? null
|
|
676
696
|
};
|
|
697
|
+
if (payload.resourceReport) body.resourceReport = payload.resourceReport;
|
|
677
698
|
if (opts.includeTestCases) {
|
|
678
699
|
body.testCases = payload.testCases.map((tc) => toWireTestCase(tc));
|
|
679
700
|
}
|
|
@@ -995,6 +1016,8 @@ var CrashRecovery = class {
|
|
|
995
1016
|
if (!data) return;
|
|
996
1017
|
this.logger.info("Found saved test data from a previous run, uploading...");
|
|
997
1018
|
delete data.instanceId;
|
|
1019
|
+
const metadata = data.metadata && typeof data.metadata === "object" ? data.metadata : {};
|
|
1020
|
+
data.metadata = { ...metadata, ingestHealth: { submitFallback: { path: "recovery" } } };
|
|
998
1021
|
try {
|
|
999
1022
|
await httpClient.postJSON("/api/test-runs/submit", data, auth);
|
|
1000
1023
|
this.logger.info("Successfully uploaded saved test data");
|
|
@@ -1056,7 +1079,8 @@ var ATTACHMENT_NAMES = {
|
|
|
1056
1079
|
pageState: "piwi-page-state",
|
|
1057
1080
|
userPick: "piwi-user-pick",
|
|
1058
1081
|
aiUsage: "piwi-ai-usage",
|
|
1059
|
-
aiMeta: "piwi-ai-meta"
|
|
1082
|
+
aiMeta: "piwi-ai-meta",
|
|
1083
|
+
resources: "piwi-resources"
|
|
1060
1084
|
};
|
|
1061
1085
|
var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
1062
1086
|
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
@@ -1277,8 +1301,8 @@ var FileHandler = class {
|
|
|
1277
1301
|
}
|
|
1278
1302
|
if (!lastPath) return null;
|
|
1279
1303
|
const hash = crypto2.createHash("sha256");
|
|
1280
|
-
await new Promise((
|
|
1281
|
-
fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end",
|
|
1304
|
+
await new Promise((resolve7, reject) => {
|
|
1305
|
+
fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve7).on("error", reject);
|
|
1282
1306
|
});
|
|
1283
1307
|
return {
|
|
1284
1308
|
tracePath: lastPath,
|
|
@@ -1308,6 +1332,35 @@ function formatMiB(bytes) {
|
|
|
1308
1332
|
|
|
1309
1333
|
// src/internal/collect/metadata-collector.ts
|
|
1310
1334
|
var import_node_child_process = require("child_process");
|
|
1335
|
+
|
|
1336
|
+
// ../core/src/wire.ts
|
|
1337
|
+
var RUN_ORIGIN_KINDS = [
|
|
1338
|
+
"ci",
|
|
1339
|
+
"ci-rerun",
|
|
1340
|
+
"local",
|
|
1341
|
+
"desktop",
|
|
1342
|
+
"editor",
|
|
1343
|
+
"preflight",
|
|
1344
|
+
"bug",
|
|
1345
|
+
"flake-lab",
|
|
1346
|
+
"probe",
|
|
1347
|
+
"bisect",
|
|
1348
|
+
"reproduce",
|
|
1349
|
+
"import"
|
|
1350
|
+
];
|
|
1351
|
+
var RUN_ORIGIN_METADATA_KEY = "piwiOrigin";
|
|
1352
|
+
var RUN_ORIGIN_REF_MAX_LENGTH = 200;
|
|
1353
|
+
var RUN_ORIGIN_REF = /^[A-Za-z0-9._:/#@-]+$/;
|
|
1354
|
+
function isRunOriginKind(value) {
|
|
1355
|
+
return typeof value === "string" && RUN_ORIGIN_KINDS.includes(value);
|
|
1356
|
+
}
|
|
1357
|
+
function parseRunOriginRef(value) {
|
|
1358
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
1359
|
+
const ref = String(value).trim();
|
|
1360
|
+
return ref && ref.length <= RUN_ORIGIN_REF_MAX_LENGTH && RUN_ORIGIN_REF.test(ref) ? ref : void 0;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/internal/collect/metadata-collector.ts
|
|
1311
1364
|
var DETACHED_HEAD = "HEAD";
|
|
1312
1365
|
function resolveScmBranch(env, gitBranch) {
|
|
1313
1366
|
const pick = (...names) => {
|
|
@@ -1352,6 +1405,19 @@ function resolveScmBaseBranch(env) {
|
|
|
1352
1405
|
const trimmed = target?.trim();
|
|
1353
1406
|
return trimmed ? normalizeRef(trimmed) : void 0;
|
|
1354
1407
|
}
|
|
1408
|
+
function detectCi(env) {
|
|
1409
|
+
return Boolean(
|
|
1410
|
+
env.JENKINS_URL || env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.TRAVIS || env.TF_BUILD || env.BITBUCKET_BUILD_NUMBER || env.CI && env.CI !== "false" && env.CI !== "0"
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
function resolveRunOrigin(env, warn) {
|
|
1414
|
+
const raw = env[PIWI_ORIGIN_ENV.kind]?.trim();
|
|
1415
|
+
let kind = detectCi(env) ? "ci" : "local";
|
|
1416
|
+
if (raw && isRunOriginKind(raw)) kind = raw;
|
|
1417
|
+
else if (raw) warn?.(`Ignoring ${PIWI_ORIGIN_ENV.kind}=${raw}: not a known run origin, recording "${kind}"`);
|
|
1418
|
+
const ref = parseRunOriginRef(env[PIWI_ORIGIN_ENV.ref]);
|
|
1419
|
+
return ref === void 0 ? { kind } : { kind, ref };
|
|
1420
|
+
}
|
|
1355
1421
|
function normalizeRef(ref) {
|
|
1356
1422
|
return ref.replace(/^refs\/heads\//, "");
|
|
1357
1423
|
}
|
|
@@ -1361,7 +1427,10 @@ var MetadataCollector = class {
|
|
|
1361
1427
|
}
|
|
1362
1428
|
/** Collect all available metadata from the environment, config, and suite */
|
|
1363
1429
|
collect(config, suite, options) {
|
|
1364
|
-
const metadata = {
|
|
1430
|
+
const metadata = {
|
|
1431
|
+
workingDir: process.cwd(),
|
|
1432
|
+
[RUN_ORIGIN_METADATA_KEY]: resolveRunOrigin(process.env, (message) => this.logger.warn(message))
|
|
1433
|
+
};
|
|
1365
1434
|
if (options.projectDescription) metadata.projectDescription = options.projectDescription;
|
|
1366
1435
|
if (options.relatedIssue) metadata.relatedIssue = options.relatedIssue;
|
|
1367
1436
|
if (options.ciInfo) metadata.ciInfo = options.ciInfo;
|
|
@@ -1538,6 +1607,15 @@ var MetadataCollector = class {
|
|
|
1538
1607
|
ci.buildUrl = `${env.SYSTEM_TEAMFOUNDATIONSERVERURI}${env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${env.BUILD_BUILDID}`;
|
|
1539
1608
|
}
|
|
1540
1609
|
ci.jobName = env.AGENT_JOBNAME;
|
|
1610
|
+
} else if (env.BITBUCKET_BUILD_NUMBER) {
|
|
1611
|
+
ci.provider = "Bitbucket Pipelines";
|
|
1612
|
+
ci.buildNumber = env.BITBUCKET_BUILD_NUMBER;
|
|
1613
|
+
ci.pipelineId = env.BITBUCKET_PIPELINE_UUID;
|
|
1614
|
+
ci.jobId = env.BITBUCKET_STEP_UUID;
|
|
1615
|
+
ci.repository = env.BITBUCKET_REPO_FULL_NAME;
|
|
1616
|
+
if (env.BITBUCKET_REPO_FULL_NAME) {
|
|
1617
|
+
ci.buildUrl = `https://bitbucket.org/${env.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${env.BITBUCKET_BUILD_NUMBER}`;
|
|
1618
|
+
}
|
|
1541
1619
|
} else if (env.CI) {
|
|
1542
1620
|
ci.provider = "Unknown CI";
|
|
1543
1621
|
ci.detected = true;
|
|
@@ -1709,10 +1787,10 @@ function createLimiter(maxConcurrent) {
|
|
|
1709
1787
|
if (run) run();
|
|
1710
1788
|
};
|
|
1711
1789
|
return function limit(fn) {
|
|
1712
|
-
return new Promise((
|
|
1790
|
+
return new Promise((resolve7, reject) => {
|
|
1713
1791
|
const run = () => {
|
|
1714
1792
|
active++;
|
|
1715
|
-
Promise.resolve().then(fn).then(
|
|
1793
|
+
Promise.resolve().then(fn).then(resolve7, reject).finally(next);
|
|
1716
1794
|
};
|
|
1717
1795
|
if (active < limitValue) run();
|
|
1718
1796
|
else queue.push(run);
|
|
@@ -2151,7 +2229,7 @@ var StreamManager = class {
|
|
|
2151
2229
|
this.logger.debugError(
|
|
2152
2230
|
`${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`
|
|
2153
2231
|
);
|
|
2154
|
-
await new Promise((
|
|
2232
|
+
await new Promise((resolve7) => setTimeout(resolve7, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
|
|
2155
2233
|
}
|
|
2156
2234
|
if (!this.pendingEvents.isEmpty) {
|
|
2157
2235
|
const remaining = this.pendingEvents.takeAll();
|
|
@@ -2208,7 +2286,7 @@ var StreamManager = class {
|
|
|
2208
2286
|
if (flush) await flush;
|
|
2209
2287
|
const delays = [0, 1e3, 3e3];
|
|
2210
2288
|
for (let attempt = 0; attempt < delays.length; attempt++) {
|
|
2211
|
-
if (delays[attempt]) await new Promise((
|
|
2289
|
+
if (delays[attempt]) await new Promise((resolve7) => setTimeout(resolve7, delays[attempt]));
|
|
2212
2290
|
try {
|
|
2213
2291
|
await this.limitLiveUpload(
|
|
2214
2292
|
() => this.uploader.uploadCaseFiles(
|
|
@@ -3686,6 +3764,131 @@ function isDueForAriaSample(testInfo) {
|
|
|
3686
3764
|
return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
|
|
3687
3765
|
}
|
|
3688
3766
|
|
|
3767
|
+
// src/internal/support/base-url-check.ts
|
|
3768
|
+
var BASE_URL_CHECK_ATTEMPTS = 3;
|
|
3769
|
+
var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
|
|
3770
|
+
var BASE_URL_CHECK_DELAY_MS = 2e3;
|
|
3771
|
+
var GATEWAY_ANSWERS = {
|
|
3772
|
+
502: "bad gateway",
|
|
3773
|
+
503: "service unavailable",
|
|
3774
|
+
504: "gateway timeout"
|
|
3775
|
+
};
|
|
3776
|
+
function cliProjectNames(argv = process.argv) {
|
|
3777
|
+
let names = null;
|
|
3778
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3779
|
+
const arg = argv[i];
|
|
3780
|
+
if (arg.startsWith("--project=")) {
|
|
3781
|
+
(names ??= []).push(arg.slice("--project=".length));
|
|
3782
|
+
} else if (arg === "--project") {
|
|
3783
|
+
names ??= [];
|
|
3784
|
+
while (i + 1 < argv.length && !argv[i + 1].startsWith("-")) names.push(argv[++i]);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
return names;
|
|
3788
|
+
}
|
|
3789
|
+
function wildcard(pattern) {
|
|
3790
|
+
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
3791
|
+
return new RegExp(`^${escaped.join(".*")}$`, "i");
|
|
3792
|
+
}
|
|
3793
|
+
function selectedProjects(projects, argv = process.argv) {
|
|
3794
|
+
const names = cliProjectNames(argv);
|
|
3795
|
+
if (!names || names.length === 0) return projects;
|
|
3796
|
+
const patterns = names.map(wildcard);
|
|
3797
|
+
const chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
|
|
3798
|
+
if (!argv.includes("--no-deps")) {
|
|
3799
|
+
const byName = new Map(projects.map((p) => [p.name ?? "", p]));
|
|
3800
|
+
const queue = [...chosen];
|
|
3801
|
+
while (queue.length > 0) {
|
|
3802
|
+
for (const dep of queue.shift().dependencies ?? []) {
|
|
3803
|
+
const project = byName.get(dep);
|
|
3804
|
+
if (project && !chosen.has(project)) {
|
|
3805
|
+
chosen.add(project);
|
|
3806
|
+
queue.push(project);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
return projects.filter((p) => chosen.has(p));
|
|
3812
|
+
}
|
|
3813
|
+
function asProxy(value) {
|
|
3814
|
+
const proxy = value;
|
|
3815
|
+
return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
|
|
3816
|
+
}
|
|
3817
|
+
function baseUrlTargets(config, argv = process.argv) {
|
|
3818
|
+
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
3819
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3820
|
+
for (const project of selectedProjects(projects, argv)) {
|
|
3821
|
+
const url = project.use?.baseURL;
|
|
3822
|
+
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
|
|
3823
|
+
const target = targets.get(url);
|
|
3824
|
+
if (target) {
|
|
3825
|
+
if (project.name) target.projects.push(project.name);
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3828
|
+
targets.set(url, {
|
|
3829
|
+
url,
|
|
3830
|
+
projects: project.name ? [project.name] : [],
|
|
3831
|
+
ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
|
|
3832
|
+
proxy: asProxy(project.use?.proxy)
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
return [...targets.values()];
|
|
3836
|
+
}
|
|
3837
|
+
function requestError(error) {
|
|
3838
|
+
const first = errorMessage(error).split("\n")[0].trim();
|
|
3839
|
+
return first.replace(/^apiRequestContext\.\w+:\s*/, "");
|
|
3840
|
+
}
|
|
3841
|
+
var playwrightProbe = async (target, timeoutMs) => {
|
|
3842
|
+
const { request } = await import("@playwright/test");
|
|
3843
|
+
const context = await request.newContext({ ignoreHTTPSErrors: target.ignoreHTTPSErrors, proxy: target.proxy });
|
|
3844
|
+
try {
|
|
3845
|
+
const response = await context.get(target.url, { timeout: timeoutMs, maxRedirects: 0, failOnStatusCode: false });
|
|
3846
|
+
return { status: response.status() };
|
|
3847
|
+
} catch (error) {
|
|
3848
|
+
return { error: requestError(error) };
|
|
3849
|
+
} finally {
|
|
3850
|
+
await context.dispose().catch(() => {
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
};
|
|
3854
|
+
async function checkBaseUrl(target, options = {}) {
|
|
3855
|
+
const attempts = Math.max(1, options.attempts ?? BASE_URL_CHECK_ATTEMPTS);
|
|
3856
|
+
const timeoutMs = options.timeoutMs ?? BASE_URL_CHECK_TIMEOUT_MS;
|
|
3857
|
+
const delayMs = options.delayMs ?? BASE_URL_CHECK_DELAY_MS;
|
|
3858
|
+
const probe = options.probe ?? playwrightProbe;
|
|
3859
|
+
let reason = "";
|
|
3860
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
3861
|
+
const outcome = await probe(target, timeoutMs);
|
|
3862
|
+
if ("status" in outcome) {
|
|
3863
|
+
const gateway = GATEWAY_ANSWERS[outcome.status];
|
|
3864
|
+
if (!gateway) return null;
|
|
3865
|
+
reason = `${outcome.status} ${gateway}`;
|
|
3866
|
+
} else {
|
|
3867
|
+
reason = outcome.error;
|
|
3868
|
+
}
|
|
3869
|
+
if (attempt < attempts) await new Promise((resolve7) => setTimeout(resolve7, delayMs));
|
|
3870
|
+
}
|
|
3871
|
+
return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
|
|
3872
|
+
}
|
|
3873
|
+
async function checkBaseUrls(config, options = {}) {
|
|
3874
|
+
const targets = baseUrlTargets(config, options.argv);
|
|
3875
|
+
const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
|
|
3876
|
+
const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
|
|
3877
|
+
if (down.length === 0) return targets;
|
|
3878
|
+
const lines = down.map(({ target, reason }) => {
|
|
3879
|
+
const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
|
|
3880
|
+
return ` ${target.url}${used}: ${reason}`;
|
|
3881
|
+
});
|
|
3882
|
+
const message = [
|
|
3883
|
+
`[Piwi Dashboard] The app under test did not answer, so no test ran:`,
|
|
3884
|
+
...lines,
|
|
3885
|
+
`Bring the environment back and run again, or set checkBaseUrl: false (PIWI_CHECK_BASE_URL=false) to run the tests anyway.`
|
|
3886
|
+
].join("\n");
|
|
3887
|
+
const error = new Error(message);
|
|
3888
|
+
error.stack = message;
|
|
3889
|
+
throw error;
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3689
3892
|
// src/internal/manifest/collect.ts
|
|
3690
3893
|
var fs12 = __toESM(require("fs"));
|
|
3691
3894
|
var path11 = __toESM(require("path"));
|
|
@@ -3785,6 +3988,10 @@ function createGlobalSetup(options, userSetup) {
|
|
|
3785
3988
|
if (userSetup) return userSetup(config);
|
|
3786
3989
|
return;
|
|
3787
3990
|
}
|
|
3991
|
+
if (opts.checkBaseUrl === true) {
|
|
3992
|
+
const checked = await checkBaseUrls(config ?? {});
|
|
3993
|
+
for (const target of checked) logger.debug(`Base URL answered: ${target.url}`);
|
|
3994
|
+
}
|
|
3788
3995
|
if (opts.enabled === false || !opts.serverUrl) {
|
|
3789
3996
|
logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
3790
3997
|
if (userSetup) return userSetup(config);
|
|
@@ -3892,7 +4099,7 @@ var path13 = __toESM(require("path"));
|
|
|
3892
4099
|
|
|
3893
4100
|
// src/internal/probe/plan.ts
|
|
3894
4101
|
function probeRunMetadata(base = {}) {
|
|
3895
|
-
return { ...base, piwiProbe: true };
|
|
4102
|
+
return { ...base, piwiProbe: true, [RUN_ORIGIN_METADATA_KEY]: { kind: "probe" } };
|
|
3896
4103
|
}
|
|
3897
4104
|
function parseRouteKey(routeKey2) {
|
|
3898
4105
|
const space = routeKey2.indexOf(" ");
|
|
@@ -3900,8 +4107,8 @@ function parseRouteKey(routeKey2) {
|
|
|
3900
4107
|
return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
|
|
3901
4108
|
}
|
|
3902
4109
|
function patternToRegex(pattern) {
|
|
3903
|
-
const
|
|
3904
|
-
const escaped =
|
|
4110
|
+
const path31 = pattern.split("?")[0] ?? pattern;
|
|
4111
|
+
const escaped = path31.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
3905
4112
|
return new RegExp(`^${escaped}$`);
|
|
3906
4113
|
}
|
|
3907
4114
|
function suitePathEqual(a, b) {
|
|
@@ -3924,13 +4131,13 @@ function matchProbeItem(plan, test) {
|
|
|
3924
4131
|
function requestMatchesRoute(method, url, routeKey2) {
|
|
3925
4132
|
const { method: rm, pattern } = parseRouteKey(routeKey2);
|
|
3926
4133
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
3927
|
-
let
|
|
4134
|
+
let path31;
|
|
3928
4135
|
try {
|
|
3929
|
-
|
|
4136
|
+
path31 = new URL(url).pathname;
|
|
3930
4137
|
} catch {
|
|
3931
|
-
|
|
4138
|
+
path31 = url.split("?")[0] ?? url;
|
|
3932
4139
|
}
|
|
3933
|
-
return patternToRegex(pattern).test(
|
|
4140
|
+
return patternToRegex(pattern).test(path31);
|
|
3934
4141
|
}
|
|
3935
4142
|
function createProbeState() {
|
|
3936
4143
|
return { navigated: false, matches: 0 };
|
|
@@ -3952,10 +4159,10 @@ function isProbeMode() {
|
|
|
3952
4159
|
function loadPlan() {
|
|
3953
4160
|
if (planCache !== void 0) return planCache;
|
|
3954
4161
|
planCache = null;
|
|
3955
|
-
const
|
|
3956
|
-
if (
|
|
4162
|
+
const path31 = process.env[PIWI_PROBE_ENV.plan];
|
|
4163
|
+
if (path31) {
|
|
3957
4164
|
try {
|
|
3958
|
-
planCache = JSON.parse(fs14.readFileSync(
|
|
4165
|
+
planCache = JSON.parse(fs14.readFileSync(path31, "utf8"));
|
|
3959
4166
|
} catch {
|
|
3960
4167
|
planCache = null;
|
|
3961
4168
|
}
|
|
@@ -3977,10 +4184,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
3977
4184
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
3978
4185
|
}
|
|
3979
4186
|
function recordProbeOutcome(line) {
|
|
3980
|
-
const
|
|
3981
|
-
if (!
|
|
4187
|
+
const path31 = process.env[PIWI_PROBE_ENV.results];
|
|
4188
|
+
if (!path31) return;
|
|
3982
4189
|
try {
|
|
3983
|
-
fs14.appendFileSync(
|
|
4190
|
+
fs14.appendFileSync(path31, `${JSON.stringify(line)}
|
|
3984
4191
|
`);
|
|
3985
4192
|
} catch {
|
|
3986
4193
|
}
|
|
@@ -4364,7 +4571,7 @@ function originOf(url) {
|
|
|
4364
4571
|
return null;
|
|
4365
4572
|
}
|
|
4366
4573
|
}
|
|
4367
|
-
var sleep = (ms) => new Promise((
|
|
4574
|
+
var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, Math.max(0, ms)));
|
|
4368
4575
|
async function performRouteAction(route, action, startedAt) {
|
|
4369
4576
|
switch (action.kind) {
|
|
4370
4577
|
case "abort":
|
|
@@ -4501,7 +4708,12 @@ function loadFlakePlan() {
|
|
|
4501
4708
|
}
|
|
4502
4709
|
}
|
|
4503
4710
|
function flakeRunMetadata(stamp, base = {}) {
|
|
4504
|
-
|
|
4711
|
+
const ref = parseRunOriginRef(stamp.experimentId);
|
|
4712
|
+
return {
|
|
4713
|
+
...base,
|
|
4714
|
+
[FLAKE_LAB_RUN_METADATA_KEY]: { experimentId: stamp.experimentId, armId: stamp.armId },
|
|
4715
|
+
[RUN_ORIGIN_METADATA_KEY]: ref === void 0 ? { kind: "flake-lab" } : { kind: "flake-lab", ref }
|
|
4716
|
+
};
|
|
4505
4717
|
}
|
|
4506
4718
|
function relativeTestFile2(file) {
|
|
4507
4719
|
return file ? path14.relative(process.cwd(), file).split(path14.sep).join("/") : null;
|
|
@@ -5061,6 +5273,15 @@ function emitGitLabDotenv(output, env, logger) {
|
|
|
5061
5273
|
logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
|
|
5062
5274
|
}
|
|
5063
5275
|
}
|
|
5276
|
+
function emitResourceSummary(lines, logger, env = process.env) {
|
|
5277
|
+
if (!env.GITHUB_ACTIONS || !env.GITHUB_STEP_SUMMARY || lines.length === 0) return;
|
|
5278
|
+
appendFileLines(
|
|
5279
|
+
env.GITHUB_STEP_SUMMARY,
|
|
5280
|
+
["### Piwi resources", "", "```text", ...lines, "```", ""],
|
|
5281
|
+
logger,
|
|
5282
|
+
"step summary"
|
|
5283
|
+
);
|
|
5284
|
+
}
|
|
5064
5285
|
function appendFileLines(file, lines, logger, label) {
|
|
5065
5286
|
try {
|
|
5066
5287
|
fs17.appendFileSync(file, lines.join("\n") + "\n");
|
|
@@ -5076,6 +5297,9 @@ function ciBuildUrlFromMetadata(metadata) {
|
|
|
5076
5297
|
|
|
5077
5298
|
// src/internal/submit/run-submitter.ts
|
|
5078
5299
|
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
5300
|
+
function markSubmitFallback(run, path31, reason) {
|
|
5301
|
+
run.metadata.ingestHealth = { submitFallback: { path: path31, reason } };
|
|
5302
|
+
}
|
|
5079
5303
|
var RunSubmitter = class {
|
|
5080
5304
|
/**
|
|
5081
5305
|
* @param httpClient HTTP client for auth resolution and the finish call.
|
|
@@ -5151,13 +5375,21 @@ var RunSubmitter = class {
|
|
|
5151
5375
|
}
|
|
5152
5376
|
if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
|
|
5153
5377
|
let outcome = { done: false, output: null };
|
|
5154
|
-
|
|
5155
|
-
|
|
5378
|
+
let fallbackReason = null;
|
|
5379
|
+
if (sm?.enabled && sm?.runId != null) {
|
|
5380
|
+
if (sm.lostResults) fallbackReason = "results-lost";
|
|
5381
|
+
else {
|
|
5382
|
+
outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
|
|
5383
|
+
if (!outcome.done) fallbackReason = "finish-failed";
|
|
5384
|
+
}
|
|
5156
5385
|
}
|
|
5157
5386
|
if (!outcome.done && !this.httpClient.closed && (this.hasReports(run) || run.options.uploadTraces)) {
|
|
5387
|
+
if (fallbackReason) markSubmitFallback(run, "upload", fallbackReason);
|
|
5158
5388
|
outcome = await this.tryUploadWithFiles(run, overallStatus, duration, auth);
|
|
5389
|
+
if (!outcome.done) fallbackReason = "upload-failed";
|
|
5159
5390
|
}
|
|
5160
5391
|
if (!outcome.done) {
|
|
5392
|
+
if (fallbackReason) markSubmitFallback(run, "submit", fallbackReason);
|
|
5161
5393
|
outcome = await this.tryUploadJSON(run, overallStatus, duration, auth);
|
|
5162
5394
|
}
|
|
5163
5395
|
if (outcome.output) {
|
|
@@ -5212,7 +5444,8 @@ var RunSubmitter = class {
|
|
|
5212
5444
|
shardIndex: run.shardInfo?.current,
|
|
5213
5445
|
shardTotal: run.shardInfo?.total,
|
|
5214
5446
|
isFullRun: run.isFullRun,
|
|
5215
|
-
filterDetails: run.filterDetails
|
|
5447
|
+
filterDetails: run.filterDetails,
|
|
5448
|
+
resourceReport: run.resourceReport ?? null
|
|
5216
5449
|
};
|
|
5217
5450
|
}
|
|
5218
5451
|
async tryFinishStreaming(run, overallStatus, duration, auth) {
|
|
@@ -5243,6 +5476,7 @@ var RunSubmitter = class {
|
|
|
5243
5476
|
isFullRun: run.isFullRun,
|
|
5244
5477
|
filterDetails: run.filterDetails ?? null
|
|
5245
5478
|
};
|
|
5479
|
+
if (run.resourceReport) finishBody.resourceReport = run.resourceReport;
|
|
5246
5480
|
if (run.shardInfo) {
|
|
5247
5481
|
finishBody.shardIndex = run.shardInfo.current;
|
|
5248
5482
|
finishBody.shardTotal = run.shardInfo.total;
|
|
@@ -5327,165 +5561,2499 @@ var RunSubmitter = class {
|
|
|
5327
5561
|
}
|
|
5328
5562
|
};
|
|
5329
5563
|
|
|
5330
|
-
//
|
|
5331
|
-
var
|
|
5332
|
-
var
|
|
5333
|
-
var
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
screenshot: "screenshot",
|
|
5357
|
-
waitFor: "wait",
|
|
5358
|
-
waitForSelector: "waitForSelector",
|
|
5359
|
-
waitForLoadState: "waitForLoadState",
|
|
5360
|
-
waitForFunction: "waitForFunction",
|
|
5361
|
-
waitForResponse: "waitForResponse",
|
|
5362
|
-
waitForRequest: "waitForRequest",
|
|
5363
|
-
waitForEvent: "waitForEvent",
|
|
5364
|
-
waitForTimeout: "waitForTimeout",
|
|
5365
|
-
innerText: "read text",
|
|
5366
|
-
textContent: "read text",
|
|
5367
|
-
inputValue: "read value",
|
|
5368
|
-
getAttribute: "read attribute",
|
|
5369
|
-
isVisible: "visibility check",
|
|
5370
|
-
isEnabled: "enabled check",
|
|
5371
|
-
isChecked: "checked check",
|
|
5372
|
-
boundingBox: "measure",
|
|
5373
|
-
evaluate: "evaluate",
|
|
5374
|
-
goto: "navigation"
|
|
5375
|
-
};
|
|
5376
|
-
var ACTION_GERUNDS = {
|
|
5377
|
-
click: "clicking",
|
|
5378
|
-
dblclick: "double-clicking",
|
|
5379
|
-
fill: "filling",
|
|
5380
|
-
type: "typing into",
|
|
5381
|
-
press: "pressing a key on",
|
|
5382
|
-
pressSequentially: "typing into",
|
|
5383
|
-
check: "checking",
|
|
5384
|
-
uncheck: "unchecking",
|
|
5385
|
-
hover: "hovering",
|
|
5386
|
-
tap: "tapping",
|
|
5387
|
-
focus: "focusing",
|
|
5388
|
-
clear: "clearing",
|
|
5389
|
-
selectOption: "selecting an option in",
|
|
5390
|
-
setInputFiles: "uploading a file to",
|
|
5391
|
-
dragTo: "dragging",
|
|
5392
|
-
waitFor: "waiting for",
|
|
5393
|
-
waitForSelector: "waiting for",
|
|
5394
|
-
evaluate: "evaluating on"
|
|
5395
|
-
};
|
|
5396
|
-
var STATE_PHRASES = {
|
|
5397
|
-
"not-found": "was not found on the page",
|
|
5398
|
-
hidden: "never became visible",
|
|
5399
|
-
"not-visible": "never became visible",
|
|
5400
|
-
"not-enabled": "never became enabled",
|
|
5401
|
-
"not-editable": "never became editable",
|
|
5402
|
-
"not-stable": "never stopped moving",
|
|
5403
|
-
"outside-viewport": "stayed outside the viewport",
|
|
5404
|
-
"intercepts-pointer": "was covered by another element",
|
|
5405
|
-
detached: "was detached from the DOM",
|
|
5406
|
-
navigating: "was still navigating"
|
|
5407
|
-
};
|
|
5408
|
-
var STATE_MATCHERS = {
|
|
5409
|
-
toBeVisible: { met: "visible", unmet: "never became visible" },
|
|
5410
|
-
toBeHidden: { met: "hidden", unmet: "never became hidden" },
|
|
5411
|
-
toBeEnabled: { met: "enabled", unmet: "never became enabled" },
|
|
5412
|
-
toBeDisabled: { met: "disabled", unmet: "never became disabled" },
|
|
5413
|
-
toBeChecked: { met: "checked", unmet: "never became checked" },
|
|
5414
|
-
toBeEditable: { met: "editable", unmet: "never became editable" },
|
|
5415
|
-
toBeFocused: { met: "focused", unmet: "never received focus" },
|
|
5416
|
-
toBeAttached: { met: "attached", unmet: "never appeared in the DOM" },
|
|
5417
|
-
toBeDetached: { met: "detached", unmet: "never left the DOM" },
|
|
5418
|
-
toBeInViewport: { met: "in the viewport", unmet: "never entered the viewport" },
|
|
5419
|
-
toBeEmpty: { met: "empty", unmet: "never became empty" }
|
|
5420
|
-
};
|
|
5421
|
-
var VALUE_MATCHERS = {
|
|
5422
|
-
toHaveText: "text",
|
|
5423
|
-
toContainText: "text containing",
|
|
5424
|
-
toHaveValue: "value",
|
|
5425
|
-
toHaveValues: "values",
|
|
5426
|
-
toHaveAttribute: "attribute",
|
|
5427
|
-
toHaveClass: "class",
|
|
5428
|
-
toContainClass: "class",
|
|
5429
|
-
toHaveId: "id",
|
|
5430
|
-
toHaveCSS: "CSS",
|
|
5431
|
-
toHaveJSProperty: "property",
|
|
5432
|
-
toHaveAccessibleName: "accessible name",
|
|
5433
|
-
toHaveAccessibleDescription: "accessible description",
|
|
5434
|
-
toHaveRole: "role",
|
|
5435
|
-
toHaveURL: "URL",
|
|
5436
|
-
toHaveTitle: "title",
|
|
5437
|
-
toHaveScreenshot: "screenshot",
|
|
5438
|
-
toMatchAriaSnapshot: "ARIA snapshot"
|
|
5439
|
-
};
|
|
5440
|
-
var NETWORK_ERRORS = {
|
|
5441
|
-
ERR_CONNECTION_REFUSED: "Connection refused loading",
|
|
5442
|
-
ERR_CONNECTION_RESET: "Connection reset loading",
|
|
5443
|
-
ERR_CONNECTION_CLOSED: "Connection closed loading",
|
|
5444
|
-
ERR_CONNECTION_TIMED_OUT: "Connection timed out loading",
|
|
5445
|
-
ERR_TIMED_OUT: "Connection timed out loading",
|
|
5446
|
-
ERR_NAME_NOT_RESOLVED: "DNS lookup failed for",
|
|
5447
|
-
ERR_INTERNET_DISCONNECTED: "No network while loading",
|
|
5448
|
-
ERR_ADDRESS_UNREACHABLE: "Address unreachable loading",
|
|
5449
|
-
ERR_ABORTED: "Navigation aborted loading",
|
|
5450
|
-
ERR_EMPTY_RESPONSE: "Empty response loading",
|
|
5451
|
-
ERR_TOO_MANY_REDIRECTS: "Too many redirects loading",
|
|
5452
|
-
ERR_SSL_PROTOCOL_ERROR: "TLS error loading",
|
|
5453
|
-
ERR_CERT_AUTHORITY_INVALID: "Untrusted certificate loading",
|
|
5454
|
-
ERR_CERT_COMMON_NAME_INVALID: "Certificate name mismatch loading",
|
|
5455
|
-
ERR_CERT_DATE_INVALID: "Expired certificate loading",
|
|
5456
|
-
ERR_BLOCKED_BY_CLIENT: "Request blocked loading",
|
|
5457
|
-
ERR_FAILED: "Request failed loading",
|
|
5458
|
-
ERR_HTTP_RESPONSE_CODE_FAILURE: "HTTP error loading",
|
|
5459
|
-
NS_ERROR_CONNECTION_REFUSED: "Connection refused loading",
|
|
5460
|
-
NS_ERROR_UNKNOWN_HOST: "DNS lookup failed for",
|
|
5461
|
-
NS_ERROR_NET_TIMEOUT: "Connection timed out loading",
|
|
5462
|
-
NS_ERROR_OFFLINE: "No network while loading",
|
|
5463
|
-
NS_ERROR_ABORT: "Navigation aborted loading",
|
|
5464
|
-
NS_BINDING_ABORTED: "Navigation aborted loading"
|
|
5465
|
-
};
|
|
5466
|
-
function formatTimeout(ms) {
|
|
5467
|
-
if (ms < 1e3) return `${ms} ms`;
|
|
5468
|
-
const seconds = ms / 1e3;
|
|
5469
|
-
const rounded = Math.round(seconds * 10) / 10;
|
|
5470
|
-
return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded} s`;
|
|
5564
|
+
// src/internal/capture/resource-ledger.ts
|
|
5565
|
+
var fs19 = __toESM(require("fs"));
|
|
5566
|
+
var path20 = __toESM(require("path"));
|
|
5567
|
+
var import_node_module2 = require("module");
|
|
5568
|
+
|
|
5569
|
+
// src/internal/capture/quiet-capture.ts
|
|
5570
|
+
var import_node_module = require("module");
|
|
5571
|
+
var path18 = __toESM(require("path"));
|
|
5572
|
+
var BOXED_PREFIX_EXPORTS = [
|
|
5573
|
+
[
|
|
5574
|
+
"playwright-core/lib/coreBundle",
|
|
5575
|
+
(mod) => mod?.utils?.setBoxedStackPrefixes
|
|
5576
|
+
],
|
|
5577
|
+
["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
|
|
5578
|
+
];
|
|
5579
|
+
var boxing;
|
|
5580
|
+
var resolvedPlaywrightPackageJson;
|
|
5581
|
+
function resolvePlaywrightPackageJson() {
|
|
5582
|
+
if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
|
|
5583
|
+
try {
|
|
5584
|
+
const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
|
|
5585
|
+
resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
|
|
5586
|
+
} catch {
|
|
5587
|
+
resolvedPlaywrightPackageJson = null;
|
|
5588
|
+
}
|
|
5589
|
+
return resolvedPlaywrightPackageJson;
|
|
5471
5590
|
}
|
|
5472
|
-
function
|
|
5591
|
+
function resolveBoxing() {
|
|
5473
5592
|
try {
|
|
5474
|
-
const
|
|
5475
|
-
|
|
5593
|
+
const root = findOwnPackageJson(__dirname)?.root;
|
|
5594
|
+
if (!root) return null;
|
|
5595
|
+
const playwrightPackageJson = resolvePlaywrightPackageJson();
|
|
5596
|
+
if (!playwrightPackageJson) return null;
|
|
5597
|
+
const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
|
|
5598
|
+
for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
|
|
5599
|
+
let set;
|
|
5600
|
+
try {
|
|
5601
|
+
set = pick(requireFromPlaywright(moduleId));
|
|
5602
|
+
} catch {
|
|
5603
|
+
continue;
|
|
5604
|
+
}
|
|
5605
|
+
if (typeof set !== "function") continue;
|
|
5606
|
+
return {
|
|
5607
|
+
set,
|
|
5608
|
+
prefixes: [path18.dirname(playwrightPackageJson), path18.join(root, "dist") + path18.sep]
|
|
5609
|
+
};
|
|
5610
|
+
}
|
|
5476
5611
|
} catch {
|
|
5477
|
-
return url;
|
|
5478
5612
|
}
|
|
5613
|
+
return null;
|
|
5479
5614
|
}
|
|
5480
|
-
function
|
|
5481
|
-
const
|
|
5482
|
-
|
|
5615
|
+
function boxCaptureFrames(extraPrefixes = []) {
|
|
5616
|
+
const first = boxing === void 0;
|
|
5617
|
+
if (first) boxing = resolveBoxing();
|
|
5618
|
+
if (!boxing) return;
|
|
5619
|
+
const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
|
|
5620
|
+
if (!first && added.length === 0) return;
|
|
5621
|
+
boxing.prefixes.push(...added);
|
|
5622
|
+
try {
|
|
5623
|
+
boxing.set([...boxing.prefixes]);
|
|
5624
|
+
} catch {
|
|
5625
|
+
}
|
|
5483
5626
|
}
|
|
5484
|
-
function
|
|
5485
|
-
const
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5627
|
+
function internalCall(owner, run) {
|
|
5628
|
+
const wrap = owner?._wrapApiCall;
|
|
5629
|
+
if (typeof wrap !== "function") return run();
|
|
5630
|
+
return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
|
|
5631
|
+
}
|
|
5632
|
+
|
|
5633
|
+
// src/internal/capture/worker-metrics.ts
|
|
5634
|
+
var import_node_perf_hooks = require("perf_hooks");
|
|
5635
|
+
|
|
5636
|
+
// src/internal/support/system-readers.ts
|
|
5637
|
+
var fs18 = __toESM(require("fs"));
|
|
5638
|
+
var path19 = __toESM(require("path"));
|
|
5639
|
+
var CLK_TCK = 100;
|
|
5640
|
+
function readText(file) {
|
|
5641
|
+
try {
|
|
5642
|
+
return fs18.readFileSync(file, "utf8");
|
|
5643
|
+
} catch {
|
|
5644
|
+
return null;
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
function parseProcStat(pid, text) {
|
|
5648
|
+
const open = text.indexOf("(");
|
|
5649
|
+
const close = text.lastIndexOf(")");
|
|
5650
|
+
if (open < 0 || close < open) return null;
|
|
5651
|
+
const fields = text.slice(close + 2).trim().split(/\s+/);
|
|
5652
|
+
const ppid = Number(fields[1]);
|
|
5653
|
+
const utime = Number(fields[11]);
|
|
5654
|
+
const stime = Number(fields[12]);
|
|
5655
|
+
const start = Number(fields[19]);
|
|
5656
|
+
if (![ppid, utime, stime, start].every(Number.isFinite)) return null;
|
|
5657
|
+
return { pid, ppid, comm: text.slice(open + 1, close), cpuMs: (utime + stime) * 1e3 / CLK_TCK, start };
|
|
5658
|
+
}
|
|
5659
|
+
function parseSchedstat(text) {
|
|
5660
|
+
const waitNs = Number(text.trim().split(/\s+/)[1]);
|
|
5661
|
+
return Number.isFinite(waitNs) ? waitNs / 1e6 : null;
|
|
5662
|
+
}
|
|
5663
|
+
function parseKbField(text, key) {
|
|
5664
|
+
const match = new RegExp(`^${key}:\\s+(\\d+)`, "m").exec(text);
|
|
5665
|
+
return match ? Number(match[1]) : null;
|
|
5666
|
+
}
|
|
5667
|
+
function parseCpuTicks(text) {
|
|
5668
|
+
const line = text.split("\n", 1)[0] ?? "";
|
|
5669
|
+
if (!line.startsWith("cpu ")) return null;
|
|
5670
|
+
const v = line.trim().split(/\s+/).slice(1, 9).map(Number);
|
|
5671
|
+
if (v.length < 8 || v.some((n) => !Number.isFinite(n))) return null;
|
|
5672
|
+
const [user, nice, system, idle, iowait, irq, softirq, steal] = v;
|
|
5673
|
+
return { busy: user + nice + system + irq + softirq, idle, iowait, steal, total: v.reduce((sum, n) => sum + n, 0) };
|
|
5674
|
+
}
|
|
5675
|
+
function parsePsiTotal(text) {
|
|
5676
|
+
const match = /^some\b.*\btotal=(\d+)/m.exec(text);
|
|
5677
|
+
return match ? Number(match[1]) : null;
|
|
5678
|
+
}
|
|
5679
|
+
function parsePsTime(text) {
|
|
5680
|
+
const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/.exec(text.trim());
|
|
5681
|
+
if (!match) return null;
|
|
5682
|
+
const [, days, hours, minutes, seconds] = match;
|
|
5683
|
+
return ((Number(days ?? 0) * 24 + Number(hours ?? 0)) * 3600 + Number(minutes) * 60 + Number(seconds)) * 1e3;
|
|
5684
|
+
}
|
|
5685
|
+
function parsePsOutput(text) {
|
|
5686
|
+
const out = [];
|
|
5687
|
+
for (const line of text.split("\n")) {
|
|
5688
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
5689
|
+
if (!match) continue;
|
|
5690
|
+
const cpuMs = parsePsTime(match[4]);
|
|
5691
|
+
if (cpuMs === null) continue;
|
|
5692
|
+
out.push({
|
|
5693
|
+
pid: Number(match[1]),
|
|
5694
|
+
ppid: Number(match[2]),
|
|
5695
|
+
rssKb: Number(match[3]),
|
|
5696
|
+
cpuMs,
|
|
5697
|
+
command: match[5].trim()
|
|
5698
|
+
});
|
|
5699
|
+
}
|
|
5700
|
+
return out;
|
|
5701
|
+
}
|
|
5702
|
+
function parseWindowsProcesses(text) {
|
|
5703
|
+
let data;
|
|
5704
|
+
try {
|
|
5705
|
+
data = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
5706
|
+
} catch {
|
|
5707
|
+
return [];
|
|
5708
|
+
}
|
|
5709
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
5710
|
+
const out = [];
|
|
5711
|
+
for (const row of rows) {
|
|
5712
|
+
if (typeof row !== "object" || row === null) continue;
|
|
5713
|
+
const record = row;
|
|
5714
|
+
const pid = Number(record.ProcessId);
|
|
5715
|
+
const ppid = Number(record.ParentProcessId);
|
|
5716
|
+
if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue;
|
|
5717
|
+
const workingSet = Number(record.WorkingSetSize);
|
|
5718
|
+
const user = Number(record.UserModeTime);
|
|
5719
|
+
const kernel = Number(record.KernelModeTime);
|
|
5720
|
+
out.push({
|
|
5721
|
+
pid,
|
|
5722
|
+
ppid,
|
|
5723
|
+
rssKb: Number.isFinite(workingSet) ? Math.round(workingSet / 1024) : 0,
|
|
5724
|
+
cpuMs: ((Number.isFinite(user) ? user : 0) + (Number.isFinite(kernel) ? kernel : 0)) / 1e4,
|
|
5725
|
+
name: typeof record.Name === "string" ? record.Name : "",
|
|
5726
|
+
commandLine: typeof record.CommandLine === "string" ? record.CommandLine : ""
|
|
5727
|
+
});
|
|
5728
|
+
}
|
|
5729
|
+
return out;
|
|
5730
|
+
}
|
|
5731
|
+
function roleOf(comm, cmdline) {
|
|
5732
|
+
const command = `${comm} ${cmdline}`;
|
|
5733
|
+
if (/workerProcessEntry/.test(cmdline)) return "worker";
|
|
5734
|
+
if (/loaderProcessEntry/.test(cmdline)) return "runner";
|
|
5735
|
+
if (/(^|[\s/])ffmpeg/.test(command)) return "ffmpeg";
|
|
5736
|
+
const type = /--type=([\w-]+)/.exec(cmdline)?.[1];
|
|
5737
|
+
if (/chrom|headless_shell|msedge|Google Chrome/i.test(command)) {
|
|
5738
|
+
if (!type) return "browser";
|
|
5739
|
+
if (type === "renderer") return "renderer";
|
|
5740
|
+
if (type === "gpu-process") return "gpu";
|
|
5741
|
+
return "utility";
|
|
5742
|
+
}
|
|
5743
|
+
if (/firefox/i.test(command)) return /-contentproc/.test(cmdline) ? "renderer" : "browser";
|
|
5744
|
+
if (/WebKitWebProcess/.test(command)) return "renderer";
|
|
5745
|
+
if (/WebKitGPUProcess/.test(command)) return "gpu";
|
|
5746
|
+
if (/WebKitNetworkProcess/.test(command)) return "utility";
|
|
5747
|
+
if (/MiniBrowser|pw_run\.sh/.test(command)) return "browser";
|
|
5748
|
+
return null;
|
|
5749
|
+
}
|
|
5750
|
+
function descendantsOf(root, table) {
|
|
5751
|
+
const children = /* @__PURE__ */ new Map();
|
|
5752
|
+
for (const proc of table) {
|
|
5753
|
+
const list = children.get(proc.ppid) ?? [];
|
|
5754
|
+
list.push(proc);
|
|
5755
|
+
children.set(proc.ppid, list);
|
|
5756
|
+
}
|
|
5757
|
+
const out = [];
|
|
5758
|
+
const stack = [root];
|
|
5759
|
+
const seen = /* @__PURE__ */ new Set([root]);
|
|
5760
|
+
while (stack.length > 0) {
|
|
5761
|
+
for (const child of children.get(stack.pop()) ?? []) {
|
|
5762
|
+
if (seen.has(child.pid)) continue;
|
|
5763
|
+
seen.add(child.pid);
|
|
5764
|
+
out.push(child);
|
|
5765
|
+
stack.push(child.pid);
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
return out;
|
|
5769
|
+
}
|
|
5770
|
+
var ProcFs = class {
|
|
5771
|
+
constructor(root = "/proc") {
|
|
5772
|
+
this.root = root;
|
|
5773
|
+
}
|
|
5774
|
+
/** Whether this looks like a Linux `/proc` at all. */
|
|
5775
|
+
available() {
|
|
5776
|
+
return fs18.existsSync(path19.join(this.root, "stat"));
|
|
5777
|
+
}
|
|
5778
|
+
/** Every process, keyed by pid. */
|
|
5779
|
+
table() {
|
|
5780
|
+
const out = /* @__PURE__ */ new Map();
|
|
5781
|
+
let names;
|
|
5782
|
+
try {
|
|
5783
|
+
names = fs18.readdirSync(this.root);
|
|
5784
|
+
} catch {
|
|
5785
|
+
return out;
|
|
5786
|
+
}
|
|
5787
|
+
for (const name of names) {
|
|
5788
|
+
if (!/^\d+$/.test(name)) continue;
|
|
5789
|
+
const text = readText(path19.join(this.root, name, "stat"));
|
|
5790
|
+
const stat = text ? parseProcStat(Number(name), text) : null;
|
|
5791
|
+
if (stat) out.set(stat.pid, stat);
|
|
5792
|
+
}
|
|
5793
|
+
return out;
|
|
5794
|
+
}
|
|
5795
|
+
stat(pid) {
|
|
5796
|
+
const text = readText(path19.join(this.root, String(pid), "stat"));
|
|
5797
|
+
return text ? parseProcStat(pid, text) : null;
|
|
5798
|
+
}
|
|
5799
|
+
runWaitMs(pid) {
|
|
5800
|
+
const text = readText(path19.join(this.root, String(pid), "schedstat"));
|
|
5801
|
+
return text ? parseSchedstat(text) : null;
|
|
5802
|
+
}
|
|
5803
|
+
cmdline(pid) {
|
|
5804
|
+
return (readText(path19.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
|
|
5805
|
+
}
|
|
5806
|
+
/** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
|
|
5807
|
+
statusKb(pid, key) {
|
|
5808
|
+
const text = readText(path19.join(this.root, String(pid), "status"));
|
|
5809
|
+
return text ? parseKbField(text, key) : null;
|
|
5810
|
+
}
|
|
5811
|
+
/** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
|
|
5812
|
+
async pssKb(pid) {
|
|
5813
|
+
try {
|
|
5814
|
+
return parseKbField(await fs18.promises.readFile(path19.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
|
|
5815
|
+
} catch {
|
|
5816
|
+
return null;
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
/** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
|
|
5820
|
+
resetPeakRss(pid) {
|
|
5821
|
+
try {
|
|
5822
|
+
fs18.writeFileSync(path19.join(this.root, String(pid), "clear_refs"), "5");
|
|
5823
|
+
return true;
|
|
5824
|
+
} catch {
|
|
5825
|
+
return false;
|
|
5826
|
+
}
|
|
5827
|
+
}
|
|
5828
|
+
cpuTicks() {
|
|
5829
|
+
const text = readText(path19.join(this.root, "stat"));
|
|
5830
|
+
return text ? parseCpuTicks(text) : null;
|
|
5831
|
+
}
|
|
5832
|
+
/** Total stall time of a resource's pressure file, in µs. */
|
|
5833
|
+
pressureUs(resource) {
|
|
5834
|
+
const text = readText(path19.join(this.root, "pressure", resource));
|
|
5835
|
+
return text ? parsePsiTotal(text) : null;
|
|
5836
|
+
}
|
|
5837
|
+
memAvailableKb() {
|
|
5838
|
+
const text = readText(path19.join(this.root, "meminfo"));
|
|
5839
|
+
return text ? parseKbField(text, "MemAvailable") : null;
|
|
5840
|
+
}
|
|
5841
|
+
/** Open file descriptors of a process. */
|
|
5842
|
+
fdCount(pid) {
|
|
5843
|
+
try {
|
|
5844
|
+
return fs18.readdirSync(path19.join(this.root, String(pid), "fd")).length;
|
|
5845
|
+
} catch {
|
|
5846
|
+
return null;
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
};
|
|
5850
|
+
var UNLIMITED = 2 ** 60;
|
|
5851
|
+
function numberIn(text, pattern) {
|
|
5852
|
+
if (text === null) return null;
|
|
5853
|
+
const raw = pattern ? pattern.exec(text)?.[1] : text.trim();
|
|
5854
|
+
const value = Number(raw);
|
|
5855
|
+
return raw !== void 0 && raw !== "" && Number.isFinite(value) ? value : null;
|
|
5856
|
+
}
|
|
5857
|
+
var CgroupFs = class {
|
|
5858
|
+
constructor(procRoot = "/proc", cgroupRoot = "/sys/fs/cgroup", pid = "self") {
|
|
5859
|
+
this.procRoot = procRoot;
|
|
5860
|
+
this.cgroupRoot = cgroupRoot;
|
|
5861
|
+
this.pid = pid;
|
|
5862
|
+
}
|
|
5863
|
+
/** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
|
|
5864
|
+
dirs() {
|
|
5865
|
+
const text = readText(path19.join(this.procRoot, String(this.pid), "cgroup"));
|
|
5866
|
+
if (!text) return null;
|
|
5867
|
+
const v1 = /* @__PURE__ */ new Map();
|
|
5868
|
+
let v2 = null;
|
|
5869
|
+
for (const line of text.split("\n")) {
|
|
5870
|
+
const match = /^\d+:([^:]*):(.*)$/.exec(line.trim());
|
|
5871
|
+
if (!match) continue;
|
|
5872
|
+
if (match[1] === "") v2 = match[2];
|
|
5873
|
+
else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
|
|
5874
|
+
}
|
|
5875
|
+
const pick = (mount, sub) => {
|
|
5876
|
+
const nested = path19.join(mount, sub);
|
|
5877
|
+
if (fs18.existsSync(nested)) return nested;
|
|
5878
|
+
return fs18.existsSync(mount) ? mount : null;
|
|
5879
|
+
};
|
|
5880
|
+
if (v1.has("memory") || v1.has("cpu")) {
|
|
5881
|
+
const memoryPath = v1.get("memory");
|
|
5882
|
+
const cpuPath = v1.get("cpu");
|
|
5883
|
+
const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path19.join(this.cgroupRoot, name)).find((dir) => fs18.existsSync(dir));
|
|
5884
|
+
return {
|
|
5885
|
+
version: 1,
|
|
5886
|
+
memory: memoryPath !== void 0 ? pick(path19.join(this.cgroupRoot, "memory"), memoryPath) : null,
|
|
5887
|
+
cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
|
|
5888
|
+
};
|
|
5889
|
+
}
|
|
5890
|
+
if (v2 !== null && fs18.existsSync(path19.join(this.cgroupRoot, "cgroup.controllers"))) {
|
|
5891
|
+
const dir = pick(this.cgroupRoot, v2);
|
|
5892
|
+
return { version: 2, memory: dir, cpu: dir };
|
|
5893
|
+
}
|
|
5894
|
+
return null;
|
|
5895
|
+
}
|
|
5896
|
+
read() {
|
|
5897
|
+
const dirs = this.dirs();
|
|
5898
|
+
if (!dirs) return null;
|
|
5899
|
+
const file = (dir, name) => dir ? readText(path19.join(dir, name)) : null;
|
|
5900
|
+
if (dirs.version === 2) {
|
|
5901
|
+
const max = file(dirs.memory, "memory.max");
|
|
5902
|
+
const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
|
|
5903
|
+
const quota = cpuMax && cpuMax[0] !== "max" ? Number(cpuMax[0]) / Number(cpuMax[1]) : null;
|
|
5904
|
+
const throttledUs = numberIn(file(dirs.cpu, "cpu.stat"), /^throttled_usec (\d+)/m);
|
|
5905
|
+
return {
|
|
5906
|
+
version: 2,
|
|
5907
|
+
memoryLimitBytes: max === null || max.trim() === "max" ? null : numberIn(max),
|
|
5908
|
+
memoryCurrentBytes: numberIn(file(dirs.memory, "memory.current")),
|
|
5909
|
+
memoryPeakBytes: numberIn(file(dirs.memory, "memory.peak")),
|
|
5910
|
+
oomKills: numberIn(file(dirs.memory, "memory.events"), /^oom_kill (\d+)/m),
|
|
5911
|
+
cpuQuotaCores: quota !== null && Number.isFinite(quota) ? quota : null,
|
|
5912
|
+
throttledMs: throttledUs === null ? null : throttledUs / 1e3
|
|
5913
|
+
};
|
|
5914
|
+
}
|
|
5915
|
+
const limit = numberIn(file(dirs.memory, "memory.limit_in_bytes"));
|
|
5916
|
+
const quotaUs = numberIn(file(dirs.cpu, "cpu.cfs_quota_us"));
|
|
5917
|
+
const periodUs = numberIn(file(dirs.cpu, "cpu.cfs_period_us"));
|
|
5918
|
+
const throttledNs = numberIn(file(dirs.cpu, "cpu.stat"), /^throttled_time (\d+)/m);
|
|
5919
|
+
return {
|
|
5920
|
+
version: 1,
|
|
5921
|
+
memoryLimitBytes: limit !== null && limit < UNLIMITED ? limit : null,
|
|
5922
|
+
memoryCurrentBytes: numberIn(file(dirs.memory, "memory.usage_in_bytes")),
|
|
5923
|
+
memoryPeakBytes: numberIn(file(dirs.memory, "memory.max_usage_in_bytes")),
|
|
5924
|
+
oomKills: numberIn(file(dirs.memory, "memory.oom_control"), /^oom_kill (\d+)/m),
|
|
5925
|
+
cpuQuotaCores: quotaUs !== null && quotaUs > 0 && periodUs ? quotaUs / periodUs : null,
|
|
5926
|
+
throttledMs: throttledNs === null ? null : throttledNs / 1e6
|
|
5927
|
+
};
|
|
5928
|
+
}
|
|
5929
|
+
};
|
|
5930
|
+
|
|
5931
|
+
// src/internal/capture/worker-metrics.ts
|
|
5932
|
+
var MB = 1024 * 1024;
|
|
5933
|
+
function round1(value) {
|
|
5934
|
+
return Math.round(value * 10) / 10;
|
|
5935
|
+
}
|
|
5936
|
+
var WorkerMetrics = class {
|
|
5937
|
+
constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
|
|
5938
|
+
this.proc = proc;
|
|
5939
|
+
this.pid = pid;
|
|
5940
|
+
this.loop = null;
|
|
5941
|
+
this.usage = null;
|
|
5942
|
+
/** Each process's CPU and wait at the start of the current window; null before the first. */
|
|
5943
|
+
this.subtree = null;
|
|
5944
|
+
this.roles = /* @__PURE__ */ new Map();
|
|
5945
|
+
this.linux = platform === "linux" && proc.available();
|
|
5946
|
+
let histogram = null;
|
|
5947
|
+
try {
|
|
5948
|
+
histogram = (0, import_node_perf_hooks.monitorEventLoopDelay)({ resolution: 20 });
|
|
5949
|
+
histogram.enable();
|
|
5950
|
+
} catch {
|
|
5951
|
+
histogram = null;
|
|
5952
|
+
}
|
|
5953
|
+
this.histogram = histogram;
|
|
5954
|
+
}
|
|
5955
|
+
/** The worker's processes under it, keyed by pid and start time. */
|
|
5956
|
+
tree() {
|
|
5957
|
+
const table = this.proc.table();
|
|
5958
|
+
return descendantsOf(this.pid, table.values()).map((stat) => {
|
|
5959
|
+
const key = `${stat.pid}:${stat.start}`;
|
|
5960
|
+
let role = this.roles.get(key);
|
|
5961
|
+
if (!role) {
|
|
5962
|
+
role = roleOf(stat.comm, this.proc.cmdline(stat.pid)) ?? "other";
|
|
5963
|
+
this.roles.set(key, role);
|
|
5964
|
+
}
|
|
5965
|
+
return { key, pid: stat.pid, cpuMs: stat.cpuMs, role };
|
|
5966
|
+
});
|
|
5967
|
+
}
|
|
5968
|
+
/**
|
|
5969
|
+
* The processes under the worker now, and the reset of their peak memory: the
|
|
5970
|
+
* start of the next window. Taken once per test, at its end, so the time
|
|
5971
|
+
* between two tests counts toward the next one.
|
|
5972
|
+
*/
|
|
5973
|
+
snapshot() {
|
|
5974
|
+
this.subtree = /* @__PURE__ */ new Map();
|
|
5975
|
+
for (const proc of this.tree()) {
|
|
5976
|
+
this.subtree.set(proc.key, { cpuMs: proc.cpuMs, waitMs: this.proc.runWaitMs(proc.pid) });
|
|
5977
|
+
this.proc.resetPeakRss(proc.pid);
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
/**
|
|
5981
|
+
* Start a test's window. `processes` says whether the worker has started any
|
|
5982
|
+
* browser to read; a worker that never did skips the process tree.
|
|
5983
|
+
*/
|
|
5984
|
+
start(processes = true) {
|
|
5985
|
+
try {
|
|
5986
|
+
this.histogram?.reset();
|
|
5987
|
+
this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
|
|
5988
|
+
this.usage = process.resourceUsage();
|
|
5989
|
+
if (this.linux && processes && !this.subtree) this.snapshot();
|
|
5990
|
+
} catch {
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
/** Close the window. Null when the test's start was never read. */
|
|
5994
|
+
end(processes = true) {
|
|
5995
|
+
if (!this.usage || !this.loop) return null;
|
|
5996
|
+
try {
|
|
5997
|
+
const usage = process.resourceUsage();
|
|
5998
|
+
const loop = import_node_perf_hooks.performance.eventLoopUtilization(this.loop);
|
|
5999
|
+
const metrics = {
|
|
6000
|
+
worker: {
|
|
6001
|
+
cpuMs: Math.round(
|
|
6002
|
+
(usage.userCPUTime - this.usage.userCPUTime + usage.systemCPUTime - this.usage.systemCPUTime) / 1e3
|
|
6003
|
+
),
|
|
6004
|
+
involuntarySwitches: usage.involuntaryContextSwitches - this.usage.involuntaryContextSwitches,
|
|
6005
|
+
loopUtilization: Math.round(loop.utilization * 1e3) / 1e3,
|
|
6006
|
+
loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
|
|
6007
|
+
loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
|
|
6008
|
+
heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
|
|
6009
|
+
fds: this.linux ? this.proc.fdCount(this.pid) : null
|
|
6010
|
+
}
|
|
6011
|
+
};
|
|
6012
|
+
this.usage = null;
|
|
6013
|
+
this.loop = null;
|
|
6014
|
+
if (this.linux && processes) metrics.roles = this.roleCosts();
|
|
6015
|
+
return metrics;
|
|
6016
|
+
} catch {
|
|
6017
|
+
return null;
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
/** The window's cost by role; what it reads becomes the start of the next window. */
|
|
6021
|
+
roleCosts() {
|
|
6022
|
+
const roles = {};
|
|
6023
|
+
const start = this.subtree ?? /* @__PURE__ */ new Map();
|
|
6024
|
+
const next = /* @__PURE__ */ new Map();
|
|
6025
|
+
for (const proc of this.tree()) {
|
|
6026
|
+
const from = start.get(proc.key) ?? { cpuMs: 0, waitMs: 0 };
|
|
6027
|
+
const waitMs = this.proc.runWaitMs(proc.pid);
|
|
6028
|
+
const peakKb = this.proc.statusKb(proc.pid, "VmHWM");
|
|
6029
|
+
const cost = roles[proc.role] ??= { cpuMs: 0, runWaitMs: null, peakRssMb: null, processes: 0 };
|
|
6030
|
+
cost.cpuMs += Math.max(0, proc.cpuMs - from.cpuMs);
|
|
6031
|
+
if (waitMs !== null && from.waitMs !== null)
|
|
6032
|
+
cost.runWaitMs = round1((cost.runWaitMs ?? 0) + Math.max(0, waitMs - from.waitMs));
|
|
6033
|
+
if (peakKb !== null) cost.peakRssMb = Math.max(cost.peakRssMb ?? 0, round1(peakKb / 1024));
|
|
6034
|
+
cost.processes++;
|
|
6035
|
+
next.set(proc.key, { cpuMs: proc.cpuMs, waitMs });
|
|
6036
|
+
this.proc.resetPeakRss(proc.pid);
|
|
6037
|
+
}
|
|
6038
|
+
this.subtree = next;
|
|
6039
|
+
return roles;
|
|
6040
|
+
}
|
|
6041
|
+
dispose() {
|
|
6042
|
+
this.histogram?.disable();
|
|
6043
|
+
}
|
|
6044
|
+
};
|
|
6045
|
+
var PageReader = class {
|
|
6046
|
+
constructor() {
|
|
6047
|
+
this.sessions = /* @__PURE__ */ new WeakMap();
|
|
6048
|
+
}
|
|
6049
|
+
async read(page) {
|
|
6050
|
+
let session = this.sessions.get(page);
|
|
6051
|
+
if (!session) {
|
|
6052
|
+
session = internalCall(page, async () => {
|
|
6053
|
+
const opened2 = await page.context().newCDPSession(page);
|
|
6054
|
+
await opened2.send("Performance.enable", { timeDomain: "threadTicks" });
|
|
6055
|
+
return opened2;
|
|
6056
|
+
}).catch(() => null);
|
|
6057
|
+
this.sessions.set(page, session);
|
|
6058
|
+
}
|
|
6059
|
+
const opened = await session;
|
|
6060
|
+
if (!opened) return null;
|
|
6061
|
+
try {
|
|
6062
|
+
const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
|
|
6063
|
+
const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
|
|
6064
|
+
return {
|
|
6065
|
+
cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
|
|
6066
|
+
heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
|
|
6067
|
+
nodes: values.get("Nodes") ?? 0,
|
|
6068
|
+
listeners: values.get("JSEventListeners") ?? 0
|
|
6069
|
+
};
|
|
6070
|
+
} catch {
|
|
6071
|
+
return null;
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
};
|
|
6075
|
+
|
|
6076
|
+
// src/internal/capture/resource-ledger.ts
|
|
6077
|
+
var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
|
|
6078
|
+
var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
|
|
6079
|
+
var CLOSE_TIMEOUT_MS = 5e3;
|
|
6080
|
+
var PAGE_READ_CAP = 50;
|
|
6081
|
+
var PAGE_READ_TIMEOUT_MS = 1e3;
|
|
6082
|
+
var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
|
|
6083
|
+
var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
|
|
6084
|
+
function relativePath(file) {
|
|
6085
|
+
return path20.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path20.sep).join("/");
|
|
6086
|
+
}
|
|
6087
|
+
function isLibraryFile(file) {
|
|
6088
|
+
return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path20.sep}node_modules${path20.sep}`) || file.includes("/node_modules/") || file === __filename;
|
|
6089
|
+
}
|
|
6090
|
+
function userSite(stack) {
|
|
6091
|
+
for (const line of (stack ?? "").split("\n").slice(1)) {
|
|
6092
|
+
const match = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):\d+\)?$/.exec(line);
|
|
6093
|
+
if (!match) continue;
|
|
6094
|
+
const file = match[1].replace(/^file:\/\//, "");
|
|
6095
|
+
if (isLibraryFile(file) || !/\.[a-z]+$/i.test(file)) continue;
|
|
6096
|
+
return `${relativePath(file)}:${match[2]}`;
|
|
6097
|
+
}
|
|
6098
|
+
return null;
|
|
6099
|
+
}
|
|
6100
|
+
function zoneSite(zone) {
|
|
6101
|
+
for (const frame of zone?.frames ?? []) {
|
|
6102
|
+
if (!frame?.file || isLibraryFile(frame.file)) continue;
|
|
6103
|
+
return `${relativePath(frame.file)}:${frame.line ?? 0}`;
|
|
6104
|
+
}
|
|
6105
|
+
return null;
|
|
6106
|
+
}
|
|
6107
|
+
function testRef2(info) {
|
|
6108
|
+
if (!info?.testId) return null;
|
|
6109
|
+
const titlePath = Array.isArray(info.titlePath) ? info.titlePath : [];
|
|
6110
|
+
return {
|
|
6111
|
+
id: info.testId,
|
|
6112
|
+
file: info.file ? relativePath(info.file) : "",
|
|
6113
|
+
suite: titlePath.slice(1, -1).map(String)
|
|
6114
|
+
};
|
|
6115
|
+
}
|
|
6116
|
+
function runnableOf(info) {
|
|
6117
|
+
if (!info) return { phase: "worker", fixture: null };
|
|
6118
|
+
const runnable = info._timeoutManager?._running?.runnable;
|
|
6119
|
+
const type = typeof runnable?.type === "string" ? runnable.type : "test";
|
|
6120
|
+
const phase = HOOK_PHASES.has(type) ? type : type === "teardown" ? "worker" : "test";
|
|
6121
|
+
const raw = runnable?.fixture;
|
|
6122
|
+
const fixture = raw && typeof raw.title === "string" ? {
|
|
6123
|
+
title: raw.title,
|
|
6124
|
+
location: raw.location?.file ? `${relativePath(raw.location.file)}:${raw.location.line ?? 0}` : null,
|
|
6125
|
+
worker: raw.slot !== void 0 && raw.slot !== null
|
|
6126
|
+
} : null;
|
|
6127
|
+
return { phase, fixture };
|
|
6128
|
+
}
|
|
6129
|
+
function fallbackRunnable(window) {
|
|
6130
|
+
return { phase: window ? "test" : "worker", fixture: null };
|
|
6131
|
+
}
|
|
6132
|
+
function playwrightTestInfoSource() {
|
|
6133
|
+
try {
|
|
6134
|
+
const packageJson = resolvePlaywrightPackageJson();
|
|
6135
|
+
if (!packageJson) return () => null;
|
|
6136
|
+
const globals = (0, import_node_module2.createRequire)(packageJson)(path20.join(path20.dirname(packageJson), "lib", "globals.js"));
|
|
6137
|
+
const read = globals.currentTestInfo;
|
|
6138
|
+
if (typeof read !== "function") return () => null;
|
|
6139
|
+
return () => {
|
|
6140
|
+
try {
|
|
6141
|
+
return read() ?? null;
|
|
6142
|
+
} catch {
|
|
6143
|
+
return null;
|
|
6144
|
+
}
|
|
6145
|
+
};
|
|
6146
|
+
} catch {
|
|
6147
|
+
return () => null;
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
|
|
6151
|
+
return value === "fail" || value === "close" ? value : "report";
|
|
6152
|
+
}
|
|
6153
|
+
function countHandles() {
|
|
6154
|
+
const counts = {};
|
|
6155
|
+
const read = process.getActiveResourcesInfo;
|
|
6156
|
+
if (typeof read !== "function") return counts;
|
|
6157
|
+
for (const type of read.call(process)) {
|
|
6158
|
+
if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
|
|
6159
|
+
}
|
|
6160
|
+
return counts;
|
|
6161
|
+
}
|
|
6162
|
+
async function settledHandles(start) {
|
|
6163
|
+
const end = countHandles();
|
|
6164
|
+
if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
|
|
6165
|
+
await new Promise((resolve7) => setTimeout(resolve7, 0));
|
|
6166
|
+
return countHandles();
|
|
6167
|
+
}
|
|
6168
|
+
function settleWithin(promise, ms) {
|
|
6169
|
+
let timer;
|
|
6170
|
+
return Promise.race([
|
|
6171
|
+
promise.catch(() => void 0),
|
|
6172
|
+
new Promise((resolve7) => {
|
|
6173
|
+
timer = setTimeout(resolve7, ms);
|
|
6174
|
+
})
|
|
6175
|
+
]).finally(() => clearTimeout(timer));
|
|
6176
|
+
}
|
|
6177
|
+
var ResourceLedger = class {
|
|
6178
|
+
constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
|
|
6179
|
+
this.playwright = playwright;
|
|
6180
|
+
this.workerIndex = workerIndex;
|
|
6181
|
+
this.leakCheck = leakCheck;
|
|
6182
|
+
this.resultsFile = resultsFile;
|
|
6183
|
+
this.testInfoSource = testInfoSource;
|
|
6184
|
+
this.now = now;
|
|
6185
|
+
this.readers = readers;
|
|
6186
|
+
this.nextId = 1;
|
|
6187
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
6188
|
+
this.byObject = /* @__PURE__ */ new WeakMap();
|
|
6189
|
+
this.pendingCalls = /* @__PURE__ */ new Set();
|
|
6190
|
+
this.lastCall = null;
|
|
6191
|
+
/** The page a wrapped page method is calling into: its first reported API call marks it used. */
|
|
6192
|
+
this.usingPage = null;
|
|
6193
|
+
this.born = [];
|
|
6194
|
+
this.closed = [];
|
|
6195
|
+
this.handlesAtStart = {};
|
|
6196
|
+
this.openAtStart = { contexts: 0, pages: 0 };
|
|
6197
|
+
/** The test between `testStarted` and `testEnded`, for when Playwright's own test info cannot be read. */
|
|
6198
|
+
this.window = null;
|
|
6199
|
+
this.listener = null;
|
|
6200
|
+
/** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
|
|
6201
|
+
this.sawBrowser = false;
|
|
6202
|
+
}
|
|
6203
|
+
/** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
|
|
6204
|
+
install() {
|
|
6205
|
+
const instrumentation = this.playwright?._instrumentation;
|
|
6206
|
+
if (!instrumentation || typeof instrumentation.addListener !== "function") return false;
|
|
6207
|
+
this.listener = {
|
|
6208
|
+
onApiCallBegin: (zone) => {
|
|
6209
|
+
if (this.pendingCalls.size > 1e3) this.pendingCalls.clear();
|
|
6210
|
+
this.pendingCalls.add(zone);
|
|
6211
|
+
this.lastCall = zone;
|
|
6212
|
+
if (this.usingPage) {
|
|
6213
|
+
this.usingPage.used = true;
|
|
6214
|
+
this.usingPage = null;
|
|
6215
|
+
}
|
|
6216
|
+
},
|
|
6217
|
+
onApiCallEnd: (zone) => {
|
|
6218
|
+
this.pendingCalls.delete(zone);
|
|
6219
|
+
},
|
|
6220
|
+
runAfterCreateBrowserContext: async (context) => {
|
|
6221
|
+
this.guard(() => this.onContext(context, userSite(new Error().stack)));
|
|
6222
|
+
},
|
|
6223
|
+
runAfterCreateRequestContext: async (request) => {
|
|
6224
|
+
this.guard(() => this.track(request, "request", null, userSite(new Error().stack)));
|
|
6225
|
+
},
|
|
6226
|
+
runBeforeCloseRequestContext: async (request) => {
|
|
6227
|
+
this.guard(() => this.onClosed(request));
|
|
6228
|
+
}
|
|
6229
|
+
};
|
|
6230
|
+
instrumentation.addListener(this.listener);
|
|
6231
|
+
for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
|
|
6232
|
+
if (type) this.wrapBrowserType(type);
|
|
6233
|
+
}
|
|
6234
|
+
return true;
|
|
6235
|
+
}
|
|
6236
|
+
/** Stop listening; the objects already tracked stay in the ledger. */
|
|
6237
|
+
uninstall() {
|
|
6238
|
+
const remove = this.playwright?._instrumentation?.removeListener;
|
|
6239
|
+
if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
|
|
6240
|
+
this.listener = null;
|
|
6241
|
+
}
|
|
6242
|
+
/** Mark a page used: a locator was built on it. */
|
|
6243
|
+
notePageUse(page) {
|
|
6244
|
+
const entry = this.byObject.get(page);
|
|
6245
|
+
if (entry) entry.used = true;
|
|
6246
|
+
}
|
|
6247
|
+
/** Open the census window of a test. */
|
|
6248
|
+
testStarted(info) {
|
|
6249
|
+
this.window = testRef2(info);
|
|
6250
|
+
this.handlesAtStart = countHandles();
|
|
6251
|
+
this.openAtStart = { contexts: 0, pages: 0 };
|
|
6252
|
+
for (const entry of this.openEntries()) {
|
|
6253
|
+
if (entry.birth.kind === "page") this.openAtStart.pages++;
|
|
6254
|
+
else if (entry.birth.kind === "context") this.openAtStart.contexts++;
|
|
6255
|
+
}
|
|
6256
|
+
this.readers.metrics?.start(this.sawBrowser);
|
|
6257
|
+
}
|
|
6258
|
+
/**
|
|
6259
|
+
* Close the census window of a test: apply `PIWI_LEAK_CHECK=close` to what the
|
|
6260
|
+
* test left open, then take the census. Called after the test-scoped fixtures
|
|
6261
|
+
* tore down, so what the test opened through them is closed by now.
|
|
6262
|
+
*/
|
|
6263
|
+
async testEnded(info) {
|
|
6264
|
+
const test = testRef2(info) ?? this.window;
|
|
6265
|
+
const failed = typeof info.status === "string" && typeof info.expectedStatus === "string" && info.status !== info.expectedStatus;
|
|
6266
|
+
const leaks = test && !failed ? this.leaksOf(test.id) : [];
|
|
6267
|
+
if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
|
|
6268
|
+
const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
|
|
6269
|
+
const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
|
|
6270
|
+
const handles = { start: this.handlesAtStart, end: await settledHandles(this.handlesAtStart) };
|
|
6271
|
+
const census = this.census(ref, handles, await this.readPages());
|
|
6272
|
+
if (metrics) census.metrics = metrics;
|
|
6273
|
+
if (test) {
|
|
6274
|
+
census.openAtStart = this.openAtStart;
|
|
6275
|
+
if (!failed) census.leftOpen = leaks.length;
|
|
6276
|
+
}
|
|
6277
|
+
this.window = null;
|
|
6278
|
+
return { census, leaks };
|
|
6279
|
+
}
|
|
6280
|
+
/** The open pages' main thread and weight, over CDP, within a deadline. */
|
|
6281
|
+
async readPages() {
|
|
6282
|
+
const out = /* @__PURE__ */ new Map();
|
|
6283
|
+
const reader = this.readers.pages;
|
|
6284
|
+
if (!reader) return out;
|
|
6285
|
+
const pages = this.openEntries().filter((entry) => entry.birth.kind === "page").slice(0, PAGE_READ_CAP);
|
|
6286
|
+
await settleWithin(
|
|
6287
|
+
Promise.all(
|
|
6288
|
+
pages.map(async (entry) => {
|
|
6289
|
+
const main = entry.object ? await reader.read(entry.object) : null;
|
|
6290
|
+
if (main) out.set(entry.birth.id, main);
|
|
6291
|
+
})
|
|
6292
|
+
),
|
|
6293
|
+
PAGE_READ_TIMEOUT_MS
|
|
6294
|
+
);
|
|
6295
|
+
return out;
|
|
6296
|
+
}
|
|
6297
|
+
/** The census a worker takes as it shuts down, appended to the results file when the reporter set one. */
|
|
6298
|
+
workerEnded() {
|
|
6299
|
+
const census = this.census(null);
|
|
6300
|
+
if (this.resultsFile) {
|
|
6301
|
+
try {
|
|
6302
|
+
fs19.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
|
|
6303
|
+
`);
|
|
6304
|
+
} catch {
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
this.readers.metrics?.dispose();
|
|
6308
|
+
this.uninstall();
|
|
6309
|
+
return census;
|
|
6310
|
+
}
|
|
6311
|
+
guard(run) {
|
|
6312
|
+
try {
|
|
6313
|
+
run();
|
|
6314
|
+
} catch {
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
wrapBrowserType(type) {
|
|
6318
|
+
for (const method of ["launch", "connect", "connectOverCDP"]) {
|
|
6319
|
+
const original = type[method];
|
|
6320
|
+
if (typeof original !== "function") continue;
|
|
6321
|
+
const bound = original.bind(type);
|
|
6322
|
+
type[method] = async (...args) => {
|
|
6323
|
+
const site = userSite(new Error().stack);
|
|
6324
|
+
const browser = await bound(...args);
|
|
6325
|
+
this.guard(() => this.onBrowser(browser, site));
|
|
6326
|
+
return browser;
|
|
6327
|
+
};
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
track(object, kind, parent, site) {
|
|
6331
|
+
const existing = this.byObject.get(object);
|
|
6332
|
+
if (existing) return existing;
|
|
6333
|
+
const info = this.testInfoSource();
|
|
6334
|
+
const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
|
|
6335
|
+
const birth = {
|
|
6336
|
+
id: this.nextId++,
|
|
6337
|
+
kind,
|
|
6338
|
+
parent: parent ? this.byObject.get(parent)?.birth.id ?? null : null,
|
|
6339
|
+
at: this.now(),
|
|
6340
|
+
phase,
|
|
6341
|
+
fixture,
|
|
6342
|
+
test: testRef2(info) ?? this.window,
|
|
6343
|
+
site
|
|
6344
|
+
};
|
|
6345
|
+
const entry = { birth, object, closed: null, used: false };
|
|
6346
|
+
if (kind === "browser" || kind === "context") this.sawBrowser = true;
|
|
6347
|
+
this.entries.set(birth.id, entry);
|
|
6348
|
+
this.byObject.set(object, entry);
|
|
6349
|
+
this.born.push(birth);
|
|
6350
|
+
return entry;
|
|
6351
|
+
}
|
|
6352
|
+
onBrowser(browser, site) {
|
|
6353
|
+
const known = this.byObject.get(browser);
|
|
6354
|
+
if (known) return known;
|
|
6355
|
+
const entry = this.track(browser, "browser", null, site);
|
|
6356
|
+
browser.on("disconnected", () => this.guard(() => this.onClosed(browser)));
|
|
6357
|
+
return entry;
|
|
6358
|
+
}
|
|
6359
|
+
onContext(context, site) {
|
|
6360
|
+
if (this.byObject.has(context)) return;
|
|
6361
|
+
const browser = context.browser();
|
|
6362
|
+
if (browser) this.onBrowser(browser, site);
|
|
6363
|
+
const entry = this.track(context, "context", browser, site);
|
|
6364
|
+
const zone = this.latestPendingCall();
|
|
6365
|
+
if (zone?.apiName?.endsWith(".newPage")) entry.birth.implicit = true;
|
|
6366
|
+
if (context._forReuse) entry.birth.reused = true;
|
|
6367
|
+
context.on("close", () => this.guard(() => this.onClosed(context)));
|
|
6368
|
+
context.on("page", (page) => this.guard(() => this.onPage(page, context)));
|
|
6369
|
+
for (const page of context.pages()) this.onPage(page, context, { site });
|
|
6370
|
+
}
|
|
6371
|
+
/** `initial` is set for a page that came with its context (a persistent context's), which is no popup. */
|
|
6372
|
+
onPage(page, context, initial) {
|
|
6373
|
+
if (this.byObject.has(page)) return;
|
|
6374
|
+
const zone = this.latestPendingCall();
|
|
6375
|
+
const viaNewPage = !!zone?.apiName?.endsWith(".newPage");
|
|
6376
|
+
const popup = !viaNewPage && !initial;
|
|
6377
|
+
const site = viaNewPage ? zoneSite(zone) : initial ? initial.site : zoneSite(this.lastCall);
|
|
6378
|
+
const entry = this.track(page, "page", context, site);
|
|
6379
|
+
if (popup) {
|
|
6380
|
+
const opener = page._opener;
|
|
6381
|
+
const openerEntry = opener ? this.byObject.get(opener) : void 0;
|
|
6382
|
+
if (openerEntry) entry.birth.opener = openerEntry.birth.id;
|
|
6383
|
+
if (this.lastCall?.apiName) entry.birth.trigger = this.lastCall.apiName;
|
|
6384
|
+
}
|
|
6385
|
+
const markUsed = () => {
|
|
6386
|
+
entry.used = true;
|
|
6387
|
+
};
|
|
6388
|
+
page.on("load", markUsed);
|
|
6389
|
+
page.on("domcontentloaded", markUsed);
|
|
6390
|
+
page.on("framenavigated", (frame) => {
|
|
6391
|
+
try {
|
|
6392
|
+
if (frame === page.mainFrame() && frame.url() !== "about:blank") entry.used = true;
|
|
6393
|
+
} catch {
|
|
6394
|
+
}
|
|
6395
|
+
});
|
|
6396
|
+
page.on("close", () => this.guard(() => this.onClosed(page)));
|
|
6397
|
+
const methods = page;
|
|
6398
|
+
for (const method of USE_METHODS) {
|
|
6399
|
+
const original = methods[method];
|
|
6400
|
+
if (typeof original !== "function") continue;
|
|
6401
|
+
const bound = original.bind(page);
|
|
6402
|
+
methods[method] = (...args) => {
|
|
6403
|
+
const previous = this.usingPage;
|
|
6404
|
+
this.usingPage = entry;
|
|
6405
|
+
try {
|
|
6406
|
+
return bound(...args);
|
|
6407
|
+
} finally {
|
|
6408
|
+
this.usingPage = previous;
|
|
6409
|
+
}
|
|
6410
|
+
};
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
onClosed(object, byPiwi = false) {
|
|
6414
|
+
const entry = this.byObject.get(object);
|
|
6415
|
+
if (!entry || entry.closed) return;
|
|
6416
|
+
const info = this.testInfoSource();
|
|
6417
|
+
const { phase } = info ? runnableOf(info) : fallbackRunnable(this.window);
|
|
6418
|
+
const close = {
|
|
6419
|
+
id: entry.birth.id,
|
|
6420
|
+
at: this.now(),
|
|
6421
|
+
phase,
|
|
6422
|
+
test: testRef2(info) ?? this.window
|
|
6423
|
+
};
|
|
6424
|
+
if (entry.birth.kind === "page") close.used = entry.used;
|
|
6425
|
+
if (byPiwi) close.byPiwi = true;
|
|
6426
|
+
entry.closed = close;
|
|
6427
|
+
entry.object = null;
|
|
6428
|
+
this.closed.push(close);
|
|
6429
|
+
}
|
|
6430
|
+
latestPendingCall() {
|
|
6431
|
+
let latest = null;
|
|
6432
|
+
for (const zone of this.pendingCalls) latest = zone;
|
|
6433
|
+
return latest;
|
|
6434
|
+
}
|
|
6435
|
+
openEntries() {
|
|
6436
|
+
return [...this.entries.values()].filter((entry) => !entry.closed && entry.object);
|
|
6437
|
+
}
|
|
6438
|
+
/**
|
|
6439
|
+
* Objects a test opened itself, by its body or its each-hooks, and left open.
|
|
6440
|
+
* A page in a leaked context, or a context in a leaked browser, goes with it;
|
|
6441
|
+
* the context `browser.newPage()` opened for its page goes with the page.
|
|
6442
|
+
*/
|
|
6443
|
+
leaksOf(testId) {
|
|
6444
|
+
const open = this.openEntries().filter(
|
|
6445
|
+
(entry) => entry.birth.test?.id === testId && entry.birth.fixture === null && (entry.birth.phase === "test" || entry.birth.phase === "beforeEach" || entry.birth.phase === "afterEach") && !entry.birth.reused
|
|
6446
|
+
);
|
|
6447
|
+
const openIds = new Set(open.map((entry) => entry.birth.id));
|
|
6448
|
+
const foldsIntoLeak = (entry) => {
|
|
6449
|
+
let parentId = entry.birth.parent;
|
|
6450
|
+
while (parentId !== null) {
|
|
6451
|
+
if (!this.isImplicit(parentId)) return openIds.has(parentId);
|
|
6452
|
+
parentId = this.entries.get(parentId)?.birth.parent ?? null;
|
|
6453
|
+
}
|
|
6454
|
+
return false;
|
|
6455
|
+
};
|
|
6456
|
+
return open.filter((entry) => !(entry.birth.kind === "context" && this.isImplicit(entry.birth.id))).filter((entry) => !foldsIntoLeak(entry)).map((entry) => ({
|
|
6457
|
+
id: entry.birth.id,
|
|
6458
|
+
kind: entry.birth.kind,
|
|
6459
|
+
site: entry.birth.site,
|
|
6460
|
+
pages: entry.birth.kind === "page" ? 0 : this.pagesUnder(entry.birth.id),
|
|
6461
|
+
implicit: false,
|
|
6462
|
+
...entry.birth.opener !== void 0 ? { trigger: entry.birth.trigger ?? "a click" } : {}
|
|
6463
|
+
})).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
|
|
6464
|
+
}
|
|
6465
|
+
/** Open pages under a context, or under any context of a browser. */
|
|
6466
|
+
pagesUnder(id) {
|
|
6467
|
+
let count = 0;
|
|
6468
|
+
for (const entry of this.openEntries()) {
|
|
6469
|
+
if (entry.birth.kind !== "page" || entry.birth.parent === null) continue;
|
|
6470
|
+
const context = this.entries.get(entry.birth.parent);
|
|
6471
|
+
if (entry.birth.parent === id || context?.birth.parent === id) count++;
|
|
6472
|
+
}
|
|
6473
|
+
return count;
|
|
6474
|
+
}
|
|
6475
|
+
isImplicit(id) {
|
|
6476
|
+
const entry = this.entries.get(id);
|
|
6477
|
+
if (!entry || entry.birth.kind !== "context") return false;
|
|
6478
|
+
if (entry.birth.implicit) return true;
|
|
6479
|
+
const owner = entry.object ? entry.object._ownerPage : void 0;
|
|
6480
|
+
if (owner) entry.birth.implicit = true;
|
|
6481
|
+
return !!owner;
|
|
6482
|
+
}
|
|
6483
|
+
openPagesOf(contextId) {
|
|
6484
|
+
let count = 0;
|
|
6485
|
+
for (const entry of this.openEntries())
|
|
6486
|
+
if (entry.birth.kind === "page" && entry.birth.parent === contextId) count++;
|
|
6487
|
+
return count;
|
|
6488
|
+
}
|
|
6489
|
+
async closeLeaks(leaks) {
|
|
6490
|
+
for (const leak of leaks) {
|
|
6491
|
+
const entry = this.entries.get(leak.id);
|
|
6492
|
+
const object = entry?.object;
|
|
6493
|
+
if (!entry || entry.closed || !object) continue;
|
|
6494
|
+
const close = leak.kind === "request" ? () => object.dispose() : () => object.close();
|
|
6495
|
+
await settleWithin(internalCall(object, close), CLOSE_TIMEOUT_MS);
|
|
6496
|
+
const closedMeanwhile = entry.closed;
|
|
6497
|
+
if (closedMeanwhile) closedMeanwhile.byPiwi = true;
|
|
6498
|
+
else this.onClosed(object, true);
|
|
6499
|
+
}
|
|
6500
|
+
}
|
|
6501
|
+
census(test, handles, mains = /* @__PURE__ */ new Map()) {
|
|
6502
|
+
const open = [];
|
|
6503
|
+
for (const entry of this.openEntries()) {
|
|
6504
|
+
const object = entry.object;
|
|
6505
|
+
if (entry.birth.kind === "page") {
|
|
6506
|
+
const main = mains.get(entry.birth.id);
|
|
6507
|
+
open.push({
|
|
6508
|
+
id: entry.birth.id,
|
|
6509
|
+
used: entry.used,
|
|
6510
|
+
url: this.pageUrl(object),
|
|
6511
|
+
listeners: this.listenerCount(object),
|
|
6512
|
+
routes: this.routeCount(object),
|
|
6513
|
+
...main ? { main } : {}
|
|
6514
|
+
});
|
|
6515
|
+
} else if (entry.birth.kind === "context") {
|
|
6516
|
+
this.isImplicit(entry.birth.id);
|
|
6517
|
+
open.push({ id: entry.birth.id, pages: this.openPagesOf(entry.birth.id), routes: this.routeCount(object) });
|
|
6518
|
+
} else {
|
|
6519
|
+
open.push({ id: entry.birth.id });
|
|
6520
|
+
}
|
|
6521
|
+
}
|
|
6522
|
+
const census = {
|
|
6523
|
+
v: 1,
|
|
6524
|
+
worker: this.workerIndex,
|
|
6525
|
+
pid: process.pid,
|
|
6526
|
+
at: this.now(),
|
|
6527
|
+
test,
|
|
6528
|
+
born: this.born,
|
|
6529
|
+
closed: this.closed,
|
|
6530
|
+
open
|
|
6531
|
+
};
|
|
6532
|
+
if (handles) census.handles = handles;
|
|
6533
|
+
this.born = [];
|
|
6534
|
+
this.closed = [];
|
|
6535
|
+
return census;
|
|
6536
|
+
}
|
|
6537
|
+
pageUrl(page) {
|
|
6538
|
+
try {
|
|
6539
|
+
const url = typeof page.url === "function" ? String(page.url()) : "";
|
|
6540
|
+
return url ? url.split(/[?#]/)[0] : void 0;
|
|
6541
|
+
} catch {
|
|
6542
|
+
return void 0;
|
|
6543
|
+
}
|
|
6544
|
+
}
|
|
6545
|
+
listenerCount(emitter) {
|
|
6546
|
+
try {
|
|
6547
|
+
if (typeof emitter.eventNames !== "function" || typeof emitter.listenerCount !== "function") return void 0;
|
|
6548
|
+
const names = emitter.eventNames();
|
|
6549
|
+
return names.reduce((sum, name) => sum + emitter.listenerCount(name), 0);
|
|
6550
|
+
} catch {
|
|
6551
|
+
return void 0;
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6554
|
+
routeCount(object) {
|
|
6555
|
+
const routes = object._routes;
|
|
6556
|
+
return Array.isArray(routes) ? routes.length : void 0;
|
|
6557
|
+
}
|
|
6558
|
+
};
|
|
6559
|
+
var activeLedger = null;
|
|
6560
|
+
function startResourceLedger(playwright, workerIndex) {
|
|
6561
|
+
if (process.env[PIWI_ENV_KEYS.captureResources] === "false") return null;
|
|
6562
|
+
if (activeLedger) return activeLedger;
|
|
6563
|
+
const metrics = new WorkerMetrics();
|
|
6564
|
+
const ledger = new ResourceLedger(
|
|
6565
|
+
playwright,
|
|
6566
|
+
workerIndex,
|
|
6567
|
+
readLeakCheck(),
|
|
6568
|
+
process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
|
|
6569
|
+
playwrightTestInfoSource(),
|
|
6570
|
+
Date.now,
|
|
6571
|
+
{ metrics, pages: new PageReader() }
|
|
6572
|
+
);
|
|
6573
|
+
if (!ledger.install()) {
|
|
6574
|
+
metrics.dispose();
|
|
6575
|
+
return null;
|
|
6576
|
+
}
|
|
6577
|
+
activeLedger = ledger;
|
|
6578
|
+
return ledger;
|
|
6579
|
+
}
|
|
6580
|
+
function activeResourceLedger() {
|
|
6581
|
+
return activeLedger;
|
|
6582
|
+
}
|
|
6583
|
+
function stopResourceLedger() {
|
|
6584
|
+
const ledger = activeLedger;
|
|
6585
|
+
activeLedger = null;
|
|
6586
|
+
return ledger ? ledger.workerEnded() : null;
|
|
6587
|
+
}
|
|
6588
|
+
function noteResourceUse(page) {
|
|
6589
|
+
activeLedger?.notePageUse(page);
|
|
6590
|
+
}
|
|
6591
|
+
var ARTICLE = {
|
|
6592
|
+
browser: "a browser",
|
|
6593
|
+
context: "a context",
|
|
6594
|
+
page: "a page",
|
|
6595
|
+
request: "an API request context"
|
|
6596
|
+
};
|
|
6597
|
+
function leakFailureMessage(leaks) {
|
|
6598
|
+
const lines = leaks.map((leak) => {
|
|
6599
|
+
const what = leak.trigger ? `a popup opened by ${leak.trigger}` : ARTICLE[leak.kind];
|
|
6600
|
+
const where = leak.site ? `${leak.trigger ? "" : " opened"} at ${leak.site}` : "";
|
|
6601
|
+
const pages = leak.pages > 0 ? `, with ${leak.pages} page${leak.pages === 1 ? "" : "s"}` : "";
|
|
6602
|
+
return ` - ${what}${where}${pages}`;
|
|
6603
|
+
});
|
|
6604
|
+
return [
|
|
6605
|
+
`Piwi: this test left ${leaks.length === 1 ? "one object" : `${leaks.length} objects`} open:`,
|
|
6606
|
+
...lines,
|
|
6607
|
+
"Close each before the test ends (await using, close() or dispose()), or set PIWI_LEAK_CHECK=report to only list them."
|
|
6608
|
+
].join("\n");
|
|
6609
|
+
}
|
|
6610
|
+
async function recordResourceCensus(testInfo) {
|
|
6611
|
+
const ledger = activeLedger;
|
|
6612
|
+
if (!ledger) return;
|
|
6613
|
+
const { census, leaks } = await ledger.testEnded(testInfo);
|
|
6614
|
+
try {
|
|
6615
|
+
await testInfo.attach(ATTACHMENT_NAMES.resources, {
|
|
6616
|
+
contentType: "application/json",
|
|
6617
|
+
body: Buffer.from(JSON.stringify(census))
|
|
6618
|
+
});
|
|
6619
|
+
} catch {
|
|
6620
|
+
}
|
|
6621
|
+
if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
|
|
6622
|
+
}
|
|
6623
|
+
|
|
6624
|
+
// src/internal/collect/resource-verdicts.ts
|
|
6625
|
+
var path21 = __toESM(require("path"));
|
|
6626
|
+
var OPEN_STEP_TITLES = {
|
|
6627
|
+
"Create context": "context",
|
|
6628
|
+
"Launch persistent context": "context",
|
|
6629
|
+
"Launch browser": "browser",
|
|
6630
|
+
"Connect over CDP": "browser"
|
|
6631
|
+
};
|
|
6632
|
+
var CLOSE_STEP_TITLES = {
|
|
6633
|
+
"Close context": "context",
|
|
6634
|
+
"Close browser": "browser"
|
|
6635
|
+
};
|
|
6636
|
+
function parseResourceCensus(body) {
|
|
6637
|
+
if (!body) return null;
|
|
6638
|
+
try {
|
|
6639
|
+
const value = JSON.parse(body.toString());
|
|
6640
|
+
if (value?.v !== 1 || !Array.isArray(value.born) || !Array.isArray(value.closed) || !Array.isArray(value.open)) {
|
|
6641
|
+
return null;
|
|
6642
|
+
}
|
|
6643
|
+
if (typeof value.worker !== "number" || typeof value.at !== "number") return null;
|
|
6644
|
+
return value;
|
|
6645
|
+
} catch {
|
|
6646
|
+
return null;
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6649
|
+
function parseResourceResults(text) {
|
|
6650
|
+
return text.split("\n").map((line) => line.trim() ? parseResourceCensus(line) : null).filter((census) => census !== null);
|
|
6651
|
+
}
|
|
6652
|
+
function userFixturesOf(steps) {
|
|
6653
|
+
const out = [];
|
|
6654
|
+
const walk = (list) => {
|
|
6655
|
+
for (const step of list) {
|
|
6656
|
+
const file = step.location?.file;
|
|
6657
|
+
if (step.category === "fixture" && file && !file.includes(`${path21.sep}node_modules${path21.sep}`)) {
|
|
6658
|
+
const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
|
|
6659
|
+
if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
|
|
6660
|
+
}
|
|
6661
|
+
walk(step.steps ?? []);
|
|
6662
|
+
}
|
|
6663
|
+
};
|
|
6664
|
+
walk(steps);
|
|
6665
|
+
return [...new Set(out)];
|
|
6666
|
+
}
|
|
6667
|
+
function tallyLifecycleSteps(file, steps) {
|
|
6668
|
+
const tally = { file, opened: [], closed: { context: 0, browser: 0 } };
|
|
6669
|
+
const walk = (list) => {
|
|
6670
|
+
for (const step of list) {
|
|
6671
|
+
if (step.category === "fixture") continue;
|
|
6672
|
+
if (step.category === "pw:api") {
|
|
6673
|
+
const opened = OPEN_STEP_TITLES[step.title];
|
|
6674
|
+
const closed = CLOSE_STEP_TITLES[step.title];
|
|
6675
|
+
if (opened) {
|
|
6676
|
+
tally.opened.push({
|
|
6677
|
+
kind: opened,
|
|
6678
|
+
site: step.location ? `${relativeFile(step.location.file)}:${step.location.line}` : null
|
|
6679
|
+
});
|
|
6680
|
+
}
|
|
6681
|
+
if (closed) tally.closed[closed]++;
|
|
6682
|
+
}
|
|
6683
|
+
walk(step.steps ?? []);
|
|
6684
|
+
}
|
|
6685
|
+
};
|
|
6686
|
+
walk(steps);
|
|
6687
|
+
return tally;
|
|
6688
|
+
}
|
|
6689
|
+
function relativeFile(file) {
|
|
6690
|
+
return path21.relative(process.cwd(), file).split(path21.sep).join("/");
|
|
6691
|
+
}
|
|
6692
|
+
function buildResourceReport(input) {
|
|
6693
|
+
const findings = [];
|
|
6694
|
+
const byWorker = /* @__PURE__ */ new Map();
|
|
6695
|
+
for (const census of input.censuses) {
|
|
6696
|
+
const list = byWorker.get(census.worker) ?? [];
|
|
6697
|
+
list.push(census);
|
|
6698
|
+
byWorker.set(census.worker, list);
|
|
6699
|
+
}
|
|
6700
|
+
const leaked = [];
|
|
6701
|
+
const idle = [];
|
|
6702
|
+
const all = [];
|
|
6703
|
+
for (const censuses of byWorker.values()) {
|
|
6704
|
+
censuses.sort((a, b) => a.at - b.at);
|
|
6705
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
6706
|
+
for (const census of censuses) {
|
|
6707
|
+
for (const birth of census.born) {
|
|
6708
|
+
tracked.set(birth.id, {
|
|
6709
|
+
worker: census.worker,
|
|
6710
|
+
birth,
|
|
6711
|
+
bornCensus: census,
|
|
6712
|
+
close: null,
|
|
6713
|
+
lastOpenAt: null,
|
|
6714
|
+
used: void 0,
|
|
6715
|
+
series: [],
|
|
6716
|
+
mainCpu: []
|
|
6717
|
+
});
|
|
6718
|
+
}
|
|
6719
|
+
for (const close of census.closed) {
|
|
6720
|
+
const entry = tracked.get(close.id);
|
|
6721
|
+
if (!entry) continue;
|
|
6722
|
+
entry.close = close;
|
|
6723
|
+
if (close.used !== void 0) entry.used = close.used;
|
|
6724
|
+
}
|
|
6725
|
+
for (const open of census.open) {
|
|
6726
|
+
const entry = tracked.get(open.id);
|
|
6727
|
+
if (!entry) continue;
|
|
6728
|
+
entry.lastOpenAt = census.at;
|
|
6729
|
+
if (open.used !== void 0) entry.used = open.used;
|
|
6730
|
+
if (census.test) entry.series.push({ pages: open.pages, listeners: open.listeners, routes: open.routes });
|
|
6731
|
+
if (open.main) entry.mainCpu.push({ at: census.at, cpuMs: open.main.cpuMs });
|
|
6732
|
+
}
|
|
6733
|
+
}
|
|
6734
|
+
const testCensuses = censuses.filter((census) => census.test !== null);
|
|
6735
|
+
const workerEnded = censuses.some((census) => census.test === null);
|
|
6736
|
+
for (const entry of tracked.values()) {
|
|
6737
|
+
all.push(entry);
|
|
6738
|
+
if (entry.birth.kind === "page" && entry.used === false && !entry.bornCensus.test?.failed) idle.push(entry);
|
|
6739
|
+
const verdict = leakVerdict(entry, testCensuses, workerEnded);
|
|
6740
|
+
if (verdict) leaked.push({ tracked: entry, ...verdict });
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6743
|
+
findings.push(...groupLeaks(leaked, all));
|
|
6744
|
+
findings.push(...pilingFindings(all));
|
|
6745
|
+
findings.push(...handleFindings(byWorker));
|
|
6746
|
+
findings.push(...groupIdle(idle, input.fixturesByTest ?? /* @__PURE__ */ new Map()));
|
|
6747
|
+
findings.push(...probableFindings(input.tallies ?? []));
|
|
6748
|
+
const counts = { leaked: 0, idle: 0, piling: 0, handle: 0, probable: 0 };
|
|
6749
|
+
for (const finding of findings) counts[finding.verdict] += finding.verdict === "idle" ? finding.count : 1;
|
|
6750
|
+
return { findings, counts };
|
|
6751
|
+
}
|
|
6752
|
+
function sameFile(birth, ref) {
|
|
6753
|
+
return !!ref && !!birth.test && ref.file === birth.test.file;
|
|
6754
|
+
}
|
|
6755
|
+
function insideBlock(birth, ref) {
|
|
6756
|
+
if (!ref || !sameFile(birth, ref)) return false;
|
|
6757
|
+
return birth.test.suite.every((title, index) => ref.suite[index] === title);
|
|
6758
|
+
}
|
|
6759
|
+
function leakVerdict(entry, testCensuses, workerEnded) {
|
|
6760
|
+
const { birth, close, bornCensus } = entry;
|
|
6761
|
+
if (birth.reused || birth.phase === "worker" || birth.fixture?.worker || !birth.test) return null;
|
|
6762
|
+
if (birth.kind === "context" && birth.implicit) return null;
|
|
6763
|
+
if (close?.byPiwi) return { scope: "test", heldMs: 0, untilWorkerEnd: false };
|
|
6764
|
+
const closedByShutdown = !!close && (close.phase === "worker" || close.test === null);
|
|
6765
|
+
if (birth.phase === "beforeAll" || birth.phase === "afterAll") {
|
|
6766
|
+
if (close && !closedByShutdown && sameFile(birth, close.test)) return null;
|
|
6767
|
+
const ended = workerEnded || testCensuses.some((census) => census.at >= bornCensus.at && !insideBlock(birth, census.test));
|
|
6768
|
+
if (!ended) return null;
|
|
6769
|
+
const lastInside = [...testCensuses].reverse().find((census) => insideBlock(birth, census.test));
|
|
6770
|
+
const blockEnd = lastInside?.at ?? bornCensus.at;
|
|
6771
|
+
const end2 = close?.at ?? entry.lastOpenAt ?? blockEnd;
|
|
6772
|
+
return { scope: "describe", heldMs: Math.max(0, end2 - blockEnd), untilWorkerEnd: !close || closedByShutdown };
|
|
6773
|
+
}
|
|
6774
|
+
if (bornCensus.test === null || bornCensus.test.id !== birth.test.id) return null;
|
|
6775
|
+
if (bornCensus.test.failed) return null;
|
|
6776
|
+
const openAtItsEnd = bornCensus.open.some((open) => open.id === birth.id);
|
|
6777
|
+
if (!openAtItsEnd) return null;
|
|
6778
|
+
if (close && !closedByShutdown && (close.test?.id === birth.test.id || sameFile(birth, close.test))) return null;
|
|
6779
|
+
const end = close?.at ?? entry.lastOpenAt ?? bornCensus.at;
|
|
6780
|
+
return { scope: "test", heldMs: Math.max(0, end - bornCensus.at), untilWorkerEnd: !close || closedByShutdown };
|
|
6781
|
+
}
|
|
6782
|
+
function whereOf(birth) {
|
|
6783
|
+
if (birth.opener !== void 0) {
|
|
6784
|
+
return `popup${birth.trigger ? ` after ${birth.trigger}` : ""}${birth.site ? ` at ${birth.site}` : ""}`;
|
|
6785
|
+
}
|
|
6786
|
+
if (birth.fixture) {
|
|
6787
|
+
const at = birth.site ?? birth.fixture.location;
|
|
6788
|
+
return `fixture "${birth.fixture.title}"${at ? ` at ${at}` : ""}`;
|
|
6789
|
+
}
|
|
6790
|
+
return birth.site ?? "an unknown line";
|
|
6791
|
+
}
|
|
6792
|
+
function afterTestCpu(entry) {
|
|
6793
|
+
const readings = entry.mainCpu;
|
|
6794
|
+
if (readings.length < 2) return 0;
|
|
6795
|
+
const first = readings.find((reading) => reading.at >= entry.bornCensus.at) ?? readings[0];
|
|
6796
|
+
return Math.max(0, readings[readings.length - 1].cpuMs - first.cpuMs);
|
|
6797
|
+
}
|
|
6798
|
+
function siteOf(birth) {
|
|
6799
|
+
return birth.site ?? birth.fixture?.location ?? null;
|
|
6800
|
+
}
|
|
6801
|
+
var keyOf = (worker, id) => `${worker}:${id}`;
|
|
6802
|
+
function groupLeaks(leaked, all) {
|
|
6803
|
+
const leaks = new Map(leaked.map((leak) => [keyOf(leak.tracked.worker, leak.tracked.birth.id), leak]));
|
|
6804
|
+
const tracked = new Map(all.map((entry) => [keyOf(entry.worker, entry.birth.id), entry]));
|
|
6805
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6806
|
+
const groupKey = (leak) => `${leak.scope}|${leak.tracked.birth.kind}|${whereOf(leak.tracked.birth)}`;
|
|
6807
|
+
for (const leak of leaked) {
|
|
6808
|
+
const { birth } = leak.tracked;
|
|
6809
|
+
if (birth.kind === "context" && birth.implicit) continue;
|
|
6810
|
+
if (foldTarget(leak.tracked, leaks, tracked)) continue;
|
|
6811
|
+
const key = groupKey(leak);
|
|
6812
|
+
const group = groups.get(key) ?? {
|
|
6813
|
+
verdict: "leaked",
|
|
6814
|
+
kind: birth.kind,
|
|
6815
|
+
where: whereOf(birth),
|
|
6816
|
+
site: siteOf(birth),
|
|
6817
|
+
scope: leak.scope,
|
|
6818
|
+
tests: 0,
|
|
6819
|
+
count: 0,
|
|
6820
|
+
heldMs: 0,
|
|
6821
|
+
untilWorkerEnd: false,
|
|
6822
|
+
pages: 0,
|
|
6823
|
+
testIds: /* @__PURE__ */ new Set(),
|
|
6824
|
+
...leak.scope === "describe" && birth.test?.suite.length ? { detail: `beforeAll of "${birth.test.suite.join(" \u203A ")}"` } : {}
|
|
6825
|
+
};
|
|
6826
|
+
group.count++;
|
|
6827
|
+
if (birth.test) group.testIds.add(birth.test.id);
|
|
6828
|
+
group.heldMs = Math.max(group.heldMs ?? 0, leak.heldMs);
|
|
6829
|
+
group.untilWorkerEnd = group.untilWorkerEnd || leak.untilWorkerEnd;
|
|
6830
|
+
if (leak.tracked.close?.byPiwi) group.closedByPiwi = true;
|
|
6831
|
+
if (birth.kind === "page" && leak.scope === "test") addCpu(group, afterTestCpu(leak.tracked));
|
|
6832
|
+
groups.set(key, group);
|
|
6833
|
+
}
|
|
6834
|
+
for (const leak of leaked) {
|
|
6835
|
+
if (leak.tracked.birth.kind !== "page") continue;
|
|
6836
|
+
const root = foldTarget(leak.tracked, leaks, tracked);
|
|
6837
|
+
const group = root ? groups.get(groupKey(root)) : void 0;
|
|
6838
|
+
if (!group) continue;
|
|
6839
|
+
group.pages = (group.pages ?? 0) + 1;
|
|
6840
|
+
if (root.scope === "test") addCpu(group, afterTestCpu(leak.tracked));
|
|
6841
|
+
}
|
|
6842
|
+
return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => a.scope === b.scope ? b.count - a.count : a.scope === "test" ? -1 : 1);
|
|
6843
|
+
}
|
|
6844
|
+
function addCpu(group, cpuMs) {
|
|
6845
|
+
if (cpuMs > 0) group.afterTestCpuMs = (group.afterTestCpuMs ?? 0) + cpuMs;
|
|
6846
|
+
}
|
|
6847
|
+
function foldTarget(entry, leaks, tracked) {
|
|
6848
|
+
let parentId = entry.birth.parent;
|
|
6849
|
+
while (parentId !== null) {
|
|
6850
|
+
const key = keyOf(entry.worker, parentId);
|
|
6851
|
+
const parent = tracked.get(key);
|
|
6852
|
+
if (!parent) return null;
|
|
6853
|
+
if (!(parent.birth.kind === "context" && parent.birth.implicit)) return leaks.get(key) ?? null;
|
|
6854
|
+
parentId = parent.birth.parent;
|
|
6855
|
+
}
|
|
6856
|
+
return null;
|
|
6857
|
+
}
|
|
6858
|
+
function groupIdle(idle, fixturesByTest) {
|
|
6859
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6860
|
+
for (const entry of idle) {
|
|
6861
|
+
const { birth } = entry;
|
|
6862
|
+
const fixtures = birth.test ? fixturesByTest.get(birth.test.id) ?? [] : [];
|
|
6863
|
+
const detail = birth.fixture && fixtures.length ? `set up with ${fixtures.join(", ")}` : void 0;
|
|
6864
|
+
const where = whereOf(birth);
|
|
6865
|
+
const key = `${where}|${detail ?? ""}`;
|
|
6866
|
+
const group = groups.get(key) ?? {
|
|
6867
|
+
verdict: "idle",
|
|
6868
|
+
kind: "page",
|
|
6869
|
+
where,
|
|
6870
|
+
site: siteOf(birth),
|
|
6871
|
+
tests: 0,
|
|
6872
|
+
count: 0,
|
|
6873
|
+
testIds: /* @__PURE__ */ new Set(),
|
|
6874
|
+
...detail ? { detail } : {}
|
|
6875
|
+
};
|
|
6876
|
+
group.count++;
|
|
6877
|
+
if (birth.test) group.testIds.add(birth.test.id);
|
|
6878
|
+
groups.set(key, group);
|
|
6879
|
+
}
|
|
6880
|
+
return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => b.count - a.count);
|
|
6881
|
+
}
|
|
6882
|
+
function pilingFindings(all) {
|
|
6883
|
+
const out = [];
|
|
6884
|
+
for (const entry of all) {
|
|
6885
|
+
for (const what of ["pages", "listeners", "routes"]) {
|
|
6886
|
+
const values = entry.series.map((point) => point[what]).filter((v) => typeof v === "number");
|
|
6887
|
+
if (values.length < 3) continue;
|
|
6888
|
+
const rising = values.every((value, i) => i === 0 || value >= values[i - 1]);
|
|
6889
|
+
const from = values[0];
|
|
6890
|
+
const to = values[values.length - 1];
|
|
6891
|
+
if (!rising || to - from < 2) continue;
|
|
6892
|
+
out.push({
|
|
6893
|
+
verdict: "piling",
|
|
6894
|
+
kind: entry.birth.kind,
|
|
6895
|
+
where: whereOf(entry.birth),
|
|
6896
|
+
site: siteOf(entry.birth),
|
|
6897
|
+
tests: values.length,
|
|
6898
|
+
count: 1,
|
|
6899
|
+
growth: { what, from, to, tests: values.length }
|
|
6900
|
+
});
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
return out.sort((a, b) => b.growth.to - b.growth.from - (a.growth.to - a.growth.from));
|
|
6904
|
+
}
|
|
6905
|
+
function handleFindings(byWorker) {
|
|
6906
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6907
|
+
for (const censuses of byWorker.values()) {
|
|
6908
|
+
const ordered = censuses.filter((census) => census.test && census.handles).sort((a, b) => a.at - b.at);
|
|
6909
|
+
for (let i = 0; i < ordered.length - 1; i++) {
|
|
6910
|
+
const census = ordered[i];
|
|
6911
|
+
const next = ordered[i + 1];
|
|
6912
|
+
for (const [type, end] of Object.entries(census.handles.end)) {
|
|
6913
|
+
const grew = end - (census.handles.start[type] ?? 0);
|
|
6914
|
+
if (grew <= 0 || (next.handles.start[type] ?? 0) < end) continue;
|
|
6915
|
+
const test = census.test;
|
|
6916
|
+
const key = `${test.id}|${type}`;
|
|
6917
|
+
const group = groups.get(key) ?? {
|
|
6918
|
+
verdict: "handle",
|
|
6919
|
+
kind: "handle",
|
|
6920
|
+
where: type,
|
|
6921
|
+
tests: 1,
|
|
6922
|
+
count: 0,
|
|
6923
|
+
detail: `"${test.title ?? ""}" (${test.file})`
|
|
6924
|
+
};
|
|
6925
|
+
group.count += grew;
|
|
6926
|
+
groups.set(key, group);
|
|
6927
|
+
}
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
return [...groups.values()];
|
|
6931
|
+
}
|
|
6932
|
+
function probableFindings(tallies) {
|
|
6933
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
6934
|
+
for (const tally of tallies) {
|
|
6935
|
+
const merged = byFile.get(tally.file) ?? { file: tally.file, opened: [], closed: { context: 0, browser: 0 } };
|
|
6936
|
+
merged.opened.push(...tally.opened);
|
|
6937
|
+
merged.closed.context += tally.closed.context;
|
|
6938
|
+
merged.closed.browser += tally.closed.browser;
|
|
6939
|
+
byFile.set(tally.file, merged);
|
|
6940
|
+
}
|
|
6941
|
+
const out = [];
|
|
6942
|
+
for (const tally of byFile.values()) {
|
|
6943
|
+
for (const kind of ["browser", "context"]) {
|
|
6944
|
+
const opened = tally.opened.filter((o) => o.kind === kind);
|
|
6945
|
+
if (opened.length <= tally.closed[kind]) continue;
|
|
6946
|
+
const sites = [...new Set(opened.map((o) => o.site ?? tally.file))];
|
|
6947
|
+
out.push({
|
|
6948
|
+
verdict: "probable",
|
|
6949
|
+
kind,
|
|
6950
|
+
where: sites.join(", "),
|
|
6951
|
+
tests: 0,
|
|
6952
|
+
count: opened.length - tally.closed[kind],
|
|
6953
|
+
detail: `${opened.length} opened, ${tally.closed[kind]} closed in ${tally.file}`
|
|
6954
|
+
});
|
|
6955
|
+
}
|
|
6956
|
+
}
|
|
6957
|
+
return out;
|
|
6958
|
+
}
|
|
6959
|
+
function formatHeld(ms) {
|
|
6960
|
+
if (ms < 1e3) return `${Math.round(ms)} ms`;
|
|
6961
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)} s`;
|
|
6962
|
+
const minutes = Math.floor(ms / 6e4);
|
|
6963
|
+
const seconds = Math.round(ms % 6e4 / 1e3);
|
|
6964
|
+
return seconds ? `${minutes} min ${seconds} s` : `${minutes} min`;
|
|
6965
|
+
}
|
|
6966
|
+
var KIND_LABEL = {
|
|
6967
|
+
browser: "browser",
|
|
6968
|
+
context: "context",
|
|
6969
|
+
page: "page",
|
|
6970
|
+
request: "API context",
|
|
6971
|
+
handle: "handle"
|
|
6972
|
+
};
|
|
6973
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
6974
|
+
var HANDLE_LABEL = {
|
|
6975
|
+
TCPServerWrap: "server",
|
|
6976
|
+
FSEventWrap: "file watcher",
|
|
6977
|
+
StatWatcher: "file watcher"
|
|
6978
|
+
};
|
|
6979
|
+
function findingLine(finding) {
|
|
6980
|
+
const verdict = finding.verdict.padEnd(8);
|
|
6981
|
+
const label = finding.verdict === "handle" ? HANDLE_LABEL[finding.where] ?? finding.where : finding.growth ? finding.growth.what : KIND_LABEL[finding.kind];
|
|
6982
|
+
const kind = label.padEnd(11);
|
|
6983
|
+
const parts = finding.verdict === "handle" ? [] : [finding.where];
|
|
6984
|
+
switch (finding.verdict) {
|
|
6985
|
+
case "leaked": {
|
|
6986
|
+
if (finding.count > 1) parts.push(`${finding.count} ${KIND_LABEL[finding.kind]}s`);
|
|
6987
|
+
if (finding.detail) parts.push(finding.detail);
|
|
6988
|
+
if (finding.pages) parts.push(`with ${plural(finding.pages, "page")}`);
|
|
6989
|
+
if (finding.tests) parts.push(plural(finding.tests, "test"));
|
|
6990
|
+
const past = finding.scope === "describe" ? "its describe block" : "its test";
|
|
6991
|
+
if (finding.closedByPiwi) parts.push("closed by Piwi at the end of its test");
|
|
6992
|
+
else if (finding.untilWorkerEnd)
|
|
6993
|
+
parts.push(`open until the worker shut down (${formatHeld(finding.heldMs ?? 0)} past ${past})`);
|
|
6994
|
+
else parts.push(`open ${formatHeld(finding.heldMs ?? 0)} past ${past}`);
|
|
6995
|
+
if ((finding.afterTestCpuMs ?? 0) >= 500)
|
|
6996
|
+
parts.push(`${formatHeld(finding.afterTestCpuMs)} of page CPU after its test`);
|
|
6997
|
+
break;
|
|
6998
|
+
}
|
|
6999
|
+
case "idle":
|
|
7000
|
+
parts.push(plural(finding.count, "page"));
|
|
7001
|
+
if (finding.tests) parts.push(plural(finding.tests, "test"));
|
|
7002
|
+
if (finding.detail) parts.push(finding.detail);
|
|
7003
|
+
break;
|
|
7004
|
+
case "piling": {
|
|
7005
|
+
const growth = finding.growth;
|
|
7006
|
+
const noun = growth.what === "pages" ? "open pages" : growth.what === "routes" ? "route handlers" : "listeners";
|
|
7007
|
+
parts.push(`${growth.from} \u2192 ${growth.to} ${noun} over ${growth.tests} tests`);
|
|
7008
|
+
break;
|
|
7009
|
+
}
|
|
7010
|
+
case "handle":
|
|
7011
|
+
parts.push(`left running in the worker by ${finding.detail ?? "a test"}`);
|
|
7012
|
+
break;
|
|
7013
|
+
case "probable":
|
|
7014
|
+
if (finding.detail) parts.push(finding.detail);
|
|
7015
|
+
break;
|
|
7016
|
+
}
|
|
7017
|
+
return ` ${verdict} ${kind} ${parts.join(" \xB7 ")}`;
|
|
7018
|
+
}
|
|
7019
|
+
function formatResourceSummary(report, leakCheck, maxLines = 10) {
|
|
7020
|
+
if (report.findings.length === 0) return [];
|
|
7021
|
+
const { counts } = report;
|
|
7022
|
+
const head = [
|
|
7023
|
+
counts.leaked ? plural(counts.leaked, "leak") : "",
|
|
7024
|
+
counts.idle ? `${plural(counts.idle, "idle page")}` : "",
|
|
7025
|
+
counts.piling ? `${counts.piling} piling up` : "",
|
|
7026
|
+
counts.handle ? plural(counts.handle, "handle") + " left in workers" : "",
|
|
7027
|
+
counts.probable ? plural(counts.probable, "probable leak") : ""
|
|
7028
|
+
].filter(Boolean);
|
|
7029
|
+
const lines = [`Resources: ${head.join(" \xB7 ")}`];
|
|
7030
|
+
for (const finding of report.findings.slice(0, maxLines)) lines.push(findingLine(finding));
|
|
7031
|
+
const rest = report.findings.length - maxLines;
|
|
7032
|
+
if (rest > 0) lines.push(` \u2026 and ${plural(rest, "more finding")}`);
|
|
7033
|
+
const testLeaks = report.findings.some((f) => f.verdict === "leaked" && f.scope === "test" && !f.closedByPiwi);
|
|
7034
|
+
if (testLeaks && leakCheck === "report") {
|
|
7035
|
+
lines.push(
|
|
7036
|
+
"Set leakCheck: 'fail' (PIWI_LEAK_CHECK=fail) to fail the tests that leave what they open, or 'close' to close it."
|
|
7037
|
+
);
|
|
7038
|
+
}
|
|
7039
|
+
if (counts.probable) {
|
|
7040
|
+
lines.push(
|
|
7041
|
+
"Probable leaks are counted from the steps alone; the capture fixtures (extendPiwiFixtures) make them exact."
|
|
7042
|
+
);
|
|
7043
|
+
}
|
|
7044
|
+
return lines;
|
|
7045
|
+
}
|
|
7046
|
+
|
|
7047
|
+
// src/internal/collect/process-sampler.ts
|
|
7048
|
+
var fs20 = __toESM(require("fs"));
|
|
7049
|
+
var os8 = __toESM(require("os"));
|
|
7050
|
+
var path22 = __toESM(require("path"));
|
|
7051
|
+
var import_node_child_process2 = require("child_process");
|
|
7052
|
+
var WALK_FILE_CAP = 1e4;
|
|
7053
|
+
var SERIES_CAP = 3600;
|
|
7054
|
+
var TIMED_CAP = 3600;
|
|
7055
|
+
var PLAYWRIGHT_TEMP = /^(playwright_\w+dev_profile-|playwright-artifacts-)/;
|
|
7056
|
+
function round12(value) {
|
|
7057
|
+
return Math.round(value * 10) / 10;
|
|
7058
|
+
}
|
|
7059
|
+
function mean(a, b) {
|
|
7060
|
+
return round12((a + b) / 2);
|
|
7061
|
+
}
|
|
7062
|
+
function mean2(a, b) {
|
|
7063
|
+
return Math.round((a + b) / 2 * 100) / 100;
|
|
7064
|
+
}
|
|
7065
|
+
function pushTimed(points, point, merge) {
|
|
7066
|
+
points.push(point);
|
|
7067
|
+
if (points.length <= TIMED_CAP) return points;
|
|
7068
|
+
const merged = [];
|
|
7069
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
7070
|
+
const next = points[i + 1];
|
|
7071
|
+
merged.push(next ? [points[i][0], merge(points[i][1], next[1])] : points[i]);
|
|
7072
|
+
}
|
|
7073
|
+
return merged;
|
|
7074
|
+
}
|
|
7075
|
+
function pct(part, whole) {
|
|
7076
|
+
return whole > 0 ? round12(part / whole * 100) : null;
|
|
7077
|
+
}
|
|
7078
|
+
function nodeCpuTicks() {
|
|
7079
|
+
const cpus2 = os8.cpus();
|
|
7080
|
+
if (cpus2.length === 0) return null;
|
|
7081
|
+
let busy = 0;
|
|
7082
|
+
let idle = 0;
|
|
7083
|
+
for (const cpu of cpus2) {
|
|
7084
|
+
busy += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq;
|
|
7085
|
+
idle += cpu.times.idle;
|
|
7086
|
+
}
|
|
7087
|
+
return { busy, idle, iowait: 0, steal: 0, total: busy + idle };
|
|
7088
|
+
}
|
|
7089
|
+
function defaultPs() {
|
|
7090
|
+
return new Promise((resolve7, reject) => {
|
|
7091
|
+
(0, import_node_child_process2.execFile)(
|
|
7092
|
+
"ps",
|
|
7093
|
+
["-A", "-o", "pid=,ppid=,rss=,time=,command="],
|
|
7094
|
+
{ timeout: 2e3, maxBuffer: 8 * 1024 * 1024 },
|
|
7095
|
+
(error, stdout) => error ? reject(error) : resolve7(stdout)
|
|
7096
|
+
);
|
|
7097
|
+
});
|
|
7098
|
+
}
|
|
7099
|
+
function defaultWindowsQuery() {
|
|
7100
|
+
return new Promise((resolve7, reject) => {
|
|
7101
|
+
(0, import_node_child_process2.execFile)(
|
|
7102
|
+
"powershell.exe",
|
|
7103
|
+
[
|
|
7104
|
+
"-NoProfile",
|
|
7105
|
+
"-NonInteractive",
|
|
7106
|
+
"-Command",
|
|
7107
|
+
"[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $PID -and $_.ParentProcessId -ne $PID } | Select-Object ProcessId,ParentProcessId,WorkingSetSize,UserModeTime,KernelModeTime,Name,CommandLine | ConvertTo-Json -Compress"
|
|
7108
|
+
],
|
|
7109
|
+
{ timeout: 1e4, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
|
|
7110
|
+
(error, stdout) => error ? reject(error) : resolve7(stdout)
|
|
7111
|
+
);
|
|
7112
|
+
});
|
|
7113
|
+
}
|
|
7114
|
+
function freeBytes(dir) {
|
|
7115
|
+
let current = path22.resolve(dir);
|
|
7116
|
+
for (; ; ) {
|
|
7117
|
+
try {
|
|
7118
|
+
const stats = fs20.statfsSync(current);
|
|
7119
|
+
return stats.bavail * stats.bsize;
|
|
7120
|
+
} catch {
|
|
7121
|
+
const parent = path22.dirname(current);
|
|
7122
|
+
if (parent === current) return null;
|
|
7123
|
+
current = parent;
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
}
|
|
7127
|
+
async function dirBytes(dir, budget) {
|
|
7128
|
+
let bytes = 0;
|
|
7129
|
+
let entries;
|
|
7130
|
+
try {
|
|
7131
|
+
entries = await fs20.promises.readdir(dir, { withFileTypes: true });
|
|
7132
|
+
} catch {
|
|
7133
|
+
return 0;
|
|
7134
|
+
}
|
|
7135
|
+
for (const entry of entries) {
|
|
7136
|
+
if (budget.files <= 0) return bytes;
|
|
7137
|
+
const full = path22.join(dir, entry.name);
|
|
7138
|
+
if (entry.isDirectory()) bytes += await dirBytes(full, budget);
|
|
7139
|
+
else if (entry.isFile()) {
|
|
7140
|
+
budget.files--;
|
|
7141
|
+
try {
|
|
7142
|
+
bytes += (await fs20.promises.lstat(full)).size;
|
|
7143
|
+
} catch {
|
|
7144
|
+
}
|
|
7145
|
+
}
|
|
7146
|
+
}
|
|
7147
|
+
return bytes;
|
|
7148
|
+
}
|
|
7149
|
+
var RunSampler = class {
|
|
7150
|
+
constructor(options = {}) {
|
|
7151
|
+
this.timer = null;
|
|
7152
|
+
this.running = null;
|
|
7153
|
+
this.samples = 0;
|
|
7154
|
+
this.startedAt = 0;
|
|
7155
|
+
this.tracked = /* @__PURE__ */ new Map();
|
|
7156
|
+
this.treeMeasured = false;
|
|
7157
|
+
this.waitMeasured = false;
|
|
7158
|
+
this.cpuStart = null;
|
|
7159
|
+
this.cpuPrevious = null;
|
|
7160
|
+
this.series = [];
|
|
7161
|
+
this.cpuPoints = [];
|
|
7162
|
+
this.memoryPoints = [];
|
|
7163
|
+
/** Each worker process's tree, by the worker's process key. */
|
|
7164
|
+
this.workerTrees = /* @__PURE__ */ new Map();
|
|
7165
|
+
/** When the workers' CPU was last read, for the cores they used since. */
|
|
7166
|
+
this.workersReadAt = null;
|
|
7167
|
+
this.pressureStart = { cpu: null, memory: null };
|
|
7168
|
+
this.containerStart = null;
|
|
7169
|
+
this.memoryKind = null;
|
|
7170
|
+
this.peakBytes = null;
|
|
7171
|
+
this.peakAtMs = null;
|
|
7172
|
+
this.largest = null;
|
|
7173
|
+
this.rssFallbackPids = /* @__PURE__ */ new Set();
|
|
7174
|
+
this.lowestAvailableKb = null;
|
|
7175
|
+
this.peakInUse = null;
|
|
7176
|
+
this.peakInUseIsLowerBound = false;
|
|
7177
|
+
this.lowestFree = null;
|
|
7178
|
+
this.rootPid = options.rootPid ?? process.pid;
|
|
7179
|
+
this.platform = options.platform ?? process.platform;
|
|
7180
|
+
this.proc = new ProcFs(options.procRoot);
|
|
7181
|
+
this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
|
|
7182
|
+
this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path22.resolve(dir)))];
|
|
7183
|
+
this.tmpDir = options.tmpDir ?? os8.tmpdir();
|
|
7184
|
+
this.intervalMs = options.intervalMs ?? 1e3;
|
|
7185
|
+
this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
|
|
7186
|
+
this.windowsTreeEvery = Math.max(1, options.windowsTreeEvery ?? 3);
|
|
7187
|
+
this.now = options.now ?? Date.now;
|
|
7188
|
+
this.runPs = options.runPs ?? defaultPs;
|
|
7189
|
+
this.runWindows = options.runWindows ?? defaultWindowsQuery;
|
|
7190
|
+
this.linux = this.platform === "linux" && this.proc.available();
|
|
7191
|
+
}
|
|
7192
|
+
/** Take the first sample and sample on a timer that never keeps the process alive. */
|
|
7193
|
+
start() {
|
|
7194
|
+
if (this.timer) return;
|
|
7195
|
+
this.startedAt = this.now();
|
|
7196
|
+
this.cpuStart = this.cpuTicks();
|
|
7197
|
+
this.cpuPrevious = this.cpuStart;
|
|
7198
|
+
if (this.linux) {
|
|
7199
|
+
this.pressureStart = { cpu: this.proc.pressureUs("cpu"), memory: this.proc.pressureUs("memory") };
|
|
7200
|
+
this.containerStart = this.cgroup.read();
|
|
7201
|
+
}
|
|
7202
|
+
this.schedule();
|
|
7203
|
+
this.timer = setInterval(() => this.schedule(), this.intervalMs);
|
|
7204
|
+
this.timer.unref?.();
|
|
7205
|
+
}
|
|
7206
|
+
/** Take the last sample and return the run's profile. Never throws. */
|
|
7207
|
+
async stop() {
|
|
7208
|
+
if (this.timer) clearInterval(this.timer);
|
|
7209
|
+
this.timer = null;
|
|
7210
|
+
try {
|
|
7211
|
+
await this.running;
|
|
7212
|
+
await this.sample(true, true);
|
|
7213
|
+
} catch {
|
|
7214
|
+
}
|
|
7215
|
+
return this.profile(await this.leftoverBytes());
|
|
7216
|
+
}
|
|
7217
|
+
/** The series read so far, each point at the time it was read. */
|
|
7218
|
+
timedSeries() {
|
|
7219
|
+
return {
|
|
7220
|
+
startedAt: this.startedAt,
|
|
7221
|
+
cpuPct: [...this.cpuPoints],
|
|
7222
|
+
memoryBytes: [...this.memoryPoints],
|
|
7223
|
+
workers: [...this.workerTrees.values()].map((tree) => ({
|
|
7224
|
+
pid: tree.pid,
|
|
7225
|
+
cpuCores: [...tree.cpuCores],
|
|
7226
|
+
memoryBytes: [...tree.memoryBytes]
|
|
7227
|
+
}))
|
|
7228
|
+
};
|
|
7229
|
+
}
|
|
7230
|
+
schedule() {
|
|
7231
|
+
if (this.running) return;
|
|
7232
|
+
const heavy = this.samples % this.heavyEvery === 0;
|
|
7233
|
+
this.running = this.sample(heavy).catch(() => void 0).finally(() => {
|
|
7234
|
+
this.running = null;
|
|
7235
|
+
});
|
|
7236
|
+
}
|
|
7237
|
+
cpuTicks() {
|
|
7238
|
+
return this.linux ? this.proc.cpuTicks() : nodeCpuTicks();
|
|
7239
|
+
}
|
|
7240
|
+
async sample(heavy, forceTree = false) {
|
|
7241
|
+
const treeDue = forceTree || this.samples % (this.heavyEvery * this.windowsTreeEvery) === 0;
|
|
7242
|
+
this.samples++;
|
|
7243
|
+
const at = this.now() - this.startedAt;
|
|
7244
|
+
const ticks = this.cpuTicks();
|
|
7245
|
+
if (ticks && this.cpuPrevious) {
|
|
7246
|
+
const busy = pct(ticks.busy - this.cpuPrevious.busy, ticks.total - this.cpuPrevious.total);
|
|
7247
|
+
if (busy !== null && this.series.length < SERIES_CAP) this.series.push(busy);
|
|
7248
|
+
if (busy !== null) this.cpuPoints = pushTimed(this.cpuPoints, [at, busy], mean);
|
|
7249
|
+
}
|
|
7250
|
+
if (ticks) this.cpuPrevious = ticks;
|
|
7251
|
+
const availableKb = this.linux ? this.proc.memAvailableKb() : Math.round(os8.freemem() / 1024);
|
|
7252
|
+
if (availableKb !== null) this.lowestAvailableKb = Math.min(this.lowestAvailableKb ?? Infinity, availableKb);
|
|
7253
|
+
if (this.linux) await this.sampleLinuxTree(heavy, at);
|
|
7254
|
+
else if (this.platform === "darwin" && heavy) await this.samplePsTree(at);
|
|
7255
|
+
else if (this.platform === "win32" && treeDue) await this.sampleWindowsTree(at);
|
|
7256
|
+
if (heavy) await this.sampleDisk();
|
|
7257
|
+
}
|
|
7258
|
+
track(key, role, cpuMs, waitMs) {
|
|
7259
|
+
const known = this.tracked.get(key);
|
|
7260
|
+
if (known) {
|
|
7261
|
+
known.lastCpuMs = cpuMs;
|
|
7262
|
+
known.lastWaitMs = waitMs;
|
|
7263
|
+
if (role) known.role = role;
|
|
7264
|
+
return;
|
|
7265
|
+
}
|
|
7266
|
+
const first = this.samples === 1;
|
|
7267
|
+
this.tracked.set(key, {
|
|
7268
|
+
role,
|
|
7269
|
+
firstCpuMs: first ? cpuMs : 0,
|
|
7270
|
+
firstWaitMs: first ? waitMs : waitMs === null ? null : 0,
|
|
7271
|
+
lastCpuMs: cpuMs,
|
|
7272
|
+
lastWaitMs: waitMs
|
|
7273
|
+
});
|
|
7274
|
+
}
|
|
7275
|
+
/**
|
|
7276
|
+
* The worker each process of the tree belongs to: a worker process itself,
|
|
7277
|
+
* or the worker it descends from (its browsers and their children). The
|
|
7278
|
+
* runner and the web server belong to none. The tree lists parents first.
|
|
7279
|
+
*/
|
|
7280
|
+
workersOf(tree, roles, keyOf2) {
|
|
7281
|
+
const out = /* @__PURE__ */ new Map();
|
|
7282
|
+
for (const proc of tree) {
|
|
7283
|
+
if (roles.get(proc.pid) === "worker") out.set(proc.pid, { key: keyOf2(proc), pid: proc.pid });
|
|
7284
|
+
else {
|
|
7285
|
+
const parent = out.get(proc.ppid);
|
|
7286
|
+
if (parent) out.set(proc.pid, parent);
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
return out;
|
|
7290
|
+
}
|
|
7291
|
+
/** The CPU a process used since the previous sample: all of it for one born since, none at the first sample. */
|
|
7292
|
+
cpuSince(key, cpuMs) {
|
|
7293
|
+
const known = this.tracked.get(key);
|
|
7294
|
+
if (known) return Math.max(0, cpuMs - known.lastCpuMs);
|
|
7295
|
+
return this.samples === 1 ? 0 : cpuMs;
|
|
7296
|
+
}
|
|
7297
|
+
/**
|
|
7298
|
+
* Add a reading to each worker's tree: the cores its processes used since
|
|
7299
|
+
* the previous reading, and on heavy samples their memory.
|
|
7300
|
+
*/
|
|
7301
|
+
noteWorkers(workers, cpuMsByWorker, bytesByWorker, at) {
|
|
7302
|
+
const elapsed = this.workersReadAt === null ? 0 : at - this.workersReadAt;
|
|
7303
|
+
for (const { key, pid } of new Map([...workers.values()].map((w) => [w.key, w])).values()) {
|
|
7304
|
+
let tree = this.workerTrees.get(key);
|
|
7305
|
+
if (!tree) {
|
|
7306
|
+
tree = { pid, cpuCores: [], memoryBytes: [] };
|
|
7307
|
+
this.workerTrees.set(key, tree);
|
|
7308
|
+
}
|
|
7309
|
+
if (elapsed > 0) {
|
|
7310
|
+
const cores = Math.round((cpuMsByWorker.get(key) ?? 0) / elapsed * 100) / 100;
|
|
7311
|
+
tree.cpuCores = pushTimed(tree.cpuCores, [at, cores], mean2);
|
|
7312
|
+
}
|
|
7313
|
+
const bytes = bytesByWorker?.get(key);
|
|
7314
|
+
if (bytes !== void 0) tree.memoryBytes = pushTimed(tree.memoryBytes, [at, bytes], Math.max);
|
|
7315
|
+
}
|
|
7316
|
+
this.workersReadAt = at;
|
|
7317
|
+
}
|
|
7318
|
+
/** The role of each process of the tree, a child of the runner being the web server unless it says otherwise. */
|
|
7319
|
+
resolveRoles(tree, explicit) {
|
|
7320
|
+
const roles = /* @__PURE__ */ new Map([[this.rootPid, "runner"]]);
|
|
7321
|
+
for (const proc of tree) {
|
|
7322
|
+
const parent = roles.get(proc.ppid);
|
|
7323
|
+
const inherited = parent === "runner" || parent === "webServer" ? "webServer" : "other";
|
|
7324
|
+
roles.set(proc.pid, explicit(proc) ?? inherited);
|
|
7325
|
+
}
|
|
7326
|
+
return roles;
|
|
7327
|
+
}
|
|
7328
|
+
async sampleLinuxTree(heavy, at) {
|
|
7329
|
+
const table = this.proc.table();
|
|
7330
|
+
const root = table.get(this.rootPid);
|
|
7331
|
+
if (!root) return;
|
|
7332
|
+
this.treeMeasured = true;
|
|
7333
|
+
const tree = [root, ...descendantsOf(this.rootPid, table.values())];
|
|
7334
|
+
const roles = this.resolveRoles(tree.slice(1), (proc) => {
|
|
7335
|
+
const key = `${proc.pid}:${proc.start}`;
|
|
7336
|
+
const known = this.tracked.get(key)?.role;
|
|
7337
|
+
return known && !heavy ? known : roleOf(proc.comm, this.proc.cmdline(proc.pid));
|
|
7338
|
+
});
|
|
7339
|
+
const workers = this.workersOf(tree, roles, (proc) => `${proc.pid}:${proc.start}`);
|
|
7340
|
+
const cpuMsByWorker = /* @__PURE__ */ new Map();
|
|
7341
|
+
for (const proc of tree) {
|
|
7342
|
+
const key = `${proc.pid}:${proc.start}`;
|
|
7343
|
+
const worker = workers.get(proc.pid);
|
|
7344
|
+
if (worker) {
|
|
7345
|
+
cpuMsByWorker.set(worker.key, (cpuMsByWorker.get(worker.key) ?? 0) + this.cpuSince(key, proc.cpuMs));
|
|
7346
|
+
}
|
|
7347
|
+
const waitMs = this.proc.runWaitMs(proc.pid);
|
|
7348
|
+
if (waitMs !== null) this.waitMeasured = true;
|
|
7349
|
+
this.track(key, roles.get(proc.pid) ?? "other", proc.cpuMs, waitMs);
|
|
7350
|
+
}
|
|
7351
|
+
if (!heavy) {
|
|
7352
|
+
this.noteWorkers(workers, cpuMsByWorker, null, at);
|
|
7353
|
+
return;
|
|
7354
|
+
}
|
|
7355
|
+
let total = 0;
|
|
7356
|
+
let largest = null;
|
|
7357
|
+
const memory = await Promise.all(
|
|
7358
|
+
tree.map(async (proc) => {
|
|
7359
|
+
const pss = await this.proc.pssKb(proc.pid);
|
|
7360
|
+
if (pss !== null) return { proc, kb: pss, fallback: false };
|
|
7361
|
+
return { proc, kb: this.proc.statusKb(proc.pid, "VmRSS"), fallback: true };
|
|
7362
|
+
})
|
|
7363
|
+
);
|
|
7364
|
+
const bytesByWorker = /* @__PURE__ */ new Map();
|
|
7365
|
+
for (const { proc, kb, fallback } of memory) {
|
|
7366
|
+
if (kb === null) continue;
|
|
7367
|
+
if (fallback) this.rssFallbackPids.add(`${proc.pid}:${proc.start}`);
|
|
7368
|
+
total += kb * 1024;
|
|
7369
|
+
if (!largest || kb * 1024 > largest.bytes) largest = { role: roles.get(proc.pid) ?? "other", bytes: kb * 1024 };
|
|
7370
|
+
const worker = workers.get(proc.pid);
|
|
7371
|
+
if (worker) bytesByWorker.set(worker.key, (bytesByWorker.get(worker.key) ?? 0) + kb * 1024);
|
|
7372
|
+
}
|
|
7373
|
+
this.memoryKind = memory.some((m) => m.kb !== null && !m.fallback) ? "pss" : memory.length ? "rss" : null;
|
|
7374
|
+
this.notePeak(total, largest, at);
|
|
7375
|
+
this.noteWorkers(workers, cpuMsByWorker, bytesByWorker, at);
|
|
7376
|
+
}
|
|
7377
|
+
async samplePsTree(at) {
|
|
7378
|
+
let list;
|
|
7379
|
+
try {
|
|
7380
|
+
list = parsePsOutput(await this.runPs());
|
|
7381
|
+
} catch {
|
|
7382
|
+
return;
|
|
7383
|
+
}
|
|
7384
|
+
this.noteTree(list, (proc) => roleOf(path22.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
|
|
7385
|
+
}
|
|
7386
|
+
async sampleWindowsTree(at) {
|
|
7387
|
+
let list;
|
|
7388
|
+
try {
|
|
7389
|
+
list = parseWindowsProcesses(await this.runWindows());
|
|
7390
|
+
} catch {
|
|
7391
|
+
return;
|
|
7392
|
+
}
|
|
7393
|
+
this.noteTree(list, (proc) => roleOf(proc.name, proc.commandLine), at);
|
|
7394
|
+
}
|
|
7395
|
+
/** One reading of the run's tree: each process's role, CPU time and memory, and each worker's share of both. */
|
|
7396
|
+
noteTree(list, role, at) {
|
|
7397
|
+
const root = list.find((proc) => proc.pid === this.rootPid);
|
|
7398
|
+
if (!root) return;
|
|
7399
|
+
this.treeMeasured = true;
|
|
7400
|
+
const tree = [root, ...descendantsOf(this.rootPid, list)];
|
|
7401
|
+
const roles = this.resolveRoles(tree.slice(1), role);
|
|
7402
|
+
let total = 0;
|
|
7403
|
+
let largest = null;
|
|
7404
|
+
const workers = this.workersOf(tree, roles, (proc) => String(proc.pid));
|
|
7405
|
+
const cpuMsByWorker = /* @__PURE__ */ new Map();
|
|
7406
|
+
const bytesByWorker = /* @__PURE__ */ new Map();
|
|
7407
|
+
for (const proc of tree) {
|
|
7408
|
+
const procRole = roles.get(proc.pid) ?? "other";
|
|
7409
|
+
const worker = workers.get(proc.pid);
|
|
7410
|
+
if (worker) {
|
|
7411
|
+
const cpuMs = this.cpuSince(String(proc.pid), proc.cpuMs);
|
|
7412
|
+
cpuMsByWorker.set(worker.key, (cpuMsByWorker.get(worker.key) ?? 0) + cpuMs);
|
|
7413
|
+
bytesByWorker.set(worker.key, (bytesByWorker.get(worker.key) ?? 0) + proc.rssKb * 1024);
|
|
7414
|
+
}
|
|
7415
|
+
this.track(String(proc.pid), procRole, proc.cpuMs, null);
|
|
7416
|
+
total += proc.rssKb * 1024;
|
|
7417
|
+
if (!largest || proc.rssKb * 1024 > largest.bytes) largest = { role: procRole, bytes: proc.rssKb * 1024 };
|
|
7418
|
+
}
|
|
7419
|
+
this.memoryKind = "rss";
|
|
7420
|
+
this.notePeak(total, largest, at);
|
|
7421
|
+
this.noteWorkers(workers, cpuMsByWorker, bytesByWorker, at);
|
|
7422
|
+
}
|
|
7423
|
+
notePeak(total, largest, at) {
|
|
7424
|
+
this.memoryPoints = pushTimed(this.memoryPoints, [at, total], Math.max);
|
|
7425
|
+
if (this.peakBytes === null || total > this.peakBytes) {
|
|
7426
|
+
this.peakBytes = total;
|
|
7427
|
+
this.peakAtMs = this.now() - this.startedAt;
|
|
7428
|
+
}
|
|
7429
|
+
if (largest && (!this.largest || largest.bytes > this.largest.bytes)) this.largest = largest;
|
|
7430
|
+
}
|
|
7431
|
+
async sampleDisk() {
|
|
7432
|
+
for (const dir of [...this.outputDirs, this.tmpDir]) {
|
|
7433
|
+
const free = freeBytes(dir);
|
|
7434
|
+
if (free !== null) this.lowestFree = Math.min(this.lowestFree ?? Infinity, free);
|
|
7435
|
+
}
|
|
7436
|
+
const budget = { files: WALK_FILE_CAP };
|
|
7437
|
+
let bytes = 0;
|
|
7438
|
+
for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
|
|
7439
|
+
for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
|
|
7440
|
+
bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
|
|
7441
|
+
}
|
|
7442
|
+
if (budget.files <= 0) this.peakInUseIsLowerBound = true;
|
|
7443
|
+
this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
|
|
7444
|
+
}
|
|
7445
|
+
/** Playwright's folders in the temp directory whose modification time passes `keep`. */
|
|
7446
|
+
async playwrightTempEntries(keep) {
|
|
7447
|
+
let names;
|
|
7448
|
+
try {
|
|
7449
|
+
names = await fs20.promises.readdir(this.tmpDir);
|
|
7450
|
+
} catch {
|
|
7451
|
+
return [];
|
|
7452
|
+
}
|
|
7453
|
+
const out = [];
|
|
7454
|
+
for (const name of names) {
|
|
7455
|
+
if (!PLAYWRIGHT_TEMP.test(name)) continue;
|
|
7456
|
+
try {
|
|
7457
|
+
if (keep((await fs20.promises.stat(path22.join(this.tmpDir, name))).mtimeMs)) out.push(name);
|
|
7458
|
+
} catch {
|
|
7459
|
+
}
|
|
7460
|
+
}
|
|
7461
|
+
return out;
|
|
7462
|
+
}
|
|
7463
|
+
async leftoverBytes() {
|
|
7464
|
+
try {
|
|
7465
|
+
const budget = { files: WALK_FILE_CAP };
|
|
7466
|
+
let bytes = 0;
|
|
7467
|
+
for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
|
|
7468
|
+
bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
|
|
7469
|
+
}
|
|
7470
|
+
return bytes;
|
|
7471
|
+
} catch {
|
|
7472
|
+
return null;
|
|
7473
|
+
}
|
|
7474
|
+
}
|
|
7475
|
+
profile(leftoverBytes) {
|
|
7476
|
+
const wallMs = Math.max(0, this.now() - this.startedAt);
|
|
7477
|
+
const notMeasured = [];
|
|
7478
|
+
let byRole = null;
|
|
7479
|
+
if (this.treeMeasured) {
|
|
7480
|
+
byRole = {};
|
|
7481
|
+
for (const entry of this.tracked.values()) {
|
|
7482
|
+
const role = entry.role ?? "other";
|
|
7483
|
+
const usage = byRole[role] ??= { cpuMs: 0, runWaitMs: this.waitMeasured ? 0 : null, processes: 0 };
|
|
7484
|
+
usage.cpuMs += Math.max(0, entry.lastCpuMs - entry.firstCpuMs);
|
|
7485
|
+
if (usage.runWaitMs !== null && entry.lastWaitMs !== null && entry.firstWaitMs !== null) {
|
|
7486
|
+
usage.runWaitMs += Math.max(0, entry.lastWaitMs - entry.firstWaitMs);
|
|
7487
|
+
}
|
|
7488
|
+
usage.processes++;
|
|
7489
|
+
}
|
|
7490
|
+
} else {
|
|
7491
|
+
notMeasured.push("CPU and memory of the run\u2019s processes");
|
|
7492
|
+
}
|
|
7493
|
+
if (this.treeMeasured && !this.waitMeasured) notMeasured.push("time waiting for a CPU");
|
|
7494
|
+
const end = this.cpuTicks();
|
|
7495
|
+
const start = this.cpuStart;
|
|
7496
|
+
const total = end && start ? end.total - start.total : 0;
|
|
7497
|
+
const machineCpu = {
|
|
7498
|
+
busyPct: end && start ? pct(end.busy - start.busy, total) : null,
|
|
7499
|
+
iowaitPct: this.linux && end && start ? pct(end.iowait - start.iowait, total) : null,
|
|
7500
|
+
stealPct: this.linux && end && start ? pct(end.steal - start.steal, total) : null
|
|
7501
|
+
};
|
|
7502
|
+
if (!this.linux) notMeasured.push("steal");
|
|
7503
|
+
const pressure = (resource, from) => {
|
|
7504
|
+
if (!this.linux || from === null) return null;
|
|
7505
|
+
const to = this.proc.pressureUs(resource);
|
|
7506
|
+
return to === null || wallMs === 0 ? null : Math.min(100, round12((to - from) / 1e3 / wallMs * 100));
|
|
7507
|
+
};
|
|
7508
|
+
const cpuPressure = pressure("cpu", this.pressureStart.cpu);
|
|
7509
|
+
const memoryPressure = pressure("memory", this.pressureStart.memory);
|
|
7510
|
+
if (cpuPressure === null) notMeasured.push("CPU pressure");
|
|
7511
|
+
const container = this.linux ? this.cgroup.read() : null;
|
|
7512
|
+
const startContainer = this.containerStart;
|
|
7513
|
+
const totalMemory = os8.totalmem();
|
|
7514
|
+
const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
|
|
7515
|
+
const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
|
|
7516
|
+
const raisedPeak = container?.memoryPeakBytes != null && startContainer?.memoryPeakBytes != null && container.memoryPeakBytes > startContainer.memoryPeakBytes ? container.memoryPeakBytes : null;
|
|
7517
|
+
const delta = (to, from) => to != null && from != null ? Math.max(0, to - from) : null;
|
|
7518
|
+
return {
|
|
7519
|
+
platform: this.platform,
|
|
7520
|
+
wallMs,
|
|
7521
|
+
machine: {
|
|
7522
|
+
cores: typeof os8.availableParallelism === "function" ? os8.availableParallelism() : os8.cpus().length,
|
|
7523
|
+
memoryBytes: totalMemory,
|
|
7524
|
+
memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null,
|
|
7525
|
+
cpuQuotaCores: container?.cpuQuotaCores ?? null
|
|
7526
|
+
},
|
|
7527
|
+
cpu: {
|
|
7528
|
+
...machineCpu,
|
|
7529
|
+
pressurePct: cpuPressure,
|
|
7530
|
+
series: this.series,
|
|
7531
|
+
byRole,
|
|
7532
|
+
throttledMs: delta(container?.throttledMs, startContainer?.throttledMs)
|
|
7533
|
+
},
|
|
7534
|
+
memory: {
|
|
7535
|
+
kind: this.memoryKind,
|
|
7536
|
+
peakBytes: this.peakBytes,
|
|
7537
|
+
peakAtMs: this.peakAtMs,
|
|
7538
|
+
largest: this.largest,
|
|
7539
|
+
rssFallbacks: this.memoryKind === "pss" ? this.rssFallbackPids.size : 0,
|
|
7540
|
+
pressurePct: memoryPressure,
|
|
7541
|
+
lowestAvailableBytes: this.lowestAvailableKb === null ? null : this.lowestAvailableKb * 1024,
|
|
7542
|
+
containerPeakBytes: raisedPeak,
|
|
7543
|
+
oomKills: delta(container?.oomKills, startContainer?.oomKills)
|
|
7544
|
+
},
|
|
7545
|
+
disk: {
|
|
7546
|
+
peakInUseBytes: this.peakInUse,
|
|
7547
|
+
peakInUseIsLowerBound: this.peakInUseIsLowerBound,
|
|
7548
|
+
lowestFreeBytes: this.lowestFree,
|
|
7549
|
+
leftoverBytes
|
|
7550
|
+
},
|
|
7551
|
+
notMeasured
|
|
7552
|
+
};
|
|
7553
|
+
}
|
|
7554
|
+
};
|
|
7555
|
+
|
|
7556
|
+
// src/internal/collect/machine-panel.ts
|
|
7557
|
+
function workerHealthOf(censuses) {
|
|
7558
|
+
const measured = censuses.filter((census) => census.test && census.metrics);
|
|
7559
|
+
if (measured.length === 0) return null;
|
|
7560
|
+
let utilization = 0;
|
|
7561
|
+
let switches = 0;
|
|
7562
|
+
let worstP99 = 0;
|
|
7563
|
+
for (const census of measured) {
|
|
7564
|
+
const worker = census.metrics.worker;
|
|
7565
|
+
utilization += worker.loopUtilization;
|
|
7566
|
+
switches += worker.involuntarySwitches;
|
|
7567
|
+
worstP99 = Math.max(worstP99, worker.loopDelayP99Ms);
|
|
7568
|
+
}
|
|
7569
|
+
return {
|
|
7570
|
+
tests: measured.length,
|
|
7571
|
+
loopUtilization: utilization / measured.length,
|
|
7572
|
+
loopDelayP99Ms: worstP99,
|
|
7573
|
+
involuntarySwitchesPerTest: switches / measured.length
|
|
7574
|
+
};
|
|
7575
|
+
}
|
|
7576
|
+
function artifactKind(name, contentType) {
|
|
7577
|
+
if (name === "trace") return "trace";
|
|
7578
|
+
if (name === "video" || contentType.startsWith("video/")) return "video";
|
|
7579
|
+
if (name === "screenshot" || contentType.startsWith("image/")) return "screenshot";
|
|
7580
|
+
return "other";
|
|
7581
|
+
}
|
|
7582
|
+
function emptyArtifacts() {
|
|
7583
|
+
return { trace: 0, video: 0, screenshot: 0, other: 0 };
|
|
7584
|
+
}
|
|
7585
|
+
var ROLE_LABEL = {
|
|
7586
|
+
renderer: "renderers",
|
|
7587
|
+
browser: "browsers",
|
|
7588
|
+
worker: "workers",
|
|
7589
|
+
gpu: "GPU",
|
|
7590
|
+
utility: "browser utilities",
|
|
7591
|
+
ffmpeg: "ffmpeg",
|
|
7592
|
+
webServer: "web server",
|
|
7593
|
+
runner: "runner",
|
|
7594
|
+
other: "other"
|
|
7595
|
+
};
|
|
7596
|
+
var ROLE_SINGULAR = {
|
|
7597
|
+
renderer: "renderer",
|
|
7598
|
+
browser: "browser",
|
|
7599
|
+
worker: "worker",
|
|
7600
|
+
gpu: "GPU",
|
|
7601
|
+
utility: "browser utility",
|
|
7602
|
+
ffmpeg: "ffmpeg",
|
|
7603
|
+
webServer: "web server",
|
|
7604
|
+
runner: "runner",
|
|
7605
|
+
other: "other"
|
|
7606
|
+
};
|
|
7607
|
+
var ARTIFACT_LABEL = {
|
|
7608
|
+
trace: "traces",
|
|
7609
|
+
video: "videos",
|
|
7610
|
+
screenshot: "screenshots",
|
|
7611
|
+
other: "other"
|
|
7612
|
+
};
|
|
7613
|
+
var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
7614
|
+
function formatBytes(bytes) {
|
|
7615
|
+
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
7616
|
+
if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`;
|
|
7617
|
+
return `${Math.max(0, Math.round(bytes / 1024))} kB`;
|
|
7618
|
+
}
|
|
7619
|
+
function sparkline(series, width = 24) {
|
|
7620
|
+
if (series.length < 3) return "";
|
|
7621
|
+
const buckets = Math.min(width, series.length);
|
|
7622
|
+
let out = "";
|
|
7623
|
+
for (let i = 0; i < buckets; i++) {
|
|
7624
|
+
const slice = series.slice(
|
|
7625
|
+
Math.floor(i * series.length / buckets),
|
|
7626
|
+
Math.floor((i + 1) * series.length / buckets)
|
|
7627
|
+
);
|
|
7628
|
+
const mean3 = slice.reduce((sum, v) => sum + v, 0) / Math.max(1, slice.length);
|
|
7629
|
+
out += BARS[Math.min(BARS.length - 1, Math.max(0, Math.floor(mean3 / 100 * BARS.length)))];
|
|
7630
|
+
}
|
|
7631
|
+
return out;
|
|
7632
|
+
}
|
|
7633
|
+
function clock(ms) {
|
|
7634
|
+
const seconds = Math.round(ms / 1e3);
|
|
7635
|
+
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
|
|
7636
|
+
}
|
|
7637
|
+
function cpuLine(profile) {
|
|
7638
|
+
const parts = [];
|
|
7639
|
+
const spark = sparkline(profile.cpu.series);
|
|
7640
|
+
if (profile.cpu.busyPct !== null) parts.push(`${spark ? `${spark} ` : ""}${Math.round(profile.cpu.busyPct)}% busy`);
|
|
7641
|
+
if (profile.cpu.pressurePct !== null) {
|
|
7642
|
+
parts.push(`a task waited for a CPU ${Math.round(profile.cpu.pressurePct)}% of the time`);
|
|
7643
|
+
}
|
|
7644
|
+
const roles = Object.entries(profile.cpu.byRole ?? {});
|
|
7645
|
+
const total = roles.reduce((sum, [, usage]) => sum + (usage?.cpuMs ?? 0), 0);
|
|
7646
|
+
if (total > 0) {
|
|
7647
|
+
const top = roles.filter(([, usage]) => (usage?.cpuMs ?? 0) >= total * 0.02).sort((a, b) => (b[1]?.cpuMs ?? 0) - (a[1]?.cpuMs ?? 0)).slice(0, 5).map(([role, usage]) => `${ROLE_LABEL[role]} ${formatHeld(usage.cpuMs)}`);
|
|
7648
|
+
parts.push(`${formatHeld(total)} of CPU: ${top.join(" \xB7 ")}`);
|
|
7649
|
+
const waiting = roles.filter(([, usage]) => (usage?.runWaitMs ?? 0) >= 1e3).sort((a, b) => (b[1]?.runWaitMs ?? 0) - (a[1]?.runWaitMs ?? 0))[0];
|
|
7650
|
+
if (waiting) parts.push(`${ROLE_LABEL[waiting[0]]} waited ${formatHeld(waiting[1].runWaitMs)} for a CPU`);
|
|
7651
|
+
}
|
|
7652
|
+
if (profile.cpu.throttledMs)
|
|
7653
|
+
parts.push(`the container's CPU quota held it back ${formatHeld(profile.cpu.throttledMs)}`);
|
|
7654
|
+
return parts.length ? ` CPU ${parts.join(" \xB7 ")}` : null;
|
|
7655
|
+
}
|
|
7656
|
+
function memoryLine(profile) {
|
|
7657
|
+
const { memory, machine } = profile;
|
|
7658
|
+
const parts = [];
|
|
7659
|
+
if (memory.kind && memory.peakBytes !== null) {
|
|
7660
|
+
const fallbacks = memory.rssFallbacks ? `, ${memory.rssFallbacks} by RSS` : "";
|
|
7661
|
+
const at = memory.peakAtMs !== null ? ` at ${clock(memory.peakAtMs)}` : "";
|
|
7662
|
+
parts.push(`peak ${formatBytes(memory.peakBytes)} (${memory.kind.toUpperCase()}${fallbacks})${at}`);
|
|
7663
|
+
const limit = machine.memoryLimitBytes;
|
|
7664
|
+
const whole = limit ?? machine.memoryBytes;
|
|
7665
|
+
if (whole > 0)
|
|
7666
|
+
parts.push(`${Math.round(memory.peakBytes / whole * 100)}% of the ${limit ? "limit" : "machine's memory"}`);
|
|
7667
|
+
if (memory.largest)
|
|
7668
|
+
parts.push(`largest process: ${ROLE_SINGULAR[memory.largest.role]} ${formatBytes(memory.largest.bytes)}`);
|
|
7669
|
+
}
|
|
7670
|
+
if (memory.containerPeakBytes !== null)
|
|
7671
|
+
parts.push(`the container peaked at ${formatBytes(memory.containerPeakBytes)}`);
|
|
7672
|
+
if (memory.oomKills) parts.push(`${memory.oomKills} process${memory.oomKills === 1 ? "" : "es"} killed for memory`);
|
|
7673
|
+
if (memory.pressurePct !== null) {
|
|
7674
|
+
parts.push(
|
|
7675
|
+
memory.pressurePct >= 0.1 ? `a task waited for memory ${memory.pressurePct}% of the time` : "no memory pressure"
|
|
7676
|
+
);
|
|
7677
|
+
}
|
|
7678
|
+
if (memory.lowestAvailableBytes !== null && memory.lowestAvailableBytes < machine.memoryBytes * 0.1) {
|
|
7679
|
+
parts.push(`${formatBytes(memory.lowestAvailableBytes)} available at the low point`);
|
|
7680
|
+
}
|
|
7681
|
+
return parts.length ? ` Memory ${parts.join(" \xB7 ")}` : null;
|
|
7682
|
+
}
|
|
7683
|
+
function diskLine(profile, artifacts) {
|
|
7684
|
+
const parts = [];
|
|
7685
|
+
if (artifacts) {
|
|
7686
|
+
const kinds = Object.entries(artifacts).filter(([, bytes]) => bytes > 0);
|
|
7687
|
+
const total = kinds.reduce((sum, [, bytes]) => sum + bytes, 0);
|
|
7688
|
+
parts.push(
|
|
7689
|
+
total > 0 ? `${formatBytes(total)} of artifacts (${kinds.sort((a, b) => b[1] - a[1]).map(([kind, bytes]) => `${ARTIFACT_LABEL[kind]} ${formatBytes(bytes)}`).join(" \xB7 ")})` : "no artifacts"
|
|
7690
|
+
);
|
|
7691
|
+
}
|
|
7692
|
+
const { disk } = profile;
|
|
7693
|
+
if (disk.peakInUseBytes !== null && disk.peakInUseBytes > 0) {
|
|
7694
|
+
parts.push(
|
|
7695
|
+
`${disk.peakInUseIsLowerBound ? "at least " : ""}${formatBytes(disk.peakInUseBytes)} in use at the peak`
|
|
7696
|
+
);
|
|
7697
|
+
}
|
|
7698
|
+
if (disk.lowestFreeBytes !== null) parts.push(`${formatBytes(disk.lowestFreeBytes)} free at the low point`);
|
|
7699
|
+
if (disk.leftoverBytes !== null && disk.leftoverBytes >= 10 * 1024 ** 2) {
|
|
7700
|
+
parts.push(`${formatBytes(disk.leftoverBytes)} left in the temp folder by earlier runs`);
|
|
7701
|
+
}
|
|
7702
|
+
return parts.length ? ` Disk ${parts.join(" \xB7 ")}` : null;
|
|
7703
|
+
}
|
|
7704
|
+
function workersLine(workers) {
|
|
7705
|
+
if (!workers || workers.tests === 0) return null;
|
|
7706
|
+
return ` Workers event loop busy ${Math.round(workers.loopUtilization * 100)}% \xB7 p99 delay ${Math.round(
|
|
7707
|
+
workers.loopDelayP99Ms
|
|
7708
|
+
)} ms at worst \xB7 ${Math.round(workers.involuntarySwitchesPerTest).toLocaleString("en-US")} involuntary context switches per test`;
|
|
7709
|
+
}
|
|
7710
|
+
function formatMachinePanel(profile, extras) {
|
|
7711
|
+
const { machine, cpu } = profile;
|
|
7712
|
+
const head = [
|
|
7713
|
+
`${machine.cores} cores${machine.cpuQuotaCores ? ` (a quota of ${machine.cpuQuotaCores})` : ""}`,
|
|
7714
|
+
formatBytes(machine.memoryBytes)
|
|
7715
|
+
];
|
|
7716
|
+
if (machine.memoryLimitBytes) head.push(`container limit ${formatBytes(machine.memoryLimitBytes)}`);
|
|
7717
|
+
if (cpu.stealPct !== null && cpu.stealPct >= 1) head.push(`steal ${cpu.stealPct}%`);
|
|
7718
|
+
const lines = [
|
|
7719
|
+
`Machine: ${head.join(" \xB7 ")}`,
|
|
7720
|
+
cpuLine(profile),
|
|
7721
|
+
memoryLine(profile),
|
|
7722
|
+
diskLine(profile, extras.artifacts),
|
|
7723
|
+
workersLine(extras.workers)
|
|
7724
|
+
].filter((line) => line !== null);
|
|
7725
|
+
if (profile.notMeasured.length) lines.push(` Not measured here: ${profile.notMeasured.join(", ")}`);
|
|
7726
|
+
return lines;
|
|
7727
|
+
}
|
|
7728
|
+
|
|
7729
|
+
// src/internal/collect/resource-wire.ts
|
|
7730
|
+
var REPORT_FINDINGS_CAP = 100;
|
|
7731
|
+
var REPORT_SERIES_CAP = 240;
|
|
7732
|
+
var REPORT_WORKER_POINTS_CAP = 200;
|
|
7733
|
+
var TIMELINE_SERIES_CAP = 600;
|
|
7734
|
+
var TIMELINE_WORKER_POINTS_CAP = 300;
|
|
7735
|
+
var TIMELINE_WORKERS_BUDGET = 6e3;
|
|
7736
|
+
function downsample(series, cap) {
|
|
7737
|
+
if (series.length <= cap) return series;
|
|
7738
|
+
const out = [];
|
|
7739
|
+
for (let i = 0; i < cap; i++) {
|
|
7740
|
+
const slice = series.slice(Math.floor(i * series.length / cap), Math.floor((i + 1) * series.length / cap));
|
|
7741
|
+
out.push(Math.round(slice.reduce((sum, v) => sum + v, 0) / Math.max(1, slice.length) * 10) / 10);
|
|
7742
|
+
}
|
|
7743
|
+
return out;
|
|
7744
|
+
}
|
|
7745
|
+
function downsamplePoints(points, cap, merge) {
|
|
7746
|
+
if (points.length <= cap) return points;
|
|
7747
|
+
const out = [];
|
|
7748
|
+
for (let i = 0; i < cap; i++) {
|
|
7749
|
+
const slice = points.slice(Math.floor(i * points.length / cap), Math.floor((i + 1) * points.length / cap));
|
|
7750
|
+
if (slice.length === 0) continue;
|
|
7751
|
+
const values = slice.map((point) => point[1]);
|
|
7752
|
+
const value = merge === "max" ? Math.max(...values) : Math.round(values.reduce((sum, v) => sum + v, 0) / values.length * 10) / 10;
|
|
7753
|
+
out.push([slice[0][0], value]);
|
|
7754
|
+
}
|
|
7755
|
+
return out;
|
|
7756
|
+
}
|
|
7757
|
+
function nonZero(bytes) {
|
|
7758
|
+
const out = Object.fromEntries(Object.entries(bytes).filter(([, n]) => (n ?? 0) > 0));
|
|
7759
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
7760
|
+
}
|
|
7761
|
+
function executionResources(census, artifacts) {
|
|
7762
|
+
const metrics = census.metrics;
|
|
7763
|
+
if (!metrics) return null;
|
|
7764
|
+
return {
|
|
7765
|
+
workerCpuMs: metrics.worker.cpuMs,
|
|
7766
|
+
roles: metrics.roles ?? null,
|
|
7767
|
+
loopUtilization: metrics.worker.loopUtilization,
|
|
7768
|
+
loopDelayP99Ms: metrics.worker.loopDelayP99Ms,
|
|
7769
|
+
involuntarySwitches: metrics.worker.involuntarySwitches,
|
|
7770
|
+
heapUsedMb: metrics.worker.heapUsedMb,
|
|
7771
|
+
openAtStart: census.openAtStart ?? null,
|
|
7772
|
+
leftOpen: census.leftOpen ?? null,
|
|
7773
|
+
artifactBytes: nonZero(artifacts)
|
|
7774
|
+
};
|
|
7775
|
+
}
|
|
7776
|
+
function openPagesByWorker(censuses) {
|
|
7777
|
+
const byWorker = /* @__PURE__ */ new Map();
|
|
7778
|
+
for (const census of censuses) {
|
|
7779
|
+
const list = byWorker.get(census.worker) ?? [];
|
|
7780
|
+
list.push(census);
|
|
7781
|
+
byWorker.set(census.worker, list);
|
|
7782
|
+
}
|
|
7783
|
+
const out = [];
|
|
7784
|
+
for (const [worker, list] of [...byWorker.entries()].sort((a, b) => a[0] - b[0])) {
|
|
7785
|
+
list.sort((a, b) => a.at - b.at);
|
|
7786
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
7787
|
+
const points = [];
|
|
7788
|
+
for (const census of list) {
|
|
7789
|
+
for (const birth of census.born) kinds.set(birth.id, birth.kind);
|
|
7790
|
+
if (census.test) points.push(census.open.filter((open) => kinds.get(open.id) === "page").length);
|
|
7791
|
+
}
|
|
7792
|
+
if (points.length > 0) out.push({ worker, openPages: downsample(points, REPORT_WORKER_POINTS_CAP) });
|
|
7793
|
+
}
|
|
7794
|
+
return out;
|
|
7795
|
+
}
|
|
7796
|
+
function pagesOverTime(censuses) {
|
|
7797
|
+
const byWorker = /* @__PURE__ */ new Map();
|
|
7798
|
+
for (const census of censuses) {
|
|
7799
|
+
const list = byWorker.get(census.worker) ?? [];
|
|
7800
|
+
list.push(census);
|
|
7801
|
+
byWorker.set(census.worker, list);
|
|
7802
|
+
}
|
|
7803
|
+
const out = [];
|
|
7804
|
+
for (const [worker, list] of [...byWorker.entries()].sort((a, b) => a[0] - b[0])) {
|
|
7805
|
+
const pages = /* @__PURE__ */ new Set();
|
|
7806
|
+
const changes = [];
|
|
7807
|
+
let lastAt = -Infinity;
|
|
7808
|
+
for (const census of list) {
|
|
7809
|
+
lastAt = Math.max(lastAt, census.at);
|
|
7810
|
+
for (const birth of census.born) {
|
|
7811
|
+
if (birth.kind !== "page") continue;
|
|
7812
|
+
pages.add(birth.id);
|
|
7813
|
+
changes.push([birth.at, 1]);
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
for (const census of list) {
|
|
7817
|
+
for (const close of census.closed) if (pages.has(close.id)) changes.push([close.at, -1]);
|
|
7818
|
+
}
|
|
7819
|
+
if (changes.length === 0) continue;
|
|
7820
|
+
changes.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
|
|
7821
|
+
const events = [];
|
|
7822
|
+
let open = 0;
|
|
7823
|
+
for (const [at, delta] of changes) {
|
|
7824
|
+
open = Math.max(0, open + delta);
|
|
7825
|
+
const last = events[events.length - 1];
|
|
7826
|
+
if (last && last[0] === at) last[1] = open;
|
|
7827
|
+
else events.push([at, open]);
|
|
7828
|
+
}
|
|
7829
|
+
if (open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
|
|
7830
|
+
out.push({ worker, events });
|
|
7831
|
+
}
|
|
7832
|
+
return out;
|
|
7833
|
+
}
|
|
7834
|
+
function workerTreesByIndex(samples, censuses) {
|
|
7835
|
+
const byPid = /* @__PURE__ */ new Map();
|
|
7836
|
+
for (const census of censuses) {
|
|
7837
|
+
const list = byPid.get(census.pid) ?? [];
|
|
7838
|
+
list.push({ worker: census.worker, at: census.at });
|
|
7839
|
+
byPid.set(census.pid, list);
|
|
7840
|
+
}
|
|
7841
|
+
const out = /* @__PURE__ */ new Map();
|
|
7842
|
+
for (const tree of samples.workers) {
|
|
7843
|
+
const candidates = byPid.get(tree.pid);
|
|
7844
|
+
const times = [...tree.cpuCores, ...tree.memoryBytes].map(([at]) => samples.startedAt + at);
|
|
7845
|
+
if (!candidates || times.length === 0) continue;
|
|
7846
|
+
const from = Math.min(...times);
|
|
7847
|
+
const to = Math.max(...times);
|
|
7848
|
+
const distance = (at) => at < from ? from - at : at > to ? at - to : 0;
|
|
7849
|
+
const nearest = candidates.reduce((best, c) => distance(c.at) < distance(best.at) ? c : best);
|
|
7850
|
+
if (!out.has(nearest.worker)) out.set(nearest.worker, { worker: nearest.worker, ...tree });
|
|
7851
|
+
}
|
|
7852
|
+
return [...out.values()].sort((a, b) => a.worker - b.worker);
|
|
7853
|
+
}
|
|
7854
|
+
function resourceTimeline(samples, censuses) {
|
|
7855
|
+
const workers = pagesOverTime(censuses);
|
|
7856
|
+
const hasSamples = samples !== null && (samples.cpuPct.length > 0 || samples.memoryBytes.length > 0);
|
|
7857
|
+
if (!hasSamples && workers.length === 0) return null;
|
|
7858
|
+
let startedAt = hasSamples ? samples.startedAt : Infinity;
|
|
7859
|
+
for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
|
|
7860
|
+
const shift = hasSamples ? samples.startedAt - startedAt : 0;
|
|
7861
|
+
const relative13 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
|
|
7862
|
+
const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
|
|
7863
|
+
const perWorker = Math.max(
|
|
7864
|
+
60,
|
|
7865
|
+
Math.min(TIMELINE_WORKER_POINTS_CAP, Math.floor(TIMELINE_WORKERS_BUDGET / Math.max(1, 2 * trees.length)))
|
|
7866
|
+
);
|
|
7867
|
+
return {
|
|
7868
|
+
startedAt,
|
|
7869
|
+
cpuPct: hasSamples ? downsamplePoints(relative13(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
|
|
7870
|
+
memoryBytes: hasSamples ? downsamplePoints(relative13(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
|
|
7871
|
+
pages: workers.map(({ worker, events }) => {
|
|
7872
|
+
const points = relative13(events, -startedAt);
|
|
7873
|
+
const last = points.pop();
|
|
7874
|
+
return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
|
|
7875
|
+
}),
|
|
7876
|
+
workers: trees.map((tree) => ({
|
|
7877
|
+
worker: tree.worker,
|
|
7878
|
+
cpuCores: downsamplePoints(relative13(tree.cpuCores, shift), perWorker, "mean"),
|
|
7879
|
+
memoryBytes: downsamplePoints(relative13(tree.memoryBytes, shift), perWorker, "max")
|
|
7880
|
+
}))
|
|
7881
|
+
};
|
|
7882
|
+
}
|
|
7883
|
+
function resourceReportWire(input) {
|
|
7884
|
+
const { report, profile } = input;
|
|
7885
|
+
return {
|
|
7886
|
+
v: 1,
|
|
7887
|
+
shardIndex: input.shardIndex,
|
|
7888
|
+
findings: (report?.findings ?? []).slice(0, REPORT_FINDINGS_CAP),
|
|
7889
|
+
counts: report?.counts ?? { leaked: 0, idle: 0, piling: 0, handle: 0, probable: 0 },
|
|
7890
|
+
profile: profile ? { ...profile, cpu: { ...profile.cpu, series: downsample(profile.cpu.series, REPORT_SERIES_CAP) } } : null,
|
|
7891
|
+
workers: openPagesByWorker(input.censuses),
|
|
7892
|
+
artifactBytes: nonZero(input.artifacts) ?? {},
|
|
7893
|
+
workerHealth: workerHealthOf(input.censuses),
|
|
7894
|
+
timeline: resourceTimeline(input.samples ?? null, input.censuses)
|
|
7895
|
+
};
|
|
7896
|
+
}
|
|
7897
|
+
|
|
7898
|
+
// ../core/src/describe-failure.ts
|
|
7899
|
+
var HEADLINE_MAX_CHARS = 120;
|
|
7900
|
+
var VALUE_MAX_CHARS = 40;
|
|
7901
|
+
var SHORT_VALUE_MAX_CHARS = 20;
|
|
7902
|
+
var MASK_TOKEN_RE = /<(?:N|VALUE|URL|STR|UUID|HASH|EMAIL)>/g;
|
|
7903
|
+
var ACTION_VERBS = {
|
|
7904
|
+
click: "click",
|
|
7905
|
+
dblclick: "double-click",
|
|
7906
|
+
fill: "fill",
|
|
7907
|
+
type: "type",
|
|
7908
|
+
press: "press",
|
|
7909
|
+
pressSequentially: "type",
|
|
7910
|
+
check: "check",
|
|
7911
|
+
uncheck: "uncheck",
|
|
7912
|
+
hover: "hover",
|
|
7913
|
+
tap: "tap",
|
|
7914
|
+
focus: "focus",
|
|
7915
|
+
blur: "blur",
|
|
7916
|
+
clear: "clear",
|
|
7917
|
+
selectOption: "select",
|
|
7918
|
+
selectText: "select text",
|
|
7919
|
+
setInputFiles: "file upload",
|
|
7920
|
+
setChecked: "check",
|
|
7921
|
+
dragTo: "drag",
|
|
7922
|
+
dragAndDrop: "drag",
|
|
7923
|
+
scrollIntoViewIfNeeded: "scroll",
|
|
7924
|
+
screenshot: "screenshot",
|
|
7925
|
+
waitFor: "wait",
|
|
7926
|
+
waitForSelector: "waitForSelector",
|
|
7927
|
+
waitForLoadState: "waitForLoadState",
|
|
7928
|
+
waitForFunction: "waitForFunction",
|
|
7929
|
+
waitForResponse: "waitForResponse",
|
|
7930
|
+
waitForRequest: "waitForRequest",
|
|
7931
|
+
waitForEvent: "waitForEvent",
|
|
7932
|
+
waitForTimeout: "waitForTimeout",
|
|
7933
|
+
innerText: "read text",
|
|
7934
|
+
textContent: "read text",
|
|
7935
|
+
inputValue: "read value",
|
|
7936
|
+
getAttribute: "read attribute",
|
|
7937
|
+
isVisible: "visibility check",
|
|
7938
|
+
isEnabled: "enabled check",
|
|
7939
|
+
isChecked: "checked check",
|
|
7940
|
+
boundingBox: "measure",
|
|
7941
|
+
evaluate: "evaluate",
|
|
7942
|
+
goto: "navigation"
|
|
7943
|
+
};
|
|
7944
|
+
var ACTION_GERUNDS = {
|
|
7945
|
+
click: "clicking",
|
|
7946
|
+
dblclick: "double-clicking",
|
|
7947
|
+
fill: "filling",
|
|
7948
|
+
type: "typing into",
|
|
7949
|
+
press: "pressing a key on",
|
|
7950
|
+
pressSequentially: "typing into",
|
|
7951
|
+
check: "checking",
|
|
7952
|
+
uncheck: "unchecking",
|
|
7953
|
+
hover: "hovering",
|
|
7954
|
+
tap: "tapping",
|
|
7955
|
+
focus: "focusing",
|
|
7956
|
+
clear: "clearing",
|
|
7957
|
+
selectOption: "selecting an option in",
|
|
7958
|
+
setInputFiles: "uploading a file to",
|
|
7959
|
+
dragTo: "dragging",
|
|
7960
|
+
waitFor: "waiting for",
|
|
7961
|
+
waitForSelector: "waiting for",
|
|
7962
|
+
evaluate: "evaluating on"
|
|
7963
|
+
};
|
|
7964
|
+
var STATE_PHRASES = {
|
|
7965
|
+
"not-found": "was not found on the page",
|
|
7966
|
+
hidden: "never became visible",
|
|
7967
|
+
"not-visible": "never became visible",
|
|
7968
|
+
"not-enabled": "never became enabled",
|
|
7969
|
+
"not-editable": "never became editable",
|
|
7970
|
+
"not-stable": "never stopped moving",
|
|
7971
|
+
"outside-viewport": "stayed outside the viewport",
|
|
7972
|
+
"intercepts-pointer": "was covered by another element",
|
|
7973
|
+
detached: "was detached from the DOM",
|
|
7974
|
+
navigating: "was still navigating"
|
|
7975
|
+
};
|
|
7976
|
+
var STATE_MATCHERS = {
|
|
7977
|
+
toBeVisible: { met: "visible", unmet: "never became visible" },
|
|
7978
|
+
toBeHidden: { met: "hidden", unmet: "never became hidden" },
|
|
7979
|
+
toBeEnabled: { met: "enabled", unmet: "never became enabled" },
|
|
7980
|
+
toBeDisabled: { met: "disabled", unmet: "never became disabled" },
|
|
7981
|
+
toBeChecked: { met: "checked", unmet: "never became checked" },
|
|
7982
|
+
toBeEditable: { met: "editable", unmet: "never became editable" },
|
|
7983
|
+
toBeFocused: { met: "focused", unmet: "never received focus" },
|
|
7984
|
+
toBeAttached: { met: "attached", unmet: "never appeared in the DOM" },
|
|
7985
|
+
toBeDetached: { met: "detached", unmet: "never left the DOM" },
|
|
7986
|
+
toBeInViewport: { met: "in the viewport", unmet: "never entered the viewport" },
|
|
7987
|
+
toBeEmpty: { met: "empty", unmet: "never became empty" }
|
|
7988
|
+
};
|
|
7989
|
+
var VALUE_MATCHERS = {
|
|
7990
|
+
toHaveText: "text",
|
|
7991
|
+
toContainText: "text containing",
|
|
7992
|
+
toHaveValue: "value",
|
|
7993
|
+
toHaveValues: "values",
|
|
7994
|
+
toHaveAttribute: "attribute",
|
|
7995
|
+
toHaveClass: "class",
|
|
7996
|
+
toContainClass: "class",
|
|
7997
|
+
toHaveId: "id",
|
|
7998
|
+
toHaveCSS: "CSS",
|
|
7999
|
+
toHaveJSProperty: "property",
|
|
8000
|
+
toHaveAccessibleName: "accessible name",
|
|
8001
|
+
toHaveAccessibleDescription: "accessible description",
|
|
8002
|
+
toHaveRole: "role",
|
|
8003
|
+
toHaveURL: "URL",
|
|
8004
|
+
toHaveTitle: "title",
|
|
8005
|
+
toHaveScreenshot: "screenshot",
|
|
8006
|
+
toMatchAriaSnapshot: "ARIA snapshot"
|
|
8007
|
+
};
|
|
8008
|
+
var NETWORK_ERRORS = {
|
|
8009
|
+
ERR_CONNECTION_REFUSED: "Connection refused loading",
|
|
8010
|
+
ERR_CONNECTION_RESET: "Connection reset loading",
|
|
8011
|
+
ERR_CONNECTION_CLOSED: "Connection closed loading",
|
|
8012
|
+
ERR_CONNECTION_TIMED_OUT: "Connection timed out loading",
|
|
8013
|
+
ERR_TIMED_OUT: "Connection timed out loading",
|
|
8014
|
+
ERR_NAME_NOT_RESOLVED: "DNS lookup failed for",
|
|
8015
|
+
ERR_INTERNET_DISCONNECTED: "No network while loading",
|
|
8016
|
+
ERR_ADDRESS_UNREACHABLE: "Address unreachable loading",
|
|
8017
|
+
ERR_ABORTED: "Navigation aborted loading",
|
|
8018
|
+
ERR_EMPTY_RESPONSE: "Empty response loading",
|
|
8019
|
+
ERR_TOO_MANY_REDIRECTS: "Too many redirects loading",
|
|
8020
|
+
ERR_SSL_PROTOCOL_ERROR: "TLS error loading",
|
|
8021
|
+
ERR_CERT_AUTHORITY_INVALID: "Untrusted certificate loading",
|
|
8022
|
+
ERR_CERT_COMMON_NAME_INVALID: "Certificate name mismatch loading",
|
|
8023
|
+
ERR_CERT_DATE_INVALID: "Expired certificate loading",
|
|
8024
|
+
ERR_BLOCKED_BY_CLIENT: "Request blocked loading",
|
|
8025
|
+
ERR_FAILED: "Request failed loading",
|
|
8026
|
+
ERR_HTTP_RESPONSE_CODE_FAILURE: "HTTP error loading",
|
|
8027
|
+
NS_ERROR_CONNECTION_REFUSED: "Connection refused loading",
|
|
8028
|
+
NS_ERROR_UNKNOWN_HOST: "DNS lookup failed for",
|
|
8029
|
+
NS_ERROR_NET_TIMEOUT: "Connection timed out loading",
|
|
8030
|
+
NS_ERROR_OFFLINE: "No network while loading",
|
|
8031
|
+
NS_ERROR_ABORT: "Navigation aborted loading",
|
|
8032
|
+
NS_BINDING_ABORTED: "Navigation aborted loading"
|
|
8033
|
+
};
|
|
8034
|
+
function formatTimeout(ms) {
|
|
8035
|
+
if (ms < 1e3) return `${ms} ms`;
|
|
8036
|
+
const seconds = ms / 1e3;
|
|
8037
|
+
const rounded = Math.round(seconds * 10) / 10;
|
|
8038
|
+
return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded} s`;
|
|
8039
|
+
}
|
|
8040
|
+
function routeOf(url) {
|
|
8041
|
+
try {
|
|
8042
|
+
const parsed = new URL(url);
|
|
8043
|
+
return `${parsed.pathname}${parsed.search}` || "/";
|
|
8044
|
+
} catch {
|
|
8045
|
+
return url;
|
|
8046
|
+
}
|
|
8047
|
+
}
|
|
8048
|
+
function truncateValue(value, max) {
|
|
8049
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
8050
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
8051
|
+
}
|
|
8052
|
+
function countNoun(locator, count) {
|
|
8053
|
+
const role = locator ? /getByRole\(\s*['"]([a-z]+)['"]/.exec(locator)?.[1] : null;
|
|
8054
|
+
const noun = role ?? "element";
|
|
8055
|
+
if (count === 1) return noun;
|
|
8056
|
+
return noun.endsWith("s") ? noun : noun.endsWith("x") || noun.endsWith("ch") ? `${noun}es` : `${noun}s`;
|
|
5489
8057
|
}
|
|
5490
8058
|
function textOfGetByText(locator) {
|
|
5491
8059
|
if (!locator) return null;
|
|
@@ -5854,7 +8422,7 @@ function testLocation(test) {
|
|
|
5854
8422
|
return `${testFile(test)}:${test.location.line}:${test.location.column}`;
|
|
5855
8423
|
}
|
|
5856
8424
|
function testFile(test) {
|
|
5857
|
-
return
|
|
8425
|
+
return path23.relative(process.cwd(), test.location.file).split(path23.sep).join("/");
|
|
5858
8426
|
}
|
|
5859
8427
|
var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
5860
8428
|
constructor(rawOptions = {}) {
|
|
@@ -5882,6 +8450,20 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5882
8450
|
this.filterDetails = null;
|
|
5883
8451
|
/** Configured `maxFailures` (0 = unlimited) — disambiguates an interrupted run's unrun reason. */
|
|
5884
8452
|
this.maxFailures = 0;
|
|
8453
|
+
/** The resource censuses the capture fixtures attached, one per test attempt. */
|
|
8454
|
+
this.resourceCensuses = [];
|
|
8455
|
+
/** Lifecycle steps of the tests that ran without the fixtures. */
|
|
8456
|
+
this.lifecycleTallies = [];
|
|
8457
|
+
/** The user fixtures set up by the tests that left a page unused, by test id. */
|
|
8458
|
+
this.fixturesByTest = /* @__PURE__ */ new Map();
|
|
8459
|
+
/** The file workers append their shutdown census to, set in `onBegin`. */
|
|
8460
|
+
this.resourcesFile = null;
|
|
8461
|
+
/** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
|
|
8462
|
+
this.sampler = null;
|
|
8463
|
+
/** Bytes of the files the tests attached, by kind. */
|
|
8464
|
+
this.artifactBytes = emptyArtifacts();
|
|
8465
|
+
/** The run's resource report for the dashboard, built in `onEnd`. */
|
|
8466
|
+
this.resourceReport = null;
|
|
5885
8467
|
this.streamManager = null;
|
|
5886
8468
|
/** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
|
|
5887
8469
|
this.setupSteps = [];
|
|
@@ -5941,6 +8523,17 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5941
8523
|
this.startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5942
8524
|
this.playwrightVersion = config.version;
|
|
5943
8525
|
this.maxFailures = config.maxFailures ?? 0;
|
|
8526
|
+
if (this.options.captureResources !== false) {
|
|
8527
|
+
this.resourcesFile = path23.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
|
|
8528
|
+
process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
|
|
8529
|
+
try {
|
|
8530
|
+
this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
|
|
8531
|
+
this.sampler.start();
|
|
8532
|
+
} catch (error) {
|
|
8533
|
+
this.sampler = null;
|
|
8534
|
+
this.logger.debug(`Run sampler not started: ${errorMessage(error)}`);
|
|
8535
|
+
}
|
|
8536
|
+
}
|
|
5944
8537
|
this.logger.info(
|
|
5945
8538
|
`Starting test run for project: ${this.options.projectName} (Playwright v${this.playwrightVersion})`
|
|
5946
8539
|
);
|
|
@@ -6165,6 +8758,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
6165
8758
|
}
|
|
6166
8759
|
}
|
|
6167
8760
|
}
|
|
8761
|
+
if (this.enabled && this.options.captureResources !== false) this.collectResources(test, result, testCase);
|
|
6168
8762
|
const reachAttachment = this.options.captureCodeReach === true && this.options.collectPerformanceMetrics !== false ? result.attachments.find((a) => a.name === ATTACHMENT_NAMES.codeReach) : void 0;
|
|
6169
8763
|
if (reachAttachment?.body) {
|
|
6170
8764
|
try {
|
|
@@ -6209,6 +8803,80 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
6209
8803
|
if (this.options.liveFileUploads) this.streamManager.scheduleLiveUpload(testCase);
|
|
6210
8804
|
}
|
|
6211
8805
|
}
|
|
8806
|
+
/**
|
|
8807
|
+
* Add up the files a test attached, and keep its resource census; for a test
|
|
8808
|
+
* that ran without the capture fixtures, tally the browsers and contexts its
|
|
8809
|
+
* steps opened and closed.
|
|
8810
|
+
*/
|
|
8811
|
+
collectResources(test, result, testCase) {
|
|
8812
|
+
const artifacts = emptyArtifacts();
|
|
8813
|
+
for (const file of result.attachments ?? []) {
|
|
8814
|
+
if (!file.path || INTERNAL_ATTACHMENT_NAMES.has(file.name)) continue;
|
|
8815
|
+
try {
|
|
8816
|
+
const kind = artifactKind(file.name, file.contentType);
|
|
8817
|
+
const bytes = fs21.statSync(file.path).size;
|
|
8818
|
+
artifacts[kind] += bytes;
|
|
8819
|
+
this.artifactBytes[kind] += bytes;
|
|
8820
|
+
} catch {
|
|
8821
|
+
}
|
|
8822
|
+
}
|
|
8823
|
+
const attachment = result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.resources);
|
|
8824
|
+
const census = parseResourceCensus(attachment?.body);
|
|
8825
|
+
if (census) {
|
|
8826
|
+
this.resourceCensuses.push(census);
|
|
8827
|
+
const resources = executionResources(census, artifacts);
|
|
8828
|
+
if (resources) testCase.resources = resources;
|
|
8829
|
+
const leftIdle = census.closed.some((c) => c.used === false) || census.open.some((o) => o.used === false);
|
|
8830
|
+
if (leftIdle) this.fixturesByTest.set(test.id, userFixturesOf(result.steps ?? []));
|
|
8831
|
+
return;
|
|
8832
|
+
}
|
|
8833
|
+
if (!result.steps?.length) return;
|
|
8834
|
+
const tally = tallyLifecycleSteps(testFile(test), result.steps);
|
|
8835
|
+
if (tally.opened.length > 0 || tally.closed.context > 0 || tally.closed.browser > 0)
|
|
8836
|
+
this.lifecycleTallies.push(tally);
|
|
8837
|
+
}
|
|
8838
|
+
/**
|
|
8839
|
+
* Print what the run left open or opened for nothing, from the censuses and
|
|
8840
|
+
* the workers' shutdown file, then what the run cost the machine.
|
|
8841
|
+
*/
|
|
8842
|
+
async reportResources() {
|
|
8843
|
+
if (this.options.captureResources === false) return;
|
|
8844
|
+
const profile = this.sampler ? await this.sampler.stop() : null;
|
|
8845
|
+
const samples = this.sampler?.timedSeries() ?? null;
|
|
8846
|
+
this.sampler = null;
|
|
8847
|
+
const censuses = [...this.resourceCensuses];
|
|
8848
|
+
if (this.resourcesFile) {
|
|
8849
|
+
try {
|
|
8850
|
+
censuses.push(...parseResourceResults(fs21.readFileSync(this.resourcesFile, "utf8")));
|
|
8851
|
+
fs21.rmSync(this.resourcesFile, { force: true });
|
|
8852
|
+
} catch {
|
|
8853
|
+
}
|
|
8854
|
+
}
|
|
8855
|
+
const lines = [];
|
|
8856
|
+
try {
|
|
8857
|
+
const report = censuses.length > 0 || this.lifecycleTallies.length > 0 ? buildResourceReport({ censuses, fixturesByTest: this.fixturesByTest, tallies: this.lifecycleTallies }) : null;
|
|
8858
|
+
if (report) lines.push(...formatResourceSummary(report, readLeakCheck(this.options.leakCheck)));
|
|
8859
|
+
if (profile) {
|
|
8860
|
+
lines.push(
|
|
8861
|
+
...formatMachinePanel(profile, { artifacts: this.artifactBytes, workers: workerHealthOf(censuses) })
|
|
8862
|
+
);
|
|
8863
|
+
}
|
|
8864
|
+
if (report || profile) {
|
|
8865
|
+
this.resourceReport = resourceReportWire({
|
|
8866
|
+
report,
|
|
8867
|
+
profile,
|
|
8868
|
+
samples,
|
|
8869
|
+
censuses,
|
|
8870
|
+
artifacts: this.artifactBytes,
|
|
8871
|
+
shardIndex: this.shardInfo?.current ?? null
|
|
8872
|
+
});
|
|
8873
|
+
}
|
|
8874
|
+
} catch (error) {
|
|
8875
|
+
this.logger.debug(`Resource summary skipped: ${errorMessage(error)}`);
|
|
8876
|
+
}
|
|
8877
|
+
for (const line of lines) this.logger.info(line);
|
|
8878
|
+
emitResourceSummary(lines, this.logger);
|
|
8879
|
+
}
|
|
6212
8880
|
/**
|
|
6213
8881
|
* Synthesize `didnotrun` cases for tests Playwright planned but never reported
|
|
6214
8882
|
* (no `onTestEnd`) — typically because `maxFailures` cut the run short. These
|
|
@@ -6258,11 +8926,16 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
6258
8926
|
/** Playwright reporter hook: called when the full test run finishes */
|
|
6259
8927
|
async onEnd(result) {
|
|
6260
8928
|
if (this.listMode || !this.enabled) return;
|
|
8929
|
+
if (this.startTime === null) {
|
|
8930
|
+
this.logger.debug("The run stopped before any test began \u2014 nothing to report.");
|
|
8931
|
+
return;
|
|
8932
|
+
}
|
|
6261
8933
|
const unrunReason = resolveUnrunReason(result?.status, {
|
|
6262
8934
|
maxFailures: this.maxFailures,
|
|
6263
8935
|
failures: this.failedTests + this.timedOutTests
|
|
6264
8936
|
});
|
|
6265
8937
|
this.materializeUnrunTests(unrunReason);
|
|
8938
|
+
await this.reportResources();
|
|
6266
8939
|
try {
|
|
6267
8940
|
await this.submitter.submit(
|
|
6268
8941
|
{
|
|
@@ -6282,7 +8955,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
6282
8955
|
shardInfo: this.shardInfo,
|
|
6283
8956
|
setupSteps: this.setupSteps,
|
|
6284
8957
|
isFullRun: this.isFullRun,
|
|
6285
|
-
filterDetails: this.filterDetails
|
|
8958
|
+
filterDetails: this.filterDetails,
|
|
8959
|
+
resourceReport: this.resourceReport
|
|
6286
8960
|
},
|
|
6287
8961
|
result
|
|
6288
8962
|
);
|
|
@@ -6366,12 +9040,14 @@ async function resolveSelection(options = {}) {
|
|
|
6366
9040
|
}
|
|
6367
9041
|
|
|
6368
9042
|
// src/internal/capture/capture-fixtures.ts
|
|
6369
|
-
var
|
|
9043
|
+
var path27 = __toESM(require("path"));
|
|
6370
9044
|
var import_node_zlib2 = require("zlib");
|
|
6371
9045
|
|
|
6372
9046
|
// ../picker-dom/src/probe.ts
|
|
6373
9047
|
function probeElementAttrs(el, arg) {
|
|
6374
9048
|
const { keep, tagRoles, inputRoles, roleSources, includeStructural } = arg;
|
|
9049
|
+
const countMatches = arg.countMatches !== false;
|
|
9050
|
+
const testIdAttr = arg.testIdAttribute || "data-testid";
|
|
6375
9051
|
const FIELD_TAGS = ["input", "select", "textarea"];
|
|
6376
9052
|
const textWithout = (root, skip) => {
|
|
6377
9053
|
if (root === skip) return "";
|
|
@@ -6396,42 +9072,45 @@ function probeElementAttrs(el, arg) {
|
|
|
6396
9072
|
}
|
|
6397
9073
|
};
|
|
6398
9074
|
const attrMap = {};
|
|
6399
|
-
|
|
9075
|
+
const keys = arg.testIdAttribute && keep.indexOf(arg.testIdAttribute) === -1 ? [...keep, arg.testIdAttribute] : keep;
|
|
9076
|
+
for (const key of keys) {
|
|
6400
9077
|
const v = el.getAttribute(key) ?? el[key];
|
|
6401
9078
|
attrMap[key] = typeof v === "string" ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
|
|
6402
9079
|
}
|
|
6403
9080
|
const r = el.getBoundingClientRect();
|
|
6404
9081
|
const selectorCounts = {};
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
const
|
|
6430
|
-
|
|
9082
|
+
if (countMatches) {
|
|
9083
|
+
try {
|
|
9084
|
+
const doc = el.ownerDocument;
|
|
9085
|
+
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
9086
|
+
const count = (sel) => {
|
|
9087
|
+
try {
|
|
9088
|
+
return doc.querySelectorAll(sel).length;
|
|
9089
|
+
} catch {
|
|
9090
|
+
return void 0;
|
|
9091
|
+
}
|
|
9092
|
+
};
|
|
9093
|
+
if (attrMap[testIdAttr]) {
|
|
9094
|
+
selectorCounts.testId = count(`[${testIdAttr}=${JSON.stringify(attrMap[testIdAttr])}]`);
|
|
9095
|
+
}
|
|
9096
|
+
if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
|
|
9097
|
+
if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
|
|
9098
|
+
if (attrMap["placeholder"]) {
|
|
9099
|
+
selectorCounts.placeholder = count(`[placeholder=${JSON.stringify(attrMap["placeholder"])}]`);
|
|
9100
|
+
}
|
|
9101
|
+
if (attrMap["alt"]) selectorCounts.alt = count(`[alt=${JSON.stringify(attrMap["alt"])}]`);
|
|
9102
|
+
if (attrMap["title"]) selectorCounts.title = count(`[title=${JSON.stringify(attrMap["title"])}]`);
|
|
9103
|
+
const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
|
|
9104
|
+
if (classList.length > 0) {
|
|
9105
|
+
const classCounts = {};
|
|
9106
|
+
for (const cls of classList) {
|
|
9107
|
+
const n = count(`.${cssEsc(cls)}`);
|
|
9108
|
+
if (n !== void 0) classCounts[cls] = n;
|
|
9109
|
+
}
|
|
9110
|
+
selectorCounts.classes = classCounts;
|
|
6431
9111
|
}
|
|
6432
|
-
|
|
9112
|
+
} catch {
|
|
6433
9113
|
}
|
|
6434
|
-
} catch {
|
|
6435
9114
|
}
|
|
6436
9115
|
let rolePosition = null;
|
|
6437
9116
|
const ancestors = [];
|
|
@@ -6448,7 +9127,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6448
9127
|
};
|
|
6449
9128
|
const roleMemo = /* @__PURE__ */ new Map();
|
|
6450
9129
|
const textMemo = /* @__PURE__ */ new Map();
|
|
6451
|
-
const
|
|
9130
|
+
const roleOf2 = (n) => {
|
|
6452
9131
|
const cached = roleMemo.get(n);
|
|
6453
9132
|
if (cached !== void 0) return cached;
|
|
6454
9133
|
let role;
|
|
@@ -6471,7 +9150,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6471
9150
|
const al = n.getAttribute("aria-level");
|
|
6472
9151
|
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
6473
9152
|
};
|
|
6474
|
-
const targetRole =
|
|
9153
|
+
const targetRole = roleOf2(el);
|
|
6475
9154
|
const targetLevel = targetRole === "heading" ? levelOf(el) : null;
|
|
6476
9155
|
const nameOf = (n) => {
|
|
6477
9156
|
const labelledBy = !!n.getAttribute("aria-labelledby");
|
|
@@ -6520,8 +9199,8 @@ function probeElementAttrs(el, arg) {
|
|
|
6520
9199
|
}
|
|
6521
9200
|
return total;
|
|
6522
9201
|
};
|
|
6523
|
-
const nodes = doc.querySelectorAll(roleSources);
|
|
6524
|
-
const nodesUsable = nodes.length <= 4e3;
|
|
9202
|
+
const nodes = countMatches ? doc.querySelectorAll(roleSources) : [];
|
|
9203
|
+
const nodesUsable = countMatches && nodes.length <= 4e3;
|
|
6525
9204
|
const rolesUsable = !!targetRole && nodesUsable;
|
|
6526
9205
|
if (rolesUsable) {
|
|
6527
9206
|
let roleCountAll = 0;
|
|
@@ -6531,7 +9210,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6531
9210
|
let visibleRoleNameCount = 0;
|
|
6532
9211
|
for (let i = 0; i < nodes.length; i++) {
|
|
6533
9212
|
const n = nodes[i];
|
|
6534
|
-
if (
|
|
9213
|
+
if (roleOf2(n) !== targetRole) continue;
|
|
6535
9214
|
if (n === el) index = roleCountAll;
|
|
6536
9215
|
roleCountAll++;
|
|
6537
9216
|
if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
|
|
@@ -6555,17 +9234,18 @@ function probeElementAttrs(el, arg) {
|
|
|
6555
9234
|
};
|
|
6556
9235
|
}
|
|
6557
9236
|
}
|
|
6558
|
-
if (textNeedle) {
|
|
9237
|
+
if (textNeedle && countMatches) {
|
|
6559
9238
|
const textCount = countTextOwners(doc.body || doc.documentElement, 4e3);
|
|
6560
9239
|
if (textCount >= 0) selectorCounts.text = textCount;
|
|
6561
9240
|
}
|
|
6562
|
-
if (rolesUsable || textNeedle) {
|
|
9241
|
+
if (rolesUsable || textNeedle || !countMatches) {
|
|
6563
9242
|
const CONTAINER_TAGS = ["form", "nav", "main", "article", "section", "dialog", "table", "li", "tr"];
|
|
6564
9243
|
const NOISY_DATA_ATTR = /^data-(v-[0-9a-f]+|reactid|react-checksum|svelte-\w+|ng-\w+|ember\w*)$/i;
|
|
6565
9244
|
const POSITIONAL_DATA_ATTR = /^data-(index|idx|i|key|row|rownum|col|column|position|pos|order|sort|offset|page)$/i;
|
|
6566
9245
|
const TEST_DATA_ATTRS2 = ["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"];
|
|
9246
|
+
const testIdName = testIdAttr.toLowerCase();
|
|
6567
9247
|
const usableDataAttr = (name, value) => {
|
|
6568
|
-
if (!name || name.slice(0, 5) !== "data-" || name ===
|
|
9248
|
+
if (!name || name.slice(0, 5) !== "data-" || name.toLowerCase() === testIdName) return false;
|
|
6569
9249
|
if (NOISY_DATA_ATTR.test(name) || POSITIONAL_DATA_ATTR.test(name)) return false;
|
|
6570
9250
|
return !!value && value.length <= 120;
|
|
6571
9251
|
};
|
|
@@ -6584,7 +9264,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6584
9264
|
};
|
|
6585
9265
|
const docRoleCount = (role) => {
|
|
6586
9266
|
let c = 0;
|
|
6587
|
-
for (let i = 0; i < nodes.length; i++) if (
|
|
9267
|
+
for (let i = 0; i < nodes.length; i++) if (roleOf2(nodes[i]) === role) c++;
|
|
6588
9268
|
return c;
|
|
6589
9269
|
};
|
|
6590
9270
|
const rawText = (n) => (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
@@ -6595,7 +9275,13 @@ function probeElementAttrs(el, arg) {
|
|
|
6595
9275
|
return digits <= letters;
|
|
6596
9276
|
};
|
|
6597
9277
|
const usableDiscriminator = (t) => !!t && t.length <= 60 && t !== targetText && namesRatherThanReports(t);
|
|
9278
|
+
const hasAtMost = (root, cap) => {
|
|
9279
|
+
const walker = doc.createTreeWalker(root, 1);
|
|
9280
|
+
for (let n = 0; walker.nextNode(); n++) if (n >= cap) return false;
|
|
9281
|
+
return true;
|
|
9282
|
+
};
|
|
6598
9283
|
const discriminatingText = (anc) => {
|
|
9284
|
+
if (!countMatches && !hasAtMost(anc, 200)) return null;
|
|
6599
9285
|
const heading = anc.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]');
|
|
6600
9286
|
if (heading) {
|
|
6601
9287
|
const t = rawText(heading);
|
|
@@ -6616,7 +9302,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6616
9302
|
const needle = text.toLowerCase();
|
|
6617
9303
|
let c = 0;
|
|
6618
9304
|
for (let i = 0; i < nodes.length; i++) {
|
|
6619
|
-
if (
|
|
9305
|
+
if (roleOf2(nodes[i]) !== role) continue;
|
|
6620
9306
|
if (normText(nodes[i]).indexOf(needle) !== -1) c++;
|
|
6621
9307
|
}
|
|
6622
9308
|
return c;
|
|
@@ -6627,7 +9313,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6627
9313
|
depth++;
|
|
6628
9314
|
const tag = (node.tagName || "").toLowerCase();
|
|
6629
9315
|
if (tag === "body" || tag === "html") break;
|
|
6630
|
-
const testId = node.getAttribute(
|
|
9316
|
+
const testId = node.getAttribute(testIdAttr);
|
|
6631
9317
|
const id = node.getAttribute("id");
|
|
6632
9318
|
const explicitRole = node.getAttribute("role");
|
|
6633
9319
|
const ariaLabel = node.getAttribute("aria-label");
|
|
@@ -6641,16 +9327,16 @@ function probeElementAttrs(el, arg) {
|
|
|
6641
9327
|
scopedRoleCount = 0;
|
|
6642
9328
|
for (let i = 0; i < scoped.length; i++) {
|
|
6643
9329
|
const n = scoped[i];
|
|
6644
|
-
if (
|
|
9330
|
+
if (roleOf2(n) !== targetRole) continue;
|
|
6645
9331
|
if (targetLevel != null && levelOf(n) !== targetLevel) continue;
|
|
6646
9332
|
scopedRoleCount++;
|
|
6647
9333
|
}
|
|
6648
9334
|
}
|
|
6649
9335
|
}
|
|
6650
|
-
const scopedTextCount = countTextOwners(node, 2e3);
|
|
9336
|
+
const scopedTextCount = countMatches ? countTextOwners(node, 2e3) : -1;
|
|
6651
9337
|
let filterText = null;
|
|
6652
9338
|
try {
|
|
6653
|
-
if (anchorRole && nodesUsable) filterText = discriminatingText(node);
|
|
9339
|
+
if (anchorRole && (nodesUsable || !countMatches)) filterText = discriminatingText(node);
|
|
6654
9340
|
} catch {
|
|
6655
9341
|
filterText = null;
|
|
6656
9342
|
}
|
|
@@ -6663,11 +9349,13 @@ function probeElementAttrs(el, arg) {
|
|
|
6663
9349
|
ariaLabel: ariaLabel || null,
|
|
6664
9350
|
...scopedRoleCount >= 0 ? { scopedRoleCount } : {},
|
|
6665
9351
|
...scopedTextCount >= 0 ? { scopedTextCount } : {},
|
|
6666
|
-
...testId ? { testIdCount: count(`[
|
|
6667
|
-
...id ? { idCount: count(`#${cssEsc(id)}`) } : {},
|
|
9352
|
+
...testId && countMatches ? { testIdCount: count(`[${testIdAttr}=${JSON.stringify(testId)}]`) } : {},
|
|
9353
|
+
...id && countMatches ? { idCount: count(`#${cssEsc(id)}`) } : {},
|
|
6668
9354
|
...anchorRole && nodesUsable ? { roleCount: docRoleCount(anchorRole) } : {},
|
|
6669
|
-
...filterText ? { filterText
|
|
6670
|
-
...
|
|
9355
|
+
...filterText ? { filterText } : {},
|
|
9356
|
+
...filterText && countMatches ? { filterRoleCount: filterMatchCount(anchorRole, filterText) } : {},
|
|
9357
|
+
...dataAttr ? { dataAttr } : {},
|
|
9358
|
+
...dataAttr && countMatches ? { dataAttrCount: count(`[${dataAttr.name}=${JSON.stringify(dataAttr.value)}]`) } : {}
|
|
6671
9359
|
});
|
|
6672
9360
|
}
|
|
6673
9361
|
node = node.parentElement;
|
|
@@ -6705,6 +9393,17 @@ function installPickerOverlay(arg) {
|
|
|
6705
9393
|
return;
|
|
6706
9394
|
}
|
|
6707
9395
|
const Z = 2147483600;
|
|
9396
|
+
const testIdAttr = arg.testIdAttribute || "data-testid";
|
|
9397
|
+
const words = {
|
|
9398
|
+
banner: "Piwi inspector \u2014 click any element to generate locators for it",
|
|
9399
|
+
keys: "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
9400
|
+
keysHovering: "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
9401
|
+
analyzing: "Analyzing element\u2026"
|
|
9402
|
+
};
|
|
9403
|
+
for (const key of Object.keys(words)) {
|
|
9404
|
+
const given = arg.strings?.[key];
|
|
9405
|
+
if (typeof given === "string" && given) words[key] = given;
|
|
9406
|
+
}
|
|
6708
9407
|
const highlight = doc.createElement("div");
|
|
6709
9408
|
highlight.id = "__piwi_picker_highlight";
|
|
6710
9409
|
highlight.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};display:none;box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55),inset 0 0 0 1px rgba(255,255,255,.5);`;
|
|
@@ -6729,27 +9428,53 @@ function installPickerOverlay(arg) {
|
|
|
6729
9428
|
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
6730
9429
|
const hlLocator = (expr) => `<code style="font-family:${MONO}">${hlTokens(expr)}</code>`;
|
|
6731
9430
|
const head = doc.createElement("div");
|
|
6732
|
-
|
|
9431
|
+
if (arg.transport === "postMessage") head.textContent = "Click an element to generate locators";
|
|
9432
|
+
else if (arg.failing)
|
|
9433
|
+
head.innerHTML = `Piwi locator picker \u2014 click the element that should replace ${hlLocator(arg.failing)}`;
|
|
9434
|
+
else head.textContent = words.banner;
|
|
6733
9435
|
const locatorLine = doc.createElement("div");
|
|
6734
9436
|
locatorLine.id = "__piwi_picker_locator";
|
|
6735
9437
|
locatorLine.style.cssText = `display:none;margin-top:7px;padding:5px 9px;border-radius:7px;background:#0b1120;border:1px solid #4c1d95;font:13.5px/1.55 ${MONO};word-break:break-word;overflow-wrap:anywhere;`;
|
|
6736
9438
|
const foot = doc.createElement("div");
|
|
6737
9439
|
foot.id = "__piwi_picker_foot";
|
|
6738
9440
|
foot.style.cssText = "color:#9ca3af;margin-top:6px;font-size:12px;";
|
|
6739
|
-
foot.textContent =
|
|
9441
|
+
foot.textContent = words.keys;
|
|
6740
9442
|
banner.appendChild(head);
|
|
6741
9443
|
banner.appendChild(locatorLine);
|
|
6742
9444
|
banner.appendChild(foot);
|
|
6743
9445
|
const label = doc.createElement("div");
|
|
6744
9446
|
label.id = "__piwi_picker_label";
|
|
6745
9447
|
label.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(620px,92vw);background:#0b1120;color:#f9fafb;border:1px solid #7c3aed;border-radius:7px;padding:4px 8px;font:12.5px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
9448
|
+
const mountParent = () => {
|
|
9449
|
+
let modal = null;
|
|
9450
|
+
try {
|
|
9451
|
+
modal = doc.querySelector("dialog:modal");
|
|
9452
|
+
} catch {
|
|
9453
|
+
modal = null;
|
|
9454
|
+
}
|
|
9455
|
+
if (!modal) return doc.body;
|
|
9456
|
+
const s = g.getComputedStyle(modal);
|
|
9457
|
+
const holdsFixed = s.transform !== "none" || s.perspective !== "none" || s.filter !== "none" || /\b(paint|layout|strict|content)\b/.test(s.contain) || /\b(transform|perspective|filter)\b/.test(s.willChange);
|
|
9458
|
+
return holdsFixed ? doc.body : modal;
|
|
9459
|
+
};
|
|
9460
|
+
const mount = (...nodes) => {
|
|
9461
|
+
const parent = mountParent();
|
|
9462
|
+
for (const node of nodes) parent.appendChild(node);
|
|
9463
|
+
if (parent !== doc.body) {
|
|
9464
|
+
parent.addEventListener(
|
|
9465
|
+
"close",
|
|
9466
|
+
() => {
|
|
9467
|
+
for (const node of nodes) if (node.parentNode === parent) doc.body.appendChild(node);
|
|
9468
|
+
},
|
|
9469
|
+
{ once: true }
|
|
9470
|
+
);
|
|
9471
|
+
}
|
|
9472
|
+
};
|
|
9473
|
+
mount(highlight, label, banner);
|
|
6749
9474
|
const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
6750
9475
|
const describe = (el) => {
|
|
6751
9476
|
const tag = (el.tagName || "?").toLowerCase();
|
|
6752
|
-
const testId = el.getAttribute && el.getAttribute(
|
|
9477
|
+
const testId = el.getAttribute && el.getAttribute(testIdAttr);
|
|
6753
9478
|
if (testId) return `getByTestId('${escJs(testId)}')`;
|
|
6754
9479
|
if (el.labels && el.labels.length > 0) {
|
|
6755
9480
|
const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
@@ -6784,7 +9509,7 @@ function installPickerOverlay(arg) {
|
|
|
6784
9509
|
const el = chain2[i];
|
|
6785
9510
|
const tag = (el.tagName || "").toLowerCase();
|
|
6786
9511
|
if (ACTIONABLE_TAGS.includes(tag)) return i;
|
|
6787
|
-
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute(
|
|
9512
|
+
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute(testIdAttr))) return i;
|
|
6788
9513
|
}
|
|
6789
9514
|
return 0;
|
|
6790
9515
|
};
|
|
@@ -6840,7 +9565,7 @@ function installPickerOverlay(arg) {
|
|
|
6840
9565
|
locatorLine.style.display = "block";
|
|
6841
9566
|
}
|
|
6842
9567
|
placeLabel(r);
|
|
6843
|
-
foot.textContent =
|
|
9568
|
+
foot.textContent = words.keysHovering;
|
|
6844
9569
|
};
|
|
6845
9570
|
const redescribe = () => {
|
|
6846
9571
|
labeled = null;
|
|
@@ -6903,6 +9628,13 @@ function installPickerOverlay(arg) {
|
|
|
6903
9628
|
banner.remove();
|
|
6904
9629
|
if (g.__piwiRedescribe === redescribe) delete g.__piwiRedescribe;
|
|
6905
9630
|
};
|
|
9631
|
+
const teardown = () => {
|
|
9632
|
+
removePickingListeners();
|
|
9633
|
+
removeSuppressed();
|
|
9634
|
+
doc.removeEventListener("click", stop, true);
|
|
9635
|
+
doc.removeEventListener("keydown", stop, true);
|
|
9636
|
+
cleanup();
|
|
9637
|
+
};
|
|
6906
9638
|
const reportPicked = (el) => {
|
|
6907
9639
|
removePickingListeners();
|
|
6908
9640
|
highlight.style.display = "none";
|
|
@@ -6913,13 +9645,13 @@ function installPickerOverlay(arg) {
|
|
|
6913
9645
|
g.__piwiSnapshotExtras?.onPick?.();
|
|
6914
9646
|
doc.addEventListener("click", stop, true);
|
|
6915
9647
|
doc.addEventListener("keydown", stop, true);
|
|
6916
|
-
foot.textContent =
|
|
9648
|
+
foot.textContent = words.analyzing;
|
|
6917
9649
|
g.parent.postMessage({ type: "elementPicked", attrs }, "*");
|
|
6918
9650
|
} else {
|
|
6919
9651
|
removeSuppressed();
|
|
6920
9652
|
g.__piwiPickedElement = el;
|
|
6921
9653
|
g.__piwiPickState = "picked";
|
|
6922
|
-
foot.textContent =
|
|
9654
|
+
foot.textContent = words.analyzing;
|
|
6923
9655
|
}
|
|
6924
9656
|
};
|
|
6925
9657
|
const reportSkipped = () => {
|
|
@@ -6967,7 +9699,7 @@ function installPickerOverlay(arg) {
|
|
|
6967
9699
|
}
|
|
6968
9700
|
});
|
|
6969
9701
|
};
|
|
6970
|
-
g.__piwiPickCleanup =
|
|
9702
|
+
g.__piwiPickCleanup = teardown;
|
|
6971
9703
|
doc.addEventListener("mousemove", onMove, true);
|
|
6972
9704
|
doc.addEventListener("click", onClick, true);
|
|
6973
9705
|
doc.addEventListener("keydown", onKey, true);
|
|
@@ -6989,7 +9721,27 @@ function showAnchorPicker(arg) {
|
|
|
6989
9721
|
}
|
|
6990
9722
|
const Z = 2147483600;
|
|
6991
9723
|
const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
|
|
6992
|
-
const
|
|
9724
|
+
const testIdAttr = arg.testIdAttribute || "data-testid";
|
|
9725
|
+
const words = {
|
|
9726
|
+
title: "Scope to stable parents (optional)",
|
|
9727
|
+
hint: "Pick one or more parents to anchor the locator to. Hover a row to see the parent.",
|
|
9728
|
+
noneSelected: "No parents selected \u2014 standard alternatives only.",
|
|
9729
|
+
matchesOne: "\u2713 Selection matches exactly 1 element",
|
|
9730
|
+
matchesMany: "\u2717 Selection matches {count} elements",
|
|
9731
|
+
countUnavailable: "Match count unavailable",
|
|
9732
|
+
containsOne: "contains exactly 1 matching element",
|
|
9733
|
+
containsMany: "contains {count} matching elements",
|
|
9734
|
+
needsTestId: `add a ${testIdAttr} to make this usable`,
|
|
9735
|
+
noHook: "no stable hook",
|
|
9736
|
+
use: "Use selected parents",
|
|
9737
|
+
skip: "Skip (Esc)"
|
|
9738
|
+
};
|
|
9739
|
+
for (const key of Object.keys(words)) {
|
|
9740
|
+
const given = arg.strings?.[key];
|
|
9741
|
+
if (typeof given === "string" && given) words[key] = given;
|
|
9742
|
+
}
|
|
9743
|
+
const counted = (template, n) => template.replace("{count}", String(n));
|
|
9744
|
+
const roleOf2 = (n) => {
|
|
6993
9745
|
const explicit = n.getAttribute && n.getAttribute("role");
|
|
6994
9746
|
if (explicit) return explicit;
|
|
6995
9747
|
const tag = (n.tagName || "").toLowerCase();
|
|
@@ -7004,15 +9756,17 @@ function showAnchorPicker(arg) {
|
|
|
7004
9756
|
const al = n.getAttribute && n.getAttribute("aria-level");
|
|
7005
9757
|
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
7006
9758
|
};
|
|
9759
|
+
const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
|
|
7007
9760
|
const leafMatches = (scope) => {
|
|
7008
9761
|
try {
|
|
7009
|
-
if (leafTestId) return scope.querySelectorAll(`[
|
|
9762
|
+
if (leafTestId) return scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
|
|
9763
|
+
if (!leafCountable) return -1;
|
|
7010
9764
|
const nodes = scope.querySelectorAll(roleSources);
|
|
7011
9765
|
if (nodes.length > 2e3) return -1;
|
|
7012
9766
|
let matched = 0;
|
|
7013
9767
|
for (let i = 0; i < nodes.length; i++) {
|
|
7014
9768
|
const n = nodes[i];
|
|
7015
|
-
if (
|
|
9769
|
+
if (roleOf2(n) !== leafRole) continue;
|
|
7016
9770
|
if (leafLevel != null && levelOf(n) !== leafLevel) continue;
|
|
7017
9771
|
matched++;
|
|
7018
9772
|
}
|
|
@@ -7042,12 +9796,12 @@ function showAnchorPicker(arg) {
|
|
|
7042
9796
|
depth++;
|
|
7043
9797
|
const tag = (node.tagName || "").toLowerCase();
|
|
7044
9798
|
if (tag === "body" || tag === "html") break;
|
|
7045
|
-
const testId = node.getAttribute(
|
|
9799
|
+
const testId = node.getAttribute(testIdAttr);
|
|
7046
9800
|
const id = node.getAttribute("id");
|
|
7047
9801
|
const ariaLabel = node.getAttribute("aria-label");
|
|
7048
|
-
const role =
|
|
9802
|
+
const role = roleOf2(node);
|
|
7049
9803
|
const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
|
|
7050
|
-
if (testId) info.testIdCount = count(`[
|
|
9804
|
+
if (testId) info.testIdCount = count(`[${testIdAttr}=${JSON.stringify(testId)}]`);
|
|
7051
9805
|
if (id) {
|
|
7052
9806
|
try {
|
|
7053
9807
|
info.idCount = count(`#${doc.defaultView.CSS.escape(id)}`);
|
|
@@ -7058,7 +9812,7 @@ function showAnchorPicker(arg) {
|
|
|
7058
9812
|
let roleCount = 0;
|
|
7059
9813
|
let labeledCount = 0;
|
|
7060
9814
|
for (const n of roleNodes) {
|
|
7061
|
-
if (
|
|
9815
|
+
if (roleOf2(n) !== role) continue;
|
|
7062
9816
|
roleCount++;
|
|
7063
9817
|
if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
|
|
7064
9818
|
}
|
|
@@ -7066,7 +9820,7 @@ function showAnchorPicker(arg) {
|
|
|
7066
9820
|
if (ariaLabel) info.labeledRoleCount = labeledCount;
|
|
7067
9821
|
}
|
|
7068
9822
|
info.scopedLeafCount = leafMatches(node);
|
|
7069
|
-
const hookLabel = testId ?
|
|
9823
|
+
const hookLabel = testId ? `${testIdAttr}="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : words.noHook;
|
|
7070
9824
|
rows.push({
|
|
7071
9825
|
node,
|
|
7072
9826
|
info,
|
|
@@ -7088,13 +9842,13 @@ function showAnchorPicker(arg) {
|
|
|
7088
9842
|
const pr = el.getBoundingClientRect();
|
|
7089
9843
|
pickedOutline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55);left:${pr.left}px;top:${pr.top}px;width:${pr.width}px;height:${pr.height}px;`;
|
|
7090
9844
|
const panel = doc.createElement("div");
|
|
7091
|
-
panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:340px;max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
|
|
9845
|
+
panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:min(340px,calc(100vw - 56px));max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
|
|
7092
9846
|
const title = doc.createElement("div");
|
|
7093
9847
|
title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
|
|
7094
|
-
title.textContent =
|
|
9848
|
+
title.textContent = words.title;
|
|
7095
9849
|
const sub = doc.createElement("div");
|
|
7096
9850
|
sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
|
|
7097
|
-
sub.textContent =
|
|
9851
|
+
sub.textContent = words.hint;
|
|
7098
9852
|
panel.appendChild(title);
|
|
7099
9853
|
panel.appendChild(sub);
|
|
7100
9854
|
const selected = /* @__PURE__ */ new Set();
|
|
@@ -7102,12 +9856,12 @@ function showAnchorPicker(arg) {
|
|
|
7102
9856
|
footer.style.cssText = "margin:10px 0;font-weight:600;";
|
|
7103
9857
|
const segMatches = (scope, info) => {
|
|
7104
9858
|
try {
|
|
7105
|
-
if (info.testId) return Array.from(scope.querySelectorAll(`[
|
|
9859
|
+
if (info.testId) return Array.from(scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(info.testId)}]`));
|
|
7106
9860
|
if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
|
|
7107
9861
|
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
7108
9862
|
if (nodes.length > 2e3) return [];
|
|
7109
9863
|
return nodes.filter(
|
|
7110
|
-
(n) =>
|
|
9864
|
+
(n) => roleOf2(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
|
|
7111
9865
|
);
|
|
7112
9866
|
} catch {
|
|
7113
9867
|
return [];
|
|
@@ -7126,14 +9880,15 @@ function showAnchorPicker(arg) {
|
|
|
7126
9880
|
let total = 0;
|
|
7127
9881
|
for (const s of scopes) {
|
|
7128
9882
|
const c = leafMatches(s);
|
|
7129
|
-
if (c
|
|
9883
|
+
if (c < 0) return -1;
|
|
9884
|
+
total += c;
|
|
7130
9885
|
if (total > 50) return total;
|
|
7131
9886
|
}
|
|
7132
9887
|
return total;
|
|
7133
9888
|
};
|
|
7134
9889
|
const refreshFooter = () => {
|
|
7135
9890
|
if (selected.size === 0) {
|
|
7136
|
-
footer.textContent =
|
|
9891
|
+
footer.textContent = words.noneSelected;
|
|
7137
9892
|
footer.style.color = "#9ca3af";
|
|
7138
9893
|
g.__piwiPickChainCount = void 0;
|
|
7139
9894
|
return;
|
|
@@ -7141,10 +9896,10 @@ function showAnchorPicker(arg) {
|
|
|
7141
9896
|
const c = chainCount();
|
|
7142
9897
|
g.__piwiPickChainCount = c;
|
|
7143
9898
|
if (c === 1) {
|
|
7144
|
-
footer.textContent =
|
|
9899
|
+
footer.textContent = words.matchesOne;
|
|
7145
9900
|
footer.style.color = "#4ade80";
|
|
7146
9901
|
} else {
|
|
7147
|
-
footer.textContent = c < 0 ?
|
|
9902
|
+
footer.textContent = c < 0 ? words.countUnavailable : counted(words.matchesMany, c);
|
|
7148
9903
|
footer.style.color = "#fbbf24";
|
|
7149
9904
|
}
|
|
7150
9905
|
};
|
|
@@ -7161,7 +9916,8 @@ function showAnchorPicker(arg) {
|
|
|
7161
9916
|
code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
7162
9917
|
const hint = doc.createElement("span");
|
|
7163
9918
|
hint.style.cssText = "color:#9ca3af;";
|
|
7164
|
-
|
|
9919
|
+
const inside = row.info.scopedLeafCount ?? -1;
|
|
9920
|
+
hint.textContent = row.selectable ? inside === 1 ? words.containsOne : inside < 0 ? words.countUnavailable : counted(words.containsMany, inside) : words.needsTestId;
|
|
7165
9921
|
text.appendChild(code);
|
|
7166
9922
|
text.appendChild(hint);
|
|
7167
9923
|
line.appendChild(box);
|
|
@@ -7199,6 +9955,7 @@ function showAnchorPicker(arg) {
|
|
|
7199
9955
|
outline.remove();
|
|
7200
9956
|
outlineLabel.remove();
|
|
7201
9957
|
pickedOutline.remove();
|
|
9958
|
+
if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
|
|
7202
9959
|
};
|
|
7203
9960
|
const done = (state) => {
|
|
7204
9961
|
g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
|
|
@@ -7215,7 +9972,7 @@ function showAnchorPicker(arg) {
|
|
|
7215
9972
|
buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
|
|
7216
9973
|
const useBtn = doc.createElement("button");
|
|
7217
9974
|
useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
|
|
7218
|
-
useBtn.textContent =
|
|
9975
|
+
useBtn.textContent = words.use;
|
|
7219
9976
|
useBtn.addEventListener("click", (e) => {
|
|
7220
9977
|
e.preventDefault();
|
|
7221
9978
|
e.stopImmediatePropagation();
|
|
@@ -7223,7 +9980,7 @@ function showAnchorPicker(arg) {
|
|
|
7223
9980
|
});
|
|
7224
9981
|
const skipBtn = doc.createElement("button");
|
|
7225
9982
|
skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
|
|
7226
|
-
skipBtn.textContent =
|
|
9983
|
+
skipBtn.textContent = words.skip;
|
|
7227
9984
|
skipBtn.addEventListener("click", (e) => {
|
|
7228
9985
|
e.preventDefault();
|
|
7229
9986
|
e.stopImmediatePropagation();
|
|
@@ -7235,10 +9992,32 @@ function showAnchorPicker(arg) {
|
|
|
7235
9992
|
refreshFooter();
|
|
7236
9993
|
g.__piwiAnchorCleanup = cleanup;
|
|
7237
9994
|
doc.addEventListener("keydown", onKey, true);
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
9995
|
+
const mountParent = () => {
|
|
9996
|
+
let modal = null;
|
|
9997
|
+
try {
|
|
9998
|
+
modal = doc.querySelector("dialog:modal");
|
|
9999
|
+
} catch {
|
|
10000
|
+
modal = null;
|
|
10001
|
+
}
|
|
10002
|
+
if (!modal) return doc.body;
|
|
10003
|
+
const s = g.getComputedStyle(modal);
|
|
10004
|
+
const holdsFixed = s.transform !== "none" || s.perspective !== "none" || s.filter !== "none" || /\b(paint|layout|strict|content)\b/.test(s.contain) || /\b(transform|perspective|filter)\b/.test(s.willChange);
|
|
10005
|
+
return holdsFixed ? doc.body : modal;
|
|
10006
|
+
};
|
|
10007
|
+
const mount = (...nodes) => {
|
|
10008
|
+
const parent = mountParent();
|
|
10009
|
+
for (const node2 of nodes) parent.appendChild(node2);
|
|
10010
|
+
if (parent !== doc.body) {
|
|
10011
|
+
parent.addEventListener(
|
|
10012
|
+
"close",
|
|
10013
|
+
() => {
|
|
10014
|
+
for (const node2 of nodes) if (node2.parentNode === parent) doc.body.appendChild(node2);
|
|
10015
|
+
},
|
|
10016
|
+
{ once: true }
|
|
10017
|
+
);
|
|
10018
|
+
}
|
|
10019
|
+
};
|
|
10020
|
+
mount(pickedOutline, outline, outlineLabel, panel);
|
|
7242
10021
|
}
|
|
7243
10022
|
|
|
7244
10023
|
// ../picker-dom/src/overlay-confirm.ts
|
|
@@ -7454,7 +10233,7 @@ function isAutoGenerated(value) {
|
|
|
7454
10233
|
return false;
|
|
7455
10234
|
}
|
|
7456
10235
|
var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
|
|
7457
|
-
function generateAlternatives(attrs) {
|
|
10236
|
+
function generateAlternatives(attrs, options = {}) {
|
|
7458
10237
|
const alts = [];
|
|
7459
10238
|
const seen = /* @__PURE__ */ new Set();
|
|
7460
10239
|
const add = (loc) => {
|
|
@@ -7465,12 +10244,14 @@ function generateAlternatives(attrs) {
|
|
|
7465
10244
|
};
|
|
7466
10245
|
const { accessibleName } = attrs;
|
|
7467
10246
|
const tag = attrs.tagName;
|
|
7468
|
-
const role = resolveAriaRole(attrs);
|
|
10247
|
+
const role = options.role !== void 0 ? options.role : resolveAriaRole(attrs);
|
|
7469
10248
|
const counts = attrs.selectorCounts;
|
|
7470
10249
|
const isUnique = (n) => n == null || n <= 1;
|
|
7471
10250
|
const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
|
|
7472
10251
|
const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
|
|
7473
|
-
const
|
|
10252
|
+
const testIdAttribute = options.testIdAttribute || "data-testid";
|
|
10253
|
+
const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
|
|
10254
|
+
const testId = attr(attrs, testIdAttribute);
|
|
7474
10255
|
if (testId && isUnique(counts?.testId)) {
|
|
7475
10256
|
add({
|
|
7476
10257
|
locator: `getByTestId('${esc(testId)}')`,
|
|
@@ -7529,7 +10310,7 @@ function generateAlternatives(attrs) {
|
|
|
7529
10310
|
});
|
|
7530
10311
|
}
|
|
7531
10312
|
const id = attr(attrs, "id");
|
|
7532
|
-
if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
10313
|
+
if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
7533
10314
|
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
7534
10315
|
add({
|
|
7535
10316
|
locator: `locator('${esc(selector)}')`,
|
|
@@ -7539,7 +10320,7 @@ function generateAlternatives(attrs) {
|
|
|
7539
10320
|
});
|
|
7540
10321
|
}
|
|
7541
10322
|
const name = attr(attrs, "name");
|
|
7542
|
-
if (name && isUnique(counts?.name)) {
|
|
10323
|
+
if (name && selectable("name") && isUnique(counts?.name)) {
|
|
7543
10324
|
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
7544
10325
|
add({
|
|
7545
10326
|
locator: `locator('${esc(selector)}')`,
|
|
@@ -7584,7 +10365,7 @@ function generateAlternatives(attrs) {
|
|
|
7584
10365
|
score: scores.testId
|
|
7585
10366
|
});
|
|
7586
10367
|
}
|
|
7587
|
-
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
10368
|
+
if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
7588
10369
|
idAnchorDone = true;
|
|
7589
10370
|
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
7590
10371
|
add({
|
|
@@ -7594,7 +10375,7 @@ function generateAlternatives(attrs) {
|
|
|
7594
10375
|
score: scores.id
|
|
7595
10376
|
});
|
|
7596
10377
|
}
|
|
7597
|
-
if (!dataAnchorDone && anc.dataAttr && anc.dataAttrCount === 1) {
|
|
10378
|
+
if (!dataAnchorDone && anc.dataAttr && selectable(anc.dataAttr.name) && anc.dataAttrCount === 1) {
|
|
7598
10379
|
dataAnchorDone = true;
|
|
7599
10380
|
const { name: name2, value } = anc.dataAttr;
|
|
7600
10381
|
const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
|
|
@@ -7760,7 +10541,7 @@ function mergeCandidates(base, extra) {
|
|
|
7760
10541
|
}
|
|
7761
10542
|
|
|
7762
10543
|
// src/internal/capture/locator-healing.ts
|
|
7763
|
-
var
|
|
10544
|
+
var path24 = __toESM(require("path"));
|
|
7764
10545
|
|
|
7765
10546
|
// ../core/src/locator-fingerprint.ts
|
|
7766
10547
|
var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
|
|
@@ -8130,10 +10911,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
8130
10911
|
}
|
|
8131
10912
|
let rel = file;
|
|
8132
10913
|
try {
|
|
8133
|
-
rel =
|
|
10914
|
+
rel = path24.relative(process.cwd(), file);
|
|
8134
10915
|
} catch {
|
|
8135
10916
|
}
|
|
8136
|
-
rel = rel.split(
|
|
10917
|
+
rel = rel.split(path24.sep).join("/");
|
|
8137
10918
|
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
8138
10919
|
return `${rel}:${m[3]}:${m[4]}`;
|
|
8139
10920
|
}
|
|
@@ -8147,7 +10928,7 @@ function collectPageInventoryInPage(maxEntries) {
|
|
|
8147
10928
|
const doc = g.document;
|
|
8148
10929
|
if (!doc || !g.location) return null;
|
|
8149
10930
|
const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
8150
|
-
const
|
|
10931
|
+
const roleOf2 = (el) => {
|
|
8151
10932
|
const explicit = el.getAttribute("role");
|
|
8152
10933
|
if (explicit) return explicit.toLowerCase();
|
|
8153
10934
|
const tag = (el.tagName || "").toLowerCase();
|
|
@@ -8201,7 +10982,7 @@ function collectPageInventoryInPage(maxEntries) {
|
|
|
8201
10982
|
return text;
|
|
8202
10983
|
};
|
|
8203
10984
|
const nameOf = (el) => {
|
|
8204
|
-
const role =
|
|
10985
|
+
const role = roleOf2(el);
|
|
8205
10986
|
const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
|
|
8206
10987
|
if (!holdsValue(el, role)) chain.push(el.textContent);
|
|
8207
10988
|
chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
|
|
@@ -8219,7 +11000,7 @@ function collectPageInventoryInPage(maxEntries) {
|
|
|
8219
11000
|
const controlSelector = 'button, [role="button"], input, select, textarea, [role="checkbox"], [role="radio"], [role="tab"], [role="menuitem"], [role="combobox"], [role="switch"], [role="slider"], [role="searchbox"], [role="spinbutton"], [role="textbox"], [role="listbox"]';
|
|
8220
11001
|
for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
|
|
8221
11002
|
if (budget <= 0) break;
|
|
8222
|
-
const role =
|
|
11003
|
+
const role = roleOf2(el);
|
|
8223
11004
|
const name = nameOf(el);
|
|
8224
11005
|
if (!name) continue;
|
|
8225
11006
|
const key = `${role}\0${name}`;
|
|
@@ -8344,7 +11125,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
8344
11125
|
}
|
|
8345
11126
|
|
|
8346
11127
|
// src/internal/capture/pick-on-failure.ts
|
|
8347
|
-
var
|
|
11128
|
+
var path25 = __toESM(require("path"));
|
|
8348
11129
|
var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
|
|
8349
11130
|
function parseLeafLocatorExpression(rawExpr) {
|
|
8350
11131
|
const leaf = parseLeafLocatorCall(rawExpr);
|
|
@@ -8361,7 +11142,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
|
|
|
8361
11142
|
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
8362
11143
|
if (!parsed) continue;
|
|
8363
11144
|
const loc = err.location;
|
|
8364
|
-
const location = loc ? `${
|
|
11145
|
+
const location = loc ? `${path25.relative(process.cwd(), loc.file).split(path25.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
8365
11146
|
return { method: parsed.method, args: parsed.args, location };
|
|
8366
11147
|
}
|
|
8367
11148
|
return null;
|
|
@@ -8509,64 +11290,11 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
8509
11290
|
return true;
|
|
8510
11291
|
}
|
|
8511
11292
|
|
|
8512
|
-
// src/internal/capture/quiet-capture.ts
|
|
8513
|
-
var import_node_module = require("module");
|
|
8514
|
-
var path21 = __toESM(require("path"));
|
|
8515
|
-
var BOXED_PREFIX_EXPORTS = [
|
|
8516
|
-
[
|
|
8517
|
-
"playwright-core/lib/coreBundle",
|
|
8518
|
-
(mod) => mod?.utils?.setBoxedStackPrefixes
|
|
8519
|
-
],
|
|
8520
|
-
["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
|
|
8521
|
-
];
|
|
8522
|
-
var boxing;
|
|
8523
|
-
function resolveBoxing() {
|
|
8524
|
-
try {
|
|
8525
|
-
const root = findOwnPackageJson(__dirname)?.root;
|
|
8526
|
-
if (!root) return null;
|
|
8527
|
-
const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
|
|
8528
|
-
const playwrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
|
|
8529
|
-
const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
|
|
8530
|
-
for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
|
|
8531
|
-
let set;
|
|
8532
|
-
try {
|
|
8533
|
-
set = pick(requireFromPlaywright(moduleId));
|
|
8534
|
-
} catch {
|
|
8535
|
-
continue;
|
|
8536
|
-
}
|
|
8537
|
-
if (typeof set !== "function") continue;
|
|
8538
|
-
return {
|
|
8539
|
-
set,
|
|
8540
|
-
prefixes: [path21.dirname(playwrightPackageJson), path21.join(root, "dist") + path21.sep]
|
|
8541
|
-
};
|
|
8542
|
-
}
|
|
8543
|
-
} catch {
|
|
8544
|
-
}
|
|
8545
|
-
return null;
|
|
8546
|
-
}
|
|
8547
|
-
function boxCaptureFrames(extraPrefixes = []) {
|
|
8548
|
-
const first = boxing === void 0;
|
|
8549
|
-
if (first) boxing = resolveBoxing();
|
|
8550
|
-
if (!boxing) return;
|
|
8551
|
-
const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
|
|
8552
|
-
if (!first && added.length === 0) return;
|
|
8553
|
-
boxing.prefixes.push(...added);
|
|
8554
|
-
try {
|
|
8555
|
-
boxing.set([...boxing.prefixes]);
|
|
8556
|
-
} catch {
|
|
8557
|
-
}
|
|
8558
|
-
}
|
|
8559
|
-
function internalCall(owner, run) {
|
|
8560
|
-
const wrap = owner?._wrapApiCall;
|
|
8561
|
-
if (typeof wrap !== "function") return run();
|
|
8562
|
-
return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
|
|
8563
|
-
}
|
|
8564
|
-
|
|
8565
11293
|
// src/internal/capture/code-reach.ts
|
|
8566
11294
|
var crypto3 = __toESM(require("crypto"));
|
|
8567
|
-
var
|
|
8568
|
-
var
|
|
8569
|
-
var
|
|
11295
|
+
var fs22 = __toESM(require("fs"));
|
|
11296
|
+
var path26 = __toESM(require("path"));
|
|
11297
|
+
var import_node_child_process3 = require("child_process");
|
|
8570
11298
|
|
|
8571
11299
|
// ../core/src/source-map.ts
|
|
8572
11300
|
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
@@ -8821,24 +11549,24 @@ function repositoryRoot(from) {
|
|
|
8821
11549
|
if (cachedRepoRoot?.from === from) return cachedRepoRoot.root;
|
|
8822
11550
|
let root = from;
|
|
8823
11551
|
try {
|
|
8824
|
-
root = (0,
|
|
11552
|
+
root = (0, import_node_child_process3.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
|
|
8825
11553
|
cwd: from,
|
|
8826
11554
|
encoding: "utf-8",
|
|
8827
11555
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8828
11556
|
}).trim();
|
|
8829
11557
|
} catch {
|
|
8830
11558
|
}
|
|
8831
|
-
cachedRepoRoot = { from, root:
|
|
11559
|
+
cachedRepoRoot = { from, root: path26.resolve(root) };
|
|
8832
11560
|
return cachedRepoRoot.root;
|
|
8833
11561
|
}
|
|
8834
11562
|
function codeReachRoots(configDir, configured) {
|
|
8835
11563
|
const repoRoot = repositoryRoot(configDir);
|
|
8836
|
-
const roots = configured?.length ? configured.map((r) =>
|
|
11564
|
+
const roots = configured?.length ? configured.map((r) => path26.resolve(configDir, r)) : [configDir];
|
|
8837
11565
|
return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
|
|
8838
11566
|
}
|
|
8839
11567
|
function isFile(file) {
|
|
8840
11568
|
try {
|
|
8841
|
-
return
|
|
11569
|
+
return fs22.statSync(file).isFile();
|
|
8842
11570
|
} catch {
|
|
8843
11571
|
return false;
|
|
8844
11572
|
}
|
|
@@ -8846,10 +11574,10 @@ function isFile(file) {
|
|
|
8846
11574
|
function resolveSourcePath(source, roots) {
|
|
8847
11575
|
const normalized = normalizeSourcePath(source);
|
|
8848
11576
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
8849
|
-
if (
|
|
8850
|
-
const
|
|
11577
|
+
if (path26.isAbsolute(normalized) && isFile(normalized)) return path26.resolve(normalized);
|
|
11578
|
+
const relative13 = normalized.replace(/^\/+/, "");
|
|
8851
11579
|
for (const root of roots) {
|
|
8852
|
-
const candidate =
|
|
11580
|
+
const candidate = path26.resolve(root, relative13);
|
|
8853
11581
|
if (isFile(candidate)) return candidate;
|
|
8854
11582
|
}
|
|
8855
11583
|
return null;
|
|
@@ -8881,7 +11609,7 @@ function fileForUrl(url, roots, bases) {
|
|
|
8881
11609
|
return file;
|
|
8882
11610
|
}
|
|
8883
11611
|
function relativeToRepo(file, repoRoot) {
|
|
8884
|
-
return
|
|
11612
|
+
return path26.relative(repoRoot, file).split(path26.sep).join("/");
|
|
8885
11613
|
}
|
|
8886
11614
|
function scriptHash(entry) {
|
|
8887
11615
|
return crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
@@ -9071,7 +11799,7 @@ function configuredCodeReachRoots() {
|
|
|
9071
11799
|
if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
|
|
9072
11800
|
} catch {
|
|
9073
11801
|
}
|
|
9074
|
-
return raw.split(
|
|
11802
|
+
return raw.split(path27.delimiter).filter(Boolean);
|
|
9075
11803
|
}
|
|
9076
11804
|
async function stopSinkCodeReach(sink, closing) {
|
|
9077
11805
|
const session = sink.codeReach;
|
|
@@ -9086,7 +11814,7 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
9086
11814
|
if (!entries) return;
|
|
9087
11815
|
const info = sink.testInfo;
|
|
9088
11816
|
const configFile = info?.config.configFile;
|
|
9089
|
-
const configDir = configFile ?
|
|
11817
|
+
const configDir = configFile ? path27.dirname(configFile) : info?.config.rootDir ?? process.cwd();
|
|
9090
11818
|
session.files = await resolveCodeReach(
|
|
9091
11819
|
entries,
|
|
9092
11820
|
codeReachRoots(configDir, configuredCodeReachRoots()),
|
|
@@ -9102,8 +11830,8 @@ async function drainPendingProbes(capMs) {
|
|
|
9102
11830
|
let cap;
|
|
9103
11831
|
await Promise.race([
|
|
9104
11832
|
Promise.allSettled(PENDING_PROBES),
|
|
9105
|
-
new Promise((
|
|
9106
|
-
cap = setTimeout(
|
|
11833
|
+
new Promise((resolve7) => {
|
|
11834
|
+
cap = setTimeout(resolve7, capMs);
|
|
9107
11835
|
})
|
|
9108
11836
|
]);
|
|
9109
11837
|
clearTimeout(cap);
|
|
@@ -9143,10 +11871,10 @@ async function readWebVitals(page) {
|
|
|
9143
11871
|
const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
|
|
9144
11872
|
paint[key] = Math.round(entry.startTime);
|
|
9145
11873
|
}
|
|
9146
|
-
const readBuffered = (type, extra) => new Promise((
|
|
11874
|
+
const readBuffered = (type, extra) => new Promise((resolve7) => {
|
|
9147
11875
|
try {
|
|
9148
11876
|
const PO = globalThis.PerformanceObserver;
|
|
9149
|
-
if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return
|
|
11877
|
+
if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve7(null);
|
|
9150
11878
|
const out = [];
|
|
9151
11879
|
const po = new PO((list) => out.push(...list.getEntries()));
|
|
9152
11880
|
po.observe({ type, buffered: true, ...extra });
|
|
@@ -9156,10 +11884,10 @@ async function readWebVitals(page) {
|
|
|
9156
11884
|
po.disconnect();
|
|
9157
11885
|
} catch {
|
|
9158
11886
|
}
|
|
9159
|
-
|
|
11887
|
+
resolve7(out);
|
|
9160
11888
|
}, 0);
|
|
9161
11889
|
} catch {
|
|
9162
|
-
|
|
11890
|
+
resolve7(null);
|
|
9163
11891
|
}
|
|
9164
11892
|
});
|
|
9165
11893
|
const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
|
|
@@ -9272,12 +12000,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
|
|
|
9272
12000
|
var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
|
|
9273
12001
|
var MAX_PAGE_INVENTORY_WINDOWS = 64;
|
|
9274
12002
|
function withTimeout(promise, ms) {
|
|
9275
|
-
return new Promise((
|
|
12003
|
+
return new Promise((resolve7, reject) => {
|
|
9276
12004
|
const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
|
|
9277
12005
|
promise.then(
|
|
9278
12006
|
(value) => {
|
|
9279
12007
|
clearTimeout(timer);
|
|
9280
|
-
|
|
12008
|
+
resolve7(value);
|
|
9281
12009
|
},
|
|
9282
12010
|
(error) => {
|
|
9283
12011
|
clearTimeout(timer);
|
|
@@ -9650,6 +12378,7 @@ function instrumentPage(page) {
|
|
|
9650
12378
|
const original = factories[method].bind(page);
|
|
9651
12379
|
factories[method] = (...args) => {
|
|
9652
12380
|
if (currentSink) currentSink.lastActivePage = page;
|
|
12381
|
+
noteResourceUse(page);
|
|
9653
12382
|
return wrapLocator(page, original(...args), method, args);
|
|
9654
12383
|
};
|
|
9655
12384
|
}
|
|
@@ -9836,8 +12565,8 @@ async function flushSink(sink, testInfo) {
|
|
|
9836
12565
|
let drainDeadline;
|
|
9837
12566
|
await Promise.race([
|
|
9838
12567
|
Promise.allSettled(sink.capturePromises),
|
|
9839
|
-
new Promise((
|
|
9840
|
-
drainDeadline = setTimeout(
|
|
12568
|
+
new Promise((resolve7) => {
|
|
12569
|
+
drainDeadline = setTimeout(resolve7, 2e3);
|
|
9841
12570
|
})
|
|
9842
12571
|
]);
|
|
9843
12572
|
clearTimeout(drainDeadline);
|
|
@@ -9979,6 +12708,22 @@ async function flushSink(sink, testInfo) {
|
|
|
9979
12708
|
}
|
|
9980
12709
|
}
|
|
9981
12710
|
var piwiFixtures = {
|
|
12711
|
+
// Auto, worker-scoped: the worker's resource ledger. Set up before any hook
|
|
12712
|
+
// (worker fixtures come first), so a `beforeAll` context is in it too, and
|
|
12713
|
+
// torn down after the browser closed, so its census sees what only the
|
|
12714
|
+
// worker's shutdown closed. Depends on `playwright` alone, which launches nothing.
|
|
12715
|
+
piwiResources: [
|
|
12716
|
+
async ({ playwright }, use, workerInfo) => {
|
|
12717
|
+
boxCaptureFrames();
|
|
12718
|
+
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
12719
|
+
try {
|
|
12720
|
+
await use();
|
|
12721
|
+
} finally {
|
|
12722
|
+
stopResourceLedger();
|
|
12723
|
+
}
|
|
12724
|
+
},
|
|
12725
|
+
{ scope: "worker", auto: true }
|
|
12726
|
+
],
|
|
9982
12727
|
// Worker-scoped: patch the shared browser so every page/context created from
|
|
9983
12728
|
// it — including by user fixtures that take `browser` directly — is captured.
|
|
9984
12729
|
browser: [
|
|
@@ -10026,6 +12771,7 @@ var piwiFixtures = {
|
|
|
10026
12771
|
file: testInfo.file,
|
|
10027
12772
|
titlePath: testInfo.titlePath
|
|
10028
12773
|
});
|
|
12774
|
+
activeResourceLedger()?.testStarted(testInfo);
|
|
10029
12775
|
currentSink = sink;
|
|
10030
12776
|
try {
|
|
10031
12777
|
await use();
|
|
@@ -10052,6 +12798,7 @@ var piwiFixtures = {
|
|
|
10052
12798
|
}
|
|
10053
12799
|
if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
|
|
10054
12800
|
await flushSink(sink, testInfo);
|
|
12801
|
+
await recordResourceCensus(testInfo);
|
|
10055
12802
|
}
|
|
10056
12803
|
},
|
|
10057
12804
|
{ auto: true }
|
|
@@ -10083,13 +12830,13 @@ function extendPiwiFixtures(test) {
|
|
|
10083
12830
|
}
|
|
10084
12831
|
|
|
10085
12832
|
// src/internal/ai/ai-fixtures.ts
|
|
10086
|
-
var
|
|
10087
|
-
var
|
|
12833
|
+
var fs24 = __toESM(require("fs"));
|
|
12834
|
+
var path30 = __toESM(require("path"));
|
|
10088
12835
|
var import_test = require("@playwright/test");
|
|
10089
12836
|
|
|
10090
12837
|
// src/internal/ai/artifact.ts
|
|
10091
|
-
var
|
|
10092
|
-
var
|
|
12838
|
+
var fs23 = __toESM(require("fs"));
|
|
12839
|
+
var path28 = __toESM(require("path"));
|
|
10093
12840
|
var ARTIFACT_VERSION = 1;
|
|
10094
12841
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
10095
12842
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -10182,7 +12929,7 @@ function parseEntry(text) {
|
|
|
10182
12929
|
function readEntry(file) {
|
|
10183
12930
|
let text;
|
|
10184
12931
|
try {
|
|
10185
|
-
text =
|
|
12932
|
+
text = fs23.readFileSync(file, "utf8");
|
|
10186
12933
|
} catch (error) {
|
|
10187
12934
|
if (error.code === "ENOENT") return null;
|
|
10188
12935
|
throw error;
|
|
@@ -10192,28 +12939,28 @@ function readEntry(file) {
|
|
|
10192
12939
|
function writeEntry(file, entry) {
|
|
10193
12940
|
const canonical = serializeEntry(entry);
|
|
10194
12941
|
return withEntryLock(file, () => {
|
|
10195
|
-
if (
|
|
10196
|
-
|
|
12942
|
+
if (fs23.existsSync(file) && fs23.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
12943
|
+
fs23.mkdirSync(path28.dirname(file), { recursive: true });
|
|
10197
12944
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
10198
|
-
|
|
10199
|
-
|
|
12945
|
+
fs23.writeFileSync(tmp, canonical);
|
|
12946
|
+
fs23.renameSync(tmp, file);
|
|
10200
12947
|
return { written: true };
|
|
10201
12948
|
});
|
|
10202
12949
|
}
|
|
10203
12950
|
function withEntryLock(file, fn) {
|
|
10204
12951
|
const lock = `${file}.lock`;
|
|
10205
|
-
|
|
12952
|
+
fs23.mkdirSync(path28.dirname(file), { recursive: true });
|
|
10206
12953
|
const deadline = Date.now() + 5e3;
|
|
10207
12954
|
for (; ; ) {
|
|
10208
12955
|
try {
|
|
10209
|
-
const fd =
|
|
10210
|
-
|
|
12956
|
+
const fd = fs23.openSync(lock, "wx");
|
|
12957
|
+
fs23.closeSync(fd);
|
|
10211
12958
|
break;
|
|
10212
12959
|
} catch (error) {
|
|
10213
12960
|
if (error.code !== "EEXIST") throw error;
|
|
10214
12961
|
if (Date.now() > deadline) {
|
|
10215
12962
|
try {
|
|
10216
|
-
|
|
12963
|
+
fs23.unlinkSync(lock);
|
|
10217
12964
|
} catch {
|
|
10218
12965
|
}
|
|
10219
12966
|
}
|
|
@@ -10223,7 +12970,7 @@ function withEntryLock(file, fn) {
|
|
|
10223
12970
|
return fn();
|
|
10224
12971
|
} finally {
|
|
10225
12972
|
try {
|
|
10226
|
-
|
|
12973
|
+
fs23.unlinkSync(lock);
|
|
10227
12974
|
} catch {
|
|
10228
12975
|
}
|
|
10229
12976
|
}
|
|
@@ -10231,7 +12978,7 @@ function withEntryLock(file, fn) {
|
|
|
10231
12978
|
|
|
10232
12979
|
// src/internal/ai/keys.ts
|
|
10233
12980
|
var crypto4 = __toESM(require("crypto"));
|
|
10234
|
-
var
|
|
12981
|
+
var path29 = __toESM(require("path"));
|
|
10235
12982
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
10236
12983
|
function normalizeTemplate(template) {
|
|
10237
12984
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -10245,13 +12992,13 @@ function slug(text, maxLength = 40) {
|
|
|
10245
12992
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
10246
12993
|
}
|
|
10247
12994
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
10248
|
-
return
|
|
12995
|
+
return path29.join(path29.dirname(specFile), dir, path29.basename(specFile));
|
|
10249
12996
|
}
|
|
10250
12997
|
function entryPath(params) {
|
|
10251
12998
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
10252
12999
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
10253
13000
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
10254
|
-
return
|
|
13001
|
+
return path29.join(entryDir(params.specFile, params.dir), name);
|
|
10255
13002
|
}
|
|
10256
13003
|
function parseLocation2(location) {
|
|
10257
13004
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -10765,7 +13512,7 @@ function testIdentity(testInfo) {
|
|
|
10765
13512
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
10766
13513
|
}
|
|
10767
13514
|
function relativeToCwd(file) {
|
|
10768
|
-
return
|
|
13515
|
+
return path30.relative(process.cwd(), file).split(path30.sep).join("/");
|
|
10769
13516
|
}
|
|
10770
13517
|
function recordIntents(intents, entry) {
|
|
10771
13518
|
const add = (locator, kind) => {
|
|
@@ -10792,7 +13539,7 @@ function createAiApi(page, testInfo, config, used, intents) {
|
|
|
10792
13539
|
const readSource = () => {
|
|
10793
13540
|
if (source === null) {
|
|
10794
13541
|
try {
|
|
10795
|
-
source =
|
|
13542
|
+
source = fs24.readFileSync(testInfo.file, "utf8");
|
|
10796
13543
|
} catch {
|
|
10797
13544
|
source = "";
|
|
10798
13545
|
}
|