@piwitests/reporter 0.52.0 → 0.54.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 +3 -1
- package/dist/global-setup-module.js +10 -1
- package/dist/index.d.ts +41 -3
- package/dist/index.js +4859 -3514
- package/dist/internal/capture/attachments.d.ts +4 -0
- package/dist/internal/capture/attachments.js +8 -0
- package/dist/internal/capture/capture-fixtures.d.ts +56 -22
- package/dist/internal/capture/capture-fixtures.js +601 -85
- package/package.json +1 -1
- package/templates/skills/investigate-failure/SKILL.md +1 -1
- package/templates/skills/stabilize-flaky-tests/SKILL.md +1 -1
- package/templates/skills/write-the-missing-test/SKILL.md +32 -6
|
@@ -49,6 +49,7 @@ __export(capture_fixtures_exports, {
|
|
|
49
49
|
pageMapFetcher: () => pageMapFetcher,
|
|
50
50
|
piwiFixtures: () => piwiFixtures,
|
|
51
51
|
probeElementAttrs: () => probeElementAttrs,
|
|
52
|
+
readPageInteraction: () => readPageInteraction,
|
|
52
53
|
recordResourceCensus: () => recordResourceCensus,
|
|
53
54
|
resolveCodeReach: () => resolveCodeReach,
|
|
54
55
|
startCodeReach: () => startCodeReach,
|
|
@@ -1634,6 +1635,57 @@ function mergeCandidates(base, extra) {
|
|
|
1634
1635
|
return merged.sort((a, b) => b.score - a.score);
|
|
1635
1636
|
}
|
|
1636
1637
|
|
|
1638
|
+
// ../core/src/page-interaction.ts
|
|
1639
|
+
var ELEMENT_PATH_MAX_CHARS = 2e3;
|
|
1640
|
+
var ELEMENT_LABEL_MAX_CHARS = 120;
|
|
1641
|
+
var MAX_INDETERMINATE = 20;
|
|
1642
|
+
var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
|
|
1643
|
+
var LEVEL = `${STEP}(?:/${STEP})*`;
|
|
1644
|
+
var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
|
|
1645
|
+
function isElementPath(path14) {
|
|
1646
|
+
return typeof path14 === "string" && path14.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path14);
|
|
1647
|
+
}
|
|
1648
|
+
var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
|
|
1649
|
+
function elementRef(v) {
|
|
1650
|
+
if (!v || typeof v !== "object") return null;
|
|
1651
|
+
const { path: path14, label } = v;
|
|
1652
|
+
if (!isElementPath(path14)) return null;
|
|
1653
|
+
return { path: path14, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
|
|
1654
|
+
}
|
|
1655
|
+
function textPoint(v) {
|
|
1656
|
+
if (!v || typeof v !== "object") return null;
|
|
1657
|
+
const { path: path14, offset } = v;
|
|
1658
|
+
const at = offsetOf(offset);
|
|
1659
|
+
return isElementPath(path14) && at !== null ? { path: path14, offset: at } : null;
|
|
1660
|
+
}
|
|
1661
|
+
function sanitizePageInteraction(raw) {
|
|
1662
|
+
if (!raw || typeof raw !== "object") return {};
|
|
1663
|
+
const r = raw;
|
|
1664
|
+
const out = {};
|
|
1665
|
+
const focus = r.focus;
|
|
1666
|
+
const focused = focus && typeof focus === "object" ? elementRef(focus.element) : null;
|
|
1667
|
+
if (focus && focused) {
|
|
1668
|
+
out.focus = { element: focused, visible: focus.visible === true };
|
|
1669
|
+
const range = Array.isArray(focus.selection) && focus.selection.length === 2 ? focus.selection : [];
|
|
1670
|
+
const [start, end] = range.map(offsetOf);
|
|
1671
|
+
if (start != null && end != null && start <= end) out.focus.selection = [start, end];
|
|
1672
|
+
}
|
|
1673
|
+
const hover = elementRef(r.hover);
|
|
1674
|
+
if (hover) out.hover = hover;
|
|
1675
|
+
const selection = r.selection;
|
|
1676
|
+
if (selection && typeof selection === "object") {
|
|
1677
|
+
const start = textPoint(selection.start);
|
|
1678
|
+
const end = textPoint(selection.end);
|
|
1679
|
+
const length = offsetOf(selection.length);
|
|
1680
|
+
if (start && end && length !== null && length > 0) out.selection = { start, end, length };
|
|
1681
|
+
}
|
|
1682
|
+
if (Array.isArray(r.indeterminate)) {
|
|
1683
|
+
const boxes = r.indeterminate.map(elementRef).filter((ref) => ref !== null).slice(0, MAX_INDETERMINATE);
|
|
1684
|
+
if (boxes.length) out.indeterminate = boxes;
|
|
1685
|
+
}
|
|
1686
|
+
return out;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1637
1689
|
// src/internal/capture/locator-healing.ts
|
|
1638
1690
|
var path = __toESM(require("path"));
|
|
1639
1691
|
|
|
@@ -1971,6 +2023,9 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
1971
2023
|
return null;
|
|
1972
2024
|
}
|
|
1973
2025
|
|
|
2026
|
+
// ../core/src/wire.ts
|
|
2027
|
+
var FAILURE_SCREENSHOT_ATTACHMENT = "piwi-failure-screenshot";
|
|
2028
|
+
|
|
1974
2029
|
// src/internal/capture/attachments.ts
|
|
1975
2030
|
var ATTACHMENT_NAMES = {
|
|
1976
2031
|
locators: "piwi-locators",
|
|
@@ -1982,6 +2037,7 @@ var ATTACHMENT_NAMES = {
|
|
|
1982
2037
|
pageInventory: "piwi-page-inventory",
|
|
1983
2038
|
locatorPages: "piwi-locator-pages",
|
|
1984
2039
|
codeReach: "piwi-code-reach",
|
|
2040
|
+
serverCoverageRequests: "piwi-server-coverage-requests",
|
|
1985
2041
|
webVitals: "piwi-web-vitals",
|
|
1986
2042
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
1987
2043
|
pageState: "piwi-page-state",
|
|
@@ -1994,6 +2050,25 @@ var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
|
1994
2050
|
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
1995
2051
|
var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
|
|
1996
2052
|
|
|
2053
|
+
// src/internal/capture/failure-screenshot.ts
|
|
2054
|
+
function keepsFailureScreenshots(option, retry) {
|
|
2055
|
+
const mode = typeof option === "string" ? option : option?.mode;
|
|
2056
|
+
return mode === "on" || mode === "only-on-failure" || mode === "on-first-failure" && retry === 0;
|
|
2057
|
+
}
|
|
2058
|
+
async function readFailureScreenshot(page, option) {
|
|
2059
|
+
const options = typeof option === "object" && option !== null ? option : null;
|
|
2060
|
+
try {
|
|
2061
|
+
return await page.screenshot({
|
|
2062
|
+
fullPage: options?.fullPage,
|
|
2063
|
+
omitBackground: options?.omitBackground,
|
|
2064
|
+
timeout: 5e3,
|
|
2065
|
+
caret: "initial"
|
|
2066
|
+
});
|
|
2067
|
+
} catch {
|
|
2068
|
+
return null;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1997
2072
|
// src/internal/capture/page-inventory.ts
|
|
1998
2073
|
var PAGE_INVENTORY_MAX_ENTRIES = 500;
|
|
1999
2074
|
function collectPageInventoryInPage(maxEntries) {
|
|
@@ -2225,7 +2300,7 @@ var DEFAULTS = {
|
|
|
2225
2300
|
// Off by default: the page inventory reads control and link names from every
|
|
2226
2301
|
// visited page, so it stays opt-in until a project turns it on.
|
|
2227
2302
|
capturePageInventory: false,
|
|
2228
|
-
// Off by default: JavaScript coverage
|
|
2303
|
+
// Off by default: JavaScript coverage adds time to every test, so a scheduled job opts in.
|
|
2229
2304
|
captureCodeReach: false,
|
|
2230
2305
|
captureServerTraces: true,
|
|
2231
2306
|
captureResources: true,
|
|
@@ -2270,6 +2345,8 @@ var PIWI_ENV_KEYS = {
|
|
|
2270
2345
|
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
2271
2346
|
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
2272
2347
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
2348
|
+
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
2349
|
+
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
2273
2350
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
2274
2351
|
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
2275
2352
|
leakCheck: "PIWI_LEAK_CHECK",
|
|
@@ -3826,6 +3903,18 @@ async function pauseHere(page, testInfo, point, probe, onPick) {
|
|
|
3826
3903
|
}
|
|
3827
3904
|
}
|
|
3828
3905
|
|
|
3906
|
+
// src/internal/collect/coverage-settings.ts
|
|
3907
|
+
var SERVER_VALUES = /* @__PURE__ */ new Set(["files", "functions", "lines"]);
|
|
3908
|
+
function isServerGranularity(value) {
|
|
3909
|
+
return typeof value === "string" && SERVER_VALUES.has(value);
|
|
3910
|
+
}
|
|
3911
|
+
function browserCoverageInWorker() {
|
|
3912
|
+
return process.env[PIWI_ENV_KEYS.captureCodeReach] === "true" || process.env[PIWI_ENV_KEYS.coverageBrowser] === "files";
|
|
3913
|
+
}
|
|
3914
|
+
function serverCoverageInWorker() {
|
|
3915
|
+
return isServerGranularity(process.env[PIWI_ENV_KEYS.coverageServer]);
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3829
3918
|
// src/internal/support/aria-sampling.ts
|
|
3830
3919
|
var path7 = __toESM(require("path"));
|
|
3831
3920
|
var os3 = __toESM(require("os"));
|
|
@@ -3950,6 +4039,9 @@ function boxCaptureFrames(extraPrefixes = []) {
|
|
|
3950
4039
|
} catch {
|
|
3951
4040
|
}
|
|
3952
4041
|
}
|
|
4042
|
+
function callMethod(target, prop, args) {
|
|
4043
|
+
return target[prop](...args);
|
|
4044
|
+
}
|
|
3953
4045
|
function internalCall(owner, run) {
|
|
3954
4046
|
const wrap = owner?._wrapApiCall;
|
|
3955
4047
|
if (typeof wrap !== "function") return run();
|
|
@@ -3962,6 +4054,16 @@ var fs6 = __toESM(require("fs"));
|
|
|
3962
4054
|
var path10 = __toESM(require("path"));
|
|
3963
4055
|
var import_node_child_process = require("child_process");
|
|
3964
4056
|
|
|
4057
|
+
// ../core/src/coverage.ts
|
|
4058
|
+
var MAX_PATH_LENGTH = 500;
|
|
4059
|
+
function normalizeCoveragePath(raw) {
|
|
4060
|
+
if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
|
|
4061
|
+
const path14 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4062
|
+
if (!path14 || path14.startsWith("../") || path14.startsWith("/") || /^[A-Za-z]:\//.test(path14)) return null;
|
|
4063
|
+
if (/(^|\/)node_modules\//.test(path14)) return null;
|
|
4064
|
+
return path14;
|
|
4065
|
+
}
|
|
4066
|
+
|
|
3965
4067
|
// ../core/src/source-map.ts
|
|
3966
4068
|
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
3967
4069
|
var BASE64_VALUE = new Int8Array(128).fill(-1);
|
|
@@ -4173,10 +4275,8 @@ function reachedSources(entry, map) {
|
|
|
4173
4275
|
function finalizeCodeReach(files) {
|
|
4174
4276
|
const out = /* @__PURE__ */ new Set();
|
|
4175
4277
|
for (const raw of files) {
|
|
4176
|
-
const file = raw
|
|
4177
|
-
if (
|
|
4178
|
-
if (/(^|\/)node_modules\//.test(file)) continue;
|
|
4179
|
-
out.add(file);
|
|
4278
|
+
const file = normalizeCoveragePath(raw);
|
|
4279
|
+
if (file !== null) out.add(file);
|
|
4180
4280
|
}
|
|
4181
4281
|
return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
|
|
4182
4282
|
}
|
|
@@ -4277,8 +4377,8 @@ function fileForUrl(url, roots, bases) {
|
|
|
4277
4377
|
function relativeToRepo(file, repoRoot) {
|
|
4278
4378
|
return path10.relative(repoRoot, file).split(path10.sep).join("/");
|
|
4279
4379
|
}
|
|
4280
|
-
function
|
|
4281
|
-
return crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
4380
|
+
function scriptVersion(entry) {
|
|
4381
|
+
return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
4282
4382
|
}
|
|
4283
4383
|
async function loadMap(entry, fetchMap) {
|
|
4284
4384
|
const ref = entry.source ? sourceMappingUrl(entry.source) : null;
|
|
@@ -4293,7 +4393,7 @@ async function loadMap(entry, fetchMap) {
|
|
|
4293
4393
|
} catch {
|
|
4294
4394
|
return null;
|
|
4295
4395
|
}
|
|
4296
|
-
const version =
|
|
4396
|
+
const version = scriptVersion(entry);
|
|
4297
4397
|
let pending = mapCache.get(absolute, version);
|
|
4298
4398
|
if (!pending) {
|
|
4299
4399
|
pending = fetchMap(absolute).then((text) => text ? parseMap(text) : null);
|
|
@@ -4309,29 +4409,34 @@ function parseMap(text) {
|
|
|
4309
4409
|
}
|
|
4310
4410
|
}
|
|
4311
4411
|
async function resolveScript(entry, roots, bases, fetchMap) {
|
|
4312
|
-
const version =
|
|
4412
|
+
const version = scriptVersion(entry);
|
|
4313
4413
|
const cached = scriptCache.get(entry.url, version);
|
|
4314
4414
|
if (cached) return cached;
|
|
4315
|
-
let resolution;
|
|
4316
4415
|
const file = fileForUrl(entry.url, roots.roots, bases);
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
scriptCache.set(entry.url, version, resolution);
|
|
4416
|
+
const lookup = await loadMap(entry, fetchMap);
|
|
4417
|
+
let resolution;
|
|
4418
|
+
if (file) resolution = { kind: "file", file, lookup, source: entry.source };
|
|
4419
|
+
else resolution = lookup ? { kind: "map", lookup, files: [], source: entry.source } : { kind: "none" };
|
|
4420
|
+
if (entry.source !== void 0) scriptCache.set(entry.url, version, resolution);
|
|
4323
4421
|
return resolution;
|
|
4324
4422
|
}
|
|
4423
|
+
function withSource(entry, resolution) {
|
|
4424
|
+
if (entry.source !== void 0 || resolution.source === void 0) return entry;
|
|
4425
|
+
return { ...entry, source: resolution.source };
|
|
4426
|
+
}
|
|
4325
4427
|
async function resolveCodeReach(entries, roots, fetchMap) {
|
|
4326
4428
|
const files = /* @__PURE__ */ new Set();
|
|
4327
4429
|
const bases = viteBases(entries);
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4430
|
+
const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
|
|
4431
|
+
const known = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
|
|
4432
|
+
for (const [i, covered] of scripts.entries()) {
|
|
4433
|
+
const resolution = known[i] ?? await resolveScript(covered, roots, bases, fetchMap);
|
|
4331
4434
|
if (resolution.kind === "file") {
|
|
4435
|
+
const entry = withSource(covered, resolution);
|
|
4332
4436
|
const ran = resolution.lookup ? reachedSources(entry, resolution.lookup).size > 0 : scriptRan(entry);
|
|
4333
4437
|
if (resolution.file && ran) files.add(relativeToRepo(resolution.file, roots.repoRoot));
|
|
4334
4438
|
} else if (resolution.kind === "map") {
|
|
4439
|
+
const entry = withSource(covered, resolution);
|
|
4335
4440
|
for (const index of reachedSources(entry, resolution.lookup)) {
|
|
4336
4441
|
if (resolution.files[index] === void 0) {
|
|
4337
4442
|
const source = resolution.lookup.sources[index] ?? "";
|
|
@@ -4373,23 +4478,85 @@ function isChromiumPage(page) {
|
|
|
4373
4478
|
return false;
|
|
4374
4479
|
}
|
|
4375
4480
|
}
|
|
4481
|
+
var captures = /* @__PURE__ */ new WeakMap();
|
|
4482
|
+
function versionKey(url, hash) {
|
|
4483
|
+
return `${hash} ${url}`;
|
|
4484
|
+
}
|
|
4485
|
+
function onScriptParsed(page, capture, event) {
|
|
4486
|
+
if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
|
|
4487
|
+
capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash });
|
|
4488
|
+
const key = versionKey(event.url, event.hash);
|
|
4489
|
+
if (capture.sources.has(key) || scriptCache.get(event.url, event.hash)) return;
|
|
4490
|
+
const source = internalCall(
|
|
4491
|
+
page,
|
|
4492
|
+
() => capture.cdp.send("Debugger.getScriptSource", { scriptId: event.scriptId })
|
|
4493
|
+
).then(
|
|
4494
|
+
(response) => response.scriptSource,
|
|
4495
|
+
() => {
|
|
4496
|
+
if (capture.sources.get(key) === source) capture.sources.delete(key);
|
|
4497
|
+
return null;
|
|
4498
|
+
}
|
|
4499
|
+
);
|
|
4500
|
+
capture.sources.set(key, source);
|
|
4501
|
+
}
|
|
4502
|
+
async function detach(page, cdp) {
|
|
4503
|
+
try {
|
|
4504
|
+
await internalCall(page, () => cdp.detach());
|
|
4505
|
+
} catch {
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4376
4508
|
async function startCodeReach(page) {
|
|
4377
|
-
if (!isChromiumPage(page) ||
|
|
4509
|
+
if (!isChromiumPage(page) || captures.has(page)) return false;
|
|
4510
|
+
let cdp = null;
|
|
4378
4511
|
try {
|
|
4512
|
+
cdp = await internalCall(page, () => page.context().newCDPSession(page));
|
|
4513
|
+
const capture = { cdp, scripts: /* @__PURE__ */ new Map(), sources: /* @__PURE__ */ new Map() };
|
|
4514
|
+
cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
|
|
4515
|
+
cdp.on("Debugger.paused", () => {
|
|
4516
|
+
internalCall(page, () => capture.cdp.send("Debugger.resume")).catch(() => {
|
|
4517
|
+
});
|
|
4518
|
+
});
|
|
4379
4519
|
await internalCall(
|
|
4380
4520
|
page,
|
|
4381
|
-
() =>
|
|
4521
|
+
() => Promise.all([
|
|
4522
|
+
capture.cdp.send("Debugger.enable"),
|
|
4523
|
+
capture.cdp.send("Debugger.setSkipAllPauses", { skip: true }),
|
|
4524
|
+
capture.cdp.send("Profiler.enable"),
|
|
4525
|
+
capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
|
|
4526
|
+
])
|
|
4382
4527
|
);
|
|
4528
|
+
captures.set(page, capture);
|
|
4383
4529
|
return true;
|
|
4384
4530
|
} catch {
|
|
4531
|
+
if (cdp) await detach(page, cdp);
|
|
4385
4532
|
return false;
|
|
4386
4533
|
}
|
|
4387
4534
|
}
|
|
4388
4535
|
async function stopCodeReach(page) {
|
|
4536
|
+
const capture = captures.get(page);
|
|
4537
|
+
if (!capture) return null;
|
|
4538
|
+
captures.delete(page);
|
|
4539
|
+
const { cdp } = capture;
|
|
4389
4540
|
try {
|
|
4390
|
-
|
|
4541
|
+
const coverage = await internalCall(page, async () => {
|
|
4542
|
+
const taken = await cdp.send("Profiler.takePreciseCoverage");
|
|
4543
|
+
await cdp.send("Profiler.stopPreciseCoverage");
|
|
4544
|
+
return taken;
|
|
4545
|
+
});
|
|
4546
|
+
const entries = [];
|
|
4547
|
+
for (const script of coverage.result) {
|
|
4548
|
+
const parsed2 = capture.scripts.get(script.scriptId);
|
|
4549
|
+
if (!parsed2) continue;
|
|
4550
|
+
const entry = { url: parsed2.url, hash: parsed2.hash, functions: script.functions };
|
|
4551
|
+
const source = await capture.sources.get(versionKey(parsed2.url, parsed2.hash));
|
|
4552
|
+
if (source != null) entry.source = source;
|
|
4553
|
+
entries.push(entry);
|
|
4554
|
+
}
|
|
4555
|
+
return entries;
|
|
4391
4556
|
} catch {
|
|
4392
4557
|
return null;
|
|
4558
|
+
} finally {
|
|
4559
|
+
await detach(page, cdp);
|
|
4393
4560
|
}
|
|
4394
4561
|
}
|
|
4395
4562
|
|
|
@@ -5258,6 +5425,243 @@ async function recordResourceCensus(testInfo) {
|
|
|
5258
5425
|
if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
|
|
5259
5426
|
}
|
|
5260
5427
|
|
|
5428
|
+
// src/internal/capture/page-interaction.ts
|
|
5429
|
+
function readPageInteraction() {
|
|
5430
|
+
const doc = globalThis.document;
|
|
5431
|
+
const SKIPPED_TEXT = ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"];
|
|
5432
|
+
const pathOf = (el) => {
|
|
5433
|
+
const levels = [];
|
|
5434
|
+
for (let node = el; node; ) {
|
|
5435
|
+
const steps = [];
|
|
5436
|
+
let root = null;
|
|
5437
|
+
for (let cur = node; cur; cur = cur.parentElement) {
|
|
5438
|
+
let position = 1;
|
|
5439
|
+
for (let s = cur.previousElementSibling; s; s = s.previousElementSibling) {
|
|
5440
|
+
if (s.localName === cur.localName) position++;
|
|
5441
|
+
}
|
|
5442
|
+
steps.unshift(`${cur.localName}:${position}`);
|
|
5443
|
+
if (!cur.parentElement) root = cur.parentNode;
|
|
5444
|
+
}
|
|
5445
|
+
levels.unshift(steps.join("/"));
|
|
5446
|
+
node = root && root.host ? root.host : null;
|
|
5447
|
+
}
|
|
5448
|
+
return levels.join("|");
|
|
5449
|
+
};
|
|
5450
|
+
const isField = (el) => /^(input|textarea|select)$/.test(el.localName) || !!el.isContentEditable;
|
|
5451
|
+
const label = (el) => {
|
|
5452
|
+
const tag = el.localName;
|
|
5453
|
+
if (el.id) return `${tag}#${el.id}`;
|
|
5454
|
+
for (const attr2 of ["name", "aria-label"]) {
|
|
5455
|
+
const value = el.getAttribute(attr2);
|
|
5456
|
+
if (value) return `${tag}[${attr2}="${value}"]`;
|
|
5457
|
+
}
|
|
5458
|
+
const text = isField(el) ? "" : String(el.textContent || "").replace(/\s+/g, " ").trim();
|
|
5459
|
+
return text ? `${tag} "${text.length > 40 ? `${text.slice(0, 40)}\u2026` : text}"` : tag;
|
|
5460
|
+
};
|
|
5461
|
+
const ref = (el) => ({ path: pathOf(el), label: label(el) });
|
|
5462
|
+
const textOffset = (el, stop, extra) => {
|
|
5463
|
+
const walker = doc.createTreeWalker(el, 4);
|
|
5464
|
+
let offset = 0;
|
|
5465
|
+
for (let text = walker.nextNode(); text; text = walker.nextNode()) {
|
|
5466
|
+
if (stop && (text === stop || stop.contains(text) || stop.compareDocumentPosition(text) & 4)) break;
|
|
5467
|
+
if (!SKIPPED_TEXT.includes(text.parentElement?.tagName)) offset += text.data.length;
|
|
5468
|
+
}
|
|
5469
|
+
return offset + extra;
|
|
5470
|
+
};
|
|
5471
|
+
const textPoint2 = (node, offset) => {
|
|
5472
|
+
if (node.nodeType === 3)
|
|
5473
|
+
return { path: pathOf(node.parentElement), offset: textOffset(node.parentElement, node, offset) };
|
|
5474
|
+
return { path: pathOf(node), offset: textOffset(node, node.childNodes[offset] ?? null, 0) };
|
|
5475
|
+
};
|
|
5476
|
+
const out = {};
|
|
5477
|
+
let active = doc.activeElement;
|
|
5478
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
5479
|
+
if (active && active !== doc.body && active !== doc.documentElement) {
|
|
5480
|
+
let visible = false;
|
|
5481
|
+
try {
|
|
5482
|
+
visible = active.matches(":focus-visible");
|
|
5483
|
+
} catch {
|
|
5484
|
+
}
|
|
5485
|
+
out.focus = { element: ref(active), visible };
|
|
5486
|
+
try {
|
|
5487
|
+
if (typeof active.selectionStart === "number" && typeof active.selectionEnd === "number") {
|
|
5488
|
+
out.focus.selection = [active.selectionStart, active.selectionEnd];
|
|
5489
|
+
}
|
|
5490
|
+
} catch {
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
let scope = doc;
|
|
5494
|
+
for (; ; ) {
|
|
5495
|
+
const chain = scope.querySelectorAll(":hover");
|
|
5496
|
+
const innermost = chain[chain.length - 1];
|
|
5497
|
+
if (!innermost) break;
|
|
5498
|
+
out.hover = ref(innermost);
|
|
5499
|
+
if (!innermost.shadowRoot) break;
|
|
5500
|
+
scope = innermost.shadowRoot;
|
|
5501
|
+
}
|
|
5502
|
+
const selection = doc.getSelection?.();
|
|
5503
|
+
if (selection && selection.rangeCount > 0 && !selection.isCollapsed && !(active && isField(active))) {
|
|
5504
|
+
const range = selection.getRangeAt(0);
|
|
5505
|
+
const start = range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer;
|
|
5506
|
+
const end = range.endContainer.nodeType === 3 ? range.endContainer.parentElement : range.endContainer;
|
|
5507
|
+
if (start?.nodeType === 1 && end?.nodeType === 1) {
|
|
5508
|
+
out.selection = {
|
|
5509
|
+
start: textPoint2(range.startContainer, range.startOffset),
|
|
5510
|
+
end: textPoint2(range.endContainer, range.endOffset),
|
|
5511
|
+
length: String(selection).length
|
|
5512
|
+
};
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
const boxes = [];
|
|
5516
|
+
const roots = [doc];
|
|
5517
|
+
for (let i = 0; i < roots.length && boxes.length < 20; i++) {
|
|
5518
|
+
for (const el of roots[i].querySelectorAll("*")) {
|
|
5519
|
+
if (el.shadowRoot) roots.push(el.shadowRoot);
|
|
5520
|
+
if (el.localName === "input" && el.type === "checkbox" && el.indeterminate && boxes.length < 20)
|
|
5521
|
+
boxes.push(ref(el));
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
if (boxes.length) out.indeterminate = boxes;
|
|
5525
|
+
return out;
|
|
5526
|
+
}
|
|
5527
|
+
|
|
5528
|
+
// src/internal/capture/api-requests.ts
|
|
5529
|
+
var SENDING_METHODS = /* @__PURE__ */ new Set(["fetch", "get", "post", "put", "patch", "delete", "head"]);
|
|
5530
|
+
var PROXIES = /* @__PURE__ */ new WeakMap();
|
|
5531
|
+
var OBSERVED = /* @__PURE__ */ new WeakSet();
|
|
5532
|
+
var PATCHED_REQUESTS = /* @__PURE__ */ new WeakSet();
|
|
5533
|
+
function observeApiRequests(playwright, recorder) {
|
|
5534
|
+
const apiRequest = playwright?.request;
|
|
5535
|
+
if (!apiRequest || typeof apiRequest !== "object" || typeof apiRequest.newContext !== "function") return;
|
|
5536
|
+
if (PATCHED_REQUESTS.has(apiRequest)) return;
|
|
5537
|
+
PATCHED_REQUESTS.add(apiRequest);
|
|
5538
|
+
const create = apiRequest.newContext.bind(apiRequest);
|
|
5539
|
+
apiRequest.newContext = async function newContext(...args) {
|
|
5540
|
+
const context = await create(...args);
|
|
5541
|
+
return observeApiRequestContext(context, recorder, baseUrlOption(args[0]));
|
|
5542
|
+
};
|
|
5543
|
+
}
|
|
5544
|
+
function observeApiRequestContext(context, recorder, baseURL = null) {
|
|
5545
|
+
if (!context || typeof context !== "object" || OBSERVED.has(context)) return context;
|
|
5546
|
+
const known = PROXIES.get(context);
|
|
5547
|
+
if (known) return known;
|
|
5548
|
+
const forwarders = /* @__PURE__ */ new Map();
|
|
5549
|
+
const proxy = new Proxy(context, {
|
|
5550
|
+
get(target, prop) {
|
|
5551
|
+
const value = Reflect.get(target, prop);
|
|
5552
|
+
if (typeof value !== "function" || prop === "constructor") return value;
|
|
5553
|
+
const cached = forwarders.get(prop);
|
|
5554
|
+
if (cached?.method === value) return cached.forward;
|
|
5555
|
+
const forward = typeof prop === "string" && SENDING_METHODS.has(prop) ? sendingMethod(target, prop, recorder, baseURL) : value.bind(target);
|
|
5556
|
+
forwarders.set(prop, { method: value, forward });
|
|
5557
|
+
return forward;
|
|
5558
|
+
}
|
|
5559
|
+
});
|
|
5560
|
+
PROXIES.set(context, proxy);
|
|
5561
|
+
OBSERVED.add(proxy);
|
|
5562
|
+
return proxy;
|
|
5563
|
+
}
|
|
5564
|
+
function sendingMethod(target, prop, recorder, baseURL) {
|
|
5565
|
+
return function send(...args) {
|
|
5566
|
+
const record = recorder();
|
|
5567
|
+
if (!record) return callMethod(target, prop, args);
|
|
5568
|
+
const startedAt = Date.now();
|
|
5569
|
+
const result = callMethod(target, prop, args);
|
|
5570
|
+
if (typeof result?.then !== "function") return result;
|
|
5571
|
+
const report = (response, error) => {
|
|
5572
|
+
try {
|
|
5573
|
+
record({
|
|
5574
|
+
method: methodOf(prop, args),
|
|
5575
|
+
url: urlOf(args[0]),
|
|
5576
|
+
baseURL,
|
|
5577
|
+
startedAt,
|
|
5578
|
+
endedAt: Date.now(),
|
|
5579
|
+
response,
|
|
5580
|
+
error
|
|
5581
|
+
});
|
|
5582
|
+
} catch {
|
|
5583
|
+
}
|
|
5584
|
+
};
|
|
5585
|
+
return result.then(
|
|
5586
|
+
(response) => {
|
|
5587
|
+
report(response);
|
|
5588
|
+
return response;
|
|
5589
|
+
},
|
|
5590
|
+
(error) => {
|
|
5591
|
+
report(null, error);
|
|
5592
|
+
throw error;
|
|
5593
|
+
}
|
|
5594
|
+
);
|
|
5595
|
+
};
|
|
5596
|
+
}
|
|
5597
|
+
function methodOf(prop, args) {
|
|
5598
|
+
if (prop !== "fetch") return prop.toUpperCase();
|
|
5599
|
+
const option = args[1]?.method;
|
|
5600
|
+
if (typeof option === "string" && option) return option.toUpperCase();
|
|
5601
|
+
const request = args[0];
|
|
5602
|
+
const method = typeof request?.method === "function" ? request.method() : null;
|
|
5603
|
+
return typeof method === "string" && method ? method.toUpperCase() : "GET";
|
|
5604
|
+
}
|
|
5605
|
+
function urlOf(target) {
|
|
5606
|
+
if (typeof target === "string") return target;
|
|
5607
|
+
const url = typeof target?.url === "function" ? target.url() : null;
|
|
5608
|
+
return typeof url === "string" ? url : null;
|
|
5609
|
+
}
|
|
5610
|
+
function baseUrlOption(options) {
|
|
5611
|
+
const baseURL = options?.baseURL;
|
|
5612
|
+
return typeof baseURL === "string" && baseURL ? baseURL : null;
|
|
5613
|
+
}
|
|
5614
|
+
var MAX_FAILURE_LENGTH = 300;
|
|
5615
|
+
var API_ERROR_PREFIX = /^apiRequestContext\.\w+: /;
|
|
5616
|
+
var STATUS_FAILURE = /^(\d{3}) /;
|
|
5617
|
+
function apiCallEntry(call, baseURL) {
|
|
5618
|
+
const response = call.response;
|
|
5619
|
+
if (response) {
|
|
5620
|
+
const timing = responseTiming(response) ?? { startTime: call.startedAt, duration: call.endedAt - call.startedAt };
|
|
5621
|
+
return {
|
|
5622
|
+
method: call.method,
|
|
5623
|
+
url: response.url(),
|
|
5624
|
+
status: response.status(),
|
|
5625
|
+
duration: timing.duration,
|
|
5626
|
+
startTime: timing.startTime
|
|
5627
|
+
};
|
|
5628
|
+
}
|
|
5629
|
+
const url = resolveUrl(call.url, call.baseURL ?? baseURL);
|
|
5630
|
+
if (!url) return null;
|
|
5631
|
+
const failure = failureText(call.error);
|
|
5632
|
+
const status = STATUS_FAILURE.exec(failure);
|
|
5633
|
+
return {
|
|
5634
|
+
method: call.method,
|
|
5635
|
+
url,
|
|
5636
|
+
status: status ? Number(status[1]) : 0,
|
|
5637
|
+
duration: Math.max(0, call.endedAt - call.startedAt),
|
|
5638
|
+
startTime: call.startedAt,
|
|
5639
|
+
...status ? {} : { failure }
|
|
5640
|
+
};
|
|
5641
|
+
}
|
|
5642
|
+
function responseTiming(response) {
|
|
5643
|
+
if (typeof response.timing !== "function") return null;
|
|
5644
|
+
const timing = response.timing();
|
|
5645
|
+
if (!(timing.startTime > 0) || !(timing.responseEnd >= 0)) return null;
|
|
5646
|
+
return {
|
|
5647
|
+
startTime: timing.startTime,
|
|
5648
|
+
duration: Math.max(0, Math.round(timing.responseEnd - Math.max(0, timing.requestStart)))
|
|
5649
|
+
};
|
|
5650
|
+
}
|
|
5651
|
+
function resolveUrl(url, baseURL) {
|
|
5652
|
+
if (!url) return null;
|
|
5653
|
+
try {
|
|
5654
|
+
return new URL(url, baseURL ?? void 0).toString();
|
|
5655
|
+
} catch {
|
|
5656
|
+
return url;
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
function failureText(error) {
|
|
5660
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5661
|
+
const line = message.split("\n", 1)[0].replace(API_ERROR_PREFIX, "").trim();
|
|
5662
|
+
return (line || "failed").slice(0, MAX_FAILURE_LENGTH);
|
|
5663
|
+
}
|
|
5664
|
+
|
|
5261
5665
|
// src/internal/capture/capture-fixtures.ts
|
|
5262
5666
|
function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
5263
5667
|
const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
@@ -5291,6 +5695,7 @@ function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
|
5291
5695
|
function createSink() {
|
|
5292
5696
|
return {
|
|
5293
5697
|
networkRequests: [],
|
|
5698
|
+
openResponses: /* @__PURE__ */ new Map(),
|
|
5294
5699
|
consoleEntries: [],
|
|
5295
5700
|
dialogs: [],
|
|
5296
5701
|
pendingHandlers: [],
|
|
@@ -5304,6 +5709,8 @@ function createSink() {
|
|
|
5304
5709
|
stashedPageState: null,
|
|
5305
5710
|
stashedAria: null,
|
|
5306
5711
|
stashedAriaJson: null,
|
|
5712
|
+
failureScreenshot: null,
|
|
5713
|
+
stashedScreenshot: null,
|
|
5307
5714
|
pageInventories: [],
|
|
5308
5715
|
probeItem: null,
|
|
5309
5716
|
probePageErrors: 0,
|
|
@@ -5314,12 +5721,14 @@ function createSink() {
|
|
|
5314
5721
|
userPick: null,
|
|
5315
5722
|
pause: null,
|
|
5316
5723
|
locatorPages: new LocatorPageLog(),
|
|
5317
|
-
codeReach: null
|
|
5724
|
+
codeReach: null,
|
|
5725
|
+
serverCoverage: /* @__PURE__ */ new Map(),
|
|
5726
|
+
baseURL: null
|
|
5318
5727
|
};
|
|
5319
5728
|
}
|
|
5320
5729
|
function maybeStartCodeReach(page) {
|
|
5321
5730
|
const sink = currentSink;
|
|
5322
|
-
if (!sink ||
|
|
5731
|
+
if (!sink || !browserCoverageInWorker()) return Promise.resolve(false);
|
|
5323
5732
|
if (sink.codeReach) return sink.codeReach.page === page ? sink.codeReach.started : Promise.resolve(false);
|
|
5324
5733
|
const started = startCodeReach(page);
|
|
5325
5734
|
sink.codeReach = { page, started, stopped: false, files: null };
|
|
@@ -5460,7 +5869,7 @@ var PAGE_STATE_MAX_STORAGE_KEYS = 50;
|
|
|
5460
5869
|
var PAGE_STATE_MAX_COOKIES = 30;
|
|
5461
5870
|
var PAGE_STATE_HISTORY_CAP = 2048;
|
|
5462
5871
|
var TOKEN_MASK_RES = [/\beyJ[\w-]{10,}\.[\w-]{5,}\.[\w-]{5,}\b/g, /\b[0-9a-f]{32,}\b/gi];
|
|
5463
|
-
function buildPageState(raw, cookies) {
|
|
5872
|
+
function buildPageState(raw, cookies, interaction) {
|
|
5464
5873
|
let historyState = raw.historyState;
|
|
5465
5874
|
if (historyState) {
|
|
5466
5875
|
for (const re of TOKEN_MASK_RES) historyState = historyState.replace(re, "[masked]");
|
|
@@ -5486,7 +5895,8 @@ function buildPageState(raw, cookies) {
|
|
|
5486
5895
|
secure: Boolean(c.secure),
|
|
5487
5896
|
...c.sameSite !== void 0 ? { sameSite: String(c.sameSite) } : {},
|
|
5488
5897
|
...typeof c.expires === "number" ? { expires: c.expires } : {}
|
|
5489
|
-
}))
|
|
5898
|
+
})),
|
|
5899
|
+
...sanitizePageInteraction(interaction)
|
|
5490
5900
|
};
|
|
5491
5901
|
}
|
|
5492
5902
|
async function readPageState(page) {
|
|
@@ -5524,7 +5934,8 @@ async function readPageState(page) {
|
|
|
5524
5934
|
} catch {
|
|
5525
5935
|
cookies = null;
|
|
5526
5936
|
}
|
|
5527
|
-
|
|
5937
|
+
const interaction = await page.evaluate(readPageInteraction).catch(() => null);
|
|
5938
|
+
return buildPageState(raw, cookies, interaction);
|
|
5528
5939
|
} catch {
|
|
5529
5940
|
return null;
|
|
5530
5941
|
}
|
|
@@ -5616,6 +6027,8 @@ async function readPageBeforeClose(sink, page) {
|
|
|
5616
6027
|
};
|
|
5617
6028
|
if (status === "failed" || status === "timedOut" || status === "interrupted") {
|
|
5618
6029
|
await sampleAria();
|
|
6030
|
+
if (sink.failureScreenshot)
|
|
6031
|
+
sink.stashedScreenshot = await readFailureScreenshot(page, sink.failureScreenshot) ?? sink.stashedScreenshot;
|
|
5619
6032
|
} else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
|
|
5620
6033
|
await sampleAria();
|
|
5621
6034
|
}
|
|
@@ -5765,9 +6178,6 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
|
5765
6178
|
})();
|
|
5766
6179
|
sink.capturePromises.push(resolveAttrs);
|
|
5767
6180
|
}
|
|
5768
|
-
function callMethod(target, prop, args) {
|
|
5769
|
-
return target[prop](...args);
|
|
5770
|
-
}
|
|
5771
6181
|
function recordLocatorPage(sink, page, locator, location, arrival) {
|
|
5772
6182
|
try {
|
|
5773
6183
|
sink.locatorPages.record({ location, locator: String(locator), url: page.url(), arrival });
|
|
@@ -5934,6 +6344,7 @@ function instrumentPage(page) {
|
|
|
5934
6344
|
if (!page || INSTRUMENTED_PAGES.has(page)) return;
|
|
5935
6345
|
INSTRUMENTED_PAGES.add(page);
|
|
5936
6346
|
void maybeStartCodeReach(page);
|
|
6347
|
+
observeOwnApiRequests(page);
|
|
5937
6348
|
const probeSink = currentSink;
|
|
5938
6349
|
if (probeSink?.probeItem && !probeSink.probeInterception) {
|
|
5939
6350
|
void installProbeInterception(page, probeSink.probeItem).then((interception) => {
|
|
@@ -6040,59 +6451,115 @@ function instrumentPage(page) {
|
|
|
6040
6451
|
} catch {
|
|
6041
6452
|
}
|
|
6042
6453
|
}
|
|
6043
|
-
|
|
6454
|
+
page.on("response", (response) => {
|
|
6044
6455
|
const sink = currentSink;
|
|
6045
6456
|
if (!sink) return;
|
|
6457
|
+
try {
|
|
6458
|
+
const request = response.request();
|
|
6459
|
+
if (isTrackedRequest(request.url(), request.resourceType())) sink.openResponses.set(request, response);
|
|
6460
|
+
} catch {
|
|
6461
|
+
}
|
|
6462
|
+
});
|
|
6463
|
+
const onRequestEnd = (request, failed) => {
|
|
6464
|
+
const sink = currentSink;
|
|
6465
|
+
if (!sink || RECORDED_OPEN.has(request)) return;
|
|
6046
6466
|
sink.lastActivePage = page;
|
|
6047
|
-
const
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
}
|
|
6467
|
+
const response = sink.openResponses.get(request) ?? null;
|
|
6468
|
+
sink.openResponses.delete(request);
|
|
6469
|
+
recordRequest(sink, request, failed ? "failed" : "finished", response);
|
|
6470
|
+
};
|
|
6471
|
+
page.on("requestfinished", (request) => onRequestEnd(request, false));
|
|
6472
|
+
page.on("requestfailed", (request) => onRequestEnd(request, true));
|
|
6473
|
+
}
|
|
6474
|
+
var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
|
|
6475
|
+
function isTrackedRequest(url, resourceType) {
|
|
6476
|
+
if (url.startsWith("data:") || url.startsWith("blob:")) return false;
|
|
6477
|
+
return TRACKED_REQUEST_TYPES.includes(resourceType);
|
|
6478
|
+
}
|
|
6479
|
+
function recordRequest(sink, request, end, response) {
|
|
6480
|
+
const p = (async () => {
|
|
6481
|
+
try {
|
|
6482
|
+
const url = request.url();
|
|
6483
|
+
const resourceType = request.resourceType();
|
|
6484
|
+
if (!isTrackedRequest(url, resourceType)) return;
|
|
6485
|
+
let failure = end === "failed" ? request.failure()?.errorText ?? "failed" : null;
|
|
6486
|
+
if (failure && CANCELLED_REQUEST.test(failure)) {
|
|
6487
|
+
if (!response) return;
|
|
6488
|
+
failure = null;
|
|
6489
|
+
end = "unfinished";
|
|
6490
|
+
}
|
|
6491
|
+
const timing = request.timing();
|
|
6492
|
+
if (end === "failed") response = null;
|
|
6493
|
+
else if (end === "finished") response ??= await request.response();
|
|
6494
|
+
const entry = {
|
|
6495
|
+
method: request.method(),
|
|
6496
|
+
url,
|
|
6497
|
+
status: response ? response.status() : 0,
|
|
6498
|
+
duration: requestDuration(timing, end),
|
|
6499
|
+
startTime: timing.startTime,
|
|
6500
|
+
resourceType
|
|
6501
|
+
};
|
|
6502
|
+
if (failure) entry.failure = failure;
|
|
6503
|
+
if (response) {
|
|
6504
|
+
if (timing.requestStart < 0 && typeof response.serverAddr === "function") {
|
|
6505
|
+
if (await response.serverAddr().catch(() => void 0) === null) entry.fulfilled = true;
|
|
6087
6506
|
}
|
|
6088
|
-
sink.
|
|
6089
|
-
} catch {
|
|
6507
|
+
readResponseHeaders(sink, entry, url, response.headers());
|
|
6090
6508
|
}
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6509
|
+
sink.networkRequests.push(entry);
|
|
6510
|
+
} catch {
|
|
6511
|
+
}
|
|
6512
|
+
})();
|
|
6513
|
+
sink.pendingHandlers.push(p);
|
|
6514
|
+
}
|
|
6515
|
+
function readResponseHeaders(sink, entry, url, headers) {
|
|
6516
|
+
const contentType = headers["content-type"];
|
|
6517
|
+
if (contentType) entry.contentType = contentType.split(";")[0].trim();
|
|
6518
|
+
const logHeader = headers["x-piwi-logs"];
|
|
6519
|
+
if (logHeader) {
|
|
6520
|
+
try {
|
|
6521
|
+
entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
|
|
6522
|
+
} catch {
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
6525
|
+
const coverageHeader = headers["x-piwi-coverage"];
|
|
6526
|
+
if (coverageHeader && serverCoverageInWorker()) recordServerCoverageIds(sink, url, coverageHeader);
|
|
6527
|
+
const traceHeader = headers["x-piwi-trace"];
|
|
6528
|
+
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
6529
|
+
try {
|
|
6530
|
+
entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
6531
|
+
} catch {
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6534
|
+
}
|
|
6535
|
+
function recordApiCall(sink, call) {
|
|
6536
|
+
try {
|
|
6537
|
+
const entry = apiCallEntry(call, sink.baseURL);
|
|
6538
|
+
if (!entry) return;
|
|
6539
|
+
if (call.response) readResponseHeaders(sink, entry, String(entry.url), call.response.headers());
|
|
6540
|
+
sink.networkRequests.push(entry);
|
|
6541
|
+
} catch {
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
function apiCallRecorder() {
|
|
6545
|
+
const sink = currentSink;
|
|
6546
|
+
return sink ? (call) => recordApiCall(sink, call) : null;
|
|
6547
|
+
}
|
|
6548
|
+
function observeOwnApiRequests(owner) {
|
|
6549
|
+
try {
|
|
6550
|
+
const holder = owner;
|
|
6551
|
+
if (holder.request && typeof holder.request === "object") {
|
|
6552
|
+
holder.request = observeApiRequestContext(holder.request, apiCallRecorder);
|
|
6553
|
+
}
|
|
6554
|
+
} catch {
|
|
6555
|
+
}
|
|
6556
|
+
}
|
|
6557
|
+
function recordOpenRequests(sink) {
|
|
6558
|
+
for (const [request, response] of sink.openResponses) {
|
|
6559
|
+
RECORDED_OPEN.add(request);
|
|
6560
|
+
recordRequest(sink, request, "unfinished", response);
|
|
6561
|
+
}
|
|
6562
|
+
sink.openResponses.clear();
|
|
6096
6563
|
}
|
|
6097
6564
|
async function awaitFlakeConditions() {
|
|
6098
6565
|
await currentSink?.flake?.conditions;
|
|
@@ -6100,6 +6567,7 @@ async function awaitFlakeConditions() {
|
|
|
6100
6567
|
function instrumentContext(context) {
|
|
6101
6568
|
if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
|
|
6102
6569
|
INSTRUMENTED_CONTEXTS.add(context);
|
|
6570
|
+
observeOwnApiRequests(context);
|
|
6103
6571
|
if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
|
|
6104
6572
|
void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
|
|
6105
6573
|
});
|
|
@@ -6159,12 +6627,36 @@ function readRootAriaJson(page) {
|
|
|
6159
6627
|
return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
|
|
6160
6628
|
}
|
|
6161
6629
|
var TRACKED_REQUEST_TYPES = ["fetch", "xhr", "document", "other"];
|
|
6162
|
-
|
|
6630
|
+
var SERVER_COVERAGE_ID = /^[\x21-\x7e]{1,128}$/;
|
|
6631
|
+
function recordServerCoverageIds(sink, url, value) {
|
|
6632
|
+
let origin;
|
|
6633
|
+
try {
|
|
6634
|
+
const parsed2 = new URL(url);
|
|
6635
|
+
if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:") return;
|
|
6636
|
+
origin = parsed2.origin;
|
|
6637
|
+
} catch {
|
|
6638
|
+
return;
|
|
6639
|
+
}
|
|
6640
|
+
for (const id of value.split(/[\s,]+/)) {
|
|
6641
|
+
if (!SERVER_COVERAGE_ID.test(id)) continue;
|
|
6642
|
+
let ids = sink.serverCoverage.get(origin);
|
|
6643
|
+
if (!ids) {
|
|
6644
|
+
ids = /* @__PURE__ */ new Set();
|
|
6645
|
+
sink.serverCoverage.set(origin, ids);
|
|
6646
|
+
}
|
|
6647
|
+
ids.add(id);
|
|
6648
|
+
}
|
|
6649
|
+
}
|
|
6650
|
+
function requestDuration(timing, end) {
|
|
6651
|
+
if (end === "unfinished") {
|
|
6652
|
+
return timing.responseStart >= 0 ? Math.max(0, Math.round(timing.responseStart - timing.requestStart)) : 0;
|
|
6653
|
+
}
|
|
6163
6654
|
if (timing.responseEnd > 0) return Math.round(timing.responseEnd - timing.requestStart);
|
|
6164
|
-
if (failed && timing.startTime > 0) return Math.max(0, Math.round(Date.now() - timing.startTime));
|
|
6655
|
+
if (end === "failed" && timing.startTime > 0) return Math.max(0, Math.round(Date.now() - timing.startTime));
|
|
6165
6656
|
return 0;
|
|
6166
6657
|
}
|
|
6167
6658
|
async function flushSink(sink, testInfo) {
|
|
6659
|
+
recordOpenRequests(sink);
|
|
6168
6660
|
await Promise.allSettled(sink.pendingHandlers);
|
|
6169
6661
|
let drainDeadline;
|
|
6170
6662
|
await Promise.race([
|
|
@@ -6193,6 +6685,14 @@ async function flushSink(sink, testInfo) {
|
|
|
6193
6685
|
body: Buffer.from(JSON.stringify(sink.codeReach.files))
|
|
6194
6686
|
});
|
|
6195
6687
|
}
|
|
6688
|
+
if (sink.serverCoverage.size > 0) {
|
|
6689
|
+
const requests = {};
|
|
6690
|
+
for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
|
|
6691
|
+
await testInfo.attach(ATTACHMENT_NAMES.serverCoverageRequests, {
|
|
6692
|
+
contentType: "application/json",
|
|
6693
|
+
body: Buffer.from(JSON.stringify(requests))
|
|
6694
|
+
});
|
|
6695
|
+
}
|
|
6196
6696
|
if (sink.locatorPages.size > 0) {
|
|
6197
6697
|
await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
|
|
6198
6698
|
contentType: "application/json",
|
|
@@ -6230,6 +6730,16 @@ async function flushSink(sink, testInfo) {
|
|
|
6230
6730
|
} catch {
|
|
6231
6731
|
}
|
|
6232
6732
|
}
|
|
6733
|
+
if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
|
|
6734
|
+
const option = sink.failureScreenshot;
|
|
6735
|
+
const shot = (pageReadable ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
|
|
6736
|
+
if (shot) {
|
|
6737
|
+
try {
|
|
6738
|
+
await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
|
|
6739
|
+
} catch {
|
|
6740
|
+
}
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6233
6743
|
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
6234
6744
|
try {
|
|
6235
6745
|
const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
@@ -6312,14 +6822,17 @@ async function flushSink(sink, testInfo) {
|
|
|
6312
6822
|
}
|
|
6313
6823
|
}
|
|
6314
6824
|
var piwiFixtures = {
|
|
6315
|
-
// Auto, worker-scoped: the worker's resource ledger
|
|
6316
|
-
//
|
|
6317
|
-
//
|
|
6318
|
-
//
|
|
6825
|
+
// Auto, worker-scoped: the worker's resource ledger, and the API request
|
|
6826
|
+
// contexts its tests create, handed out behind the call proxy. Set up before
|
|
6827
|
+
// any hook (worker fixtures come first), so a `beforeAll` context is in the
|
|
6828
|
+
// ledger and records the calls the tests make through it, and torn down after
|
|
6829
|
+
// the browser closed, so its census sees what only the worker's shutdown
|
|
6830
|
+
// closed. Depends on `playwright` alone, which launches nothing.
|
|
6319
6831
|
piwiResources: [
|
|
6320
6832
|
async ({ playwright }, use, workerInfo) => {
|
|
6321
6833
|
boxCaptureFrames();
|
|
6322
6834
|
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
6835
|
+
observeApiRequests(playwright, apiCallRecorder);
|
|
6323
6836
|
try {
|
|
6324
6837
|
await use();
|
|
6325
6838
|
} finally {
|
|
@@ -6350,12 +6863,14 @@ var piwiFixtures = {
|
|
|
6350
6863
|
// (attach the collected data) at teardown. Runs for every test without being
|
|
6351
6864
|
// requested, so suites that never destructure `page` are still captured.
|
|
6352
6865
|
piwiCapture: [
|
|
6353
|
-
async ({}, use, testInfo) => {
|
|
6866
|
+
async ({ screenshot, baseURL }, use, testInfo) => {
|
|
6354
6867
|
boxCaptureFrames();
|
|
6355
6868
|
const conflict = labModeConflict();
|
|
6356
6869
|
if (conflict) throw new Error(conflict);
|
|
6357
6870
|
const sink = createSink();
|
|
6358
6871
|
sink.testInfo = testInfo;
|
|
6872
|
+
sink.baseURL = baseURL ?? null;
|
|
6873
|
+
if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
|
|
6359
6874
|
const pauseSet = currentPauseSet();
|
|
6360
6875
|
if (pauseSet.size) {
|
|
6361
6876
|
const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
|
|
@@ -6461,6 +6976,7 @@ function extendPiwiFixtures(test) {
|
|
|
6461
6976
|
pageMapFetcher,
|
|
6462
6977
|
piwiFixtures,
|
|
6463
6978
|
probeElementAttrs,
|
|
6979
|
+
readPageInteraction,
|
|
6464
6980
|
recordResourceCensus,
|
|
6465
6981
|
resolveCodeReach,
|
|
6466
6982
|
startCodeReach,
|