@piwitests/reporter 0.51.0 → 0.53.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 +22 -5
- package/dist/index.d.ts +41 -3
- package/dist/index.js +4670 -3485
- 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 +39 -10
- package/dist/internal/capture/capture-fixtures.js +424 -76
- package/package.json +1 -1
|
@@ -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"));
|
|
@@ -3962,6 +4051,16 @@ var fs6 = __toESM(require("fs"));
|
|
|
3962
4051
|
var path10 = __toESM(require("path"));
|
|
3963
4052
|
var import_node_child_process = require("child_process");
|
|
3964
4053
|
|
|
4054
|
+
// ../core/src/coverage.ts
|
|
4055
|
+
var MAX_PATH_LENGTH = 500;
|
|
4056
|
+
function normalizeCoveragePath(raw) {
|
|
4057
|
+
if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
|
|
4058
|
+
const path14 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4059
|
+
if (!path14 || path14.startsWith("../") || path14.startsWith("/") || /^[A-Za-z]:\//.test(path14)) return null;
|
|
4060
|
+
if (/(^|\/)node_modules\//.test(path14)) return null;
|
|
4061
|
+
return path14;
|
|
4062
|
+
}
|
|
4063
|
+
|
|
3965
4064
|
// ../core/src/source-map.ts
|
|
3966
4065
|
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
3967
4066
|
var BASE64_VALUE = new Int8Array(128).fill(-1);
|
|
@@ -4173,10 +4272,8 @@ function reachedSources(entry, map) {
|
|
|
4173
4272
|
function finalizeCodeReach(files) {
|
|
4174
4273
|
const out = /* @__PURE__ */ new Set();
|
|
4175
4274
|
for (const raw of files) {
|
|
4176
|
-
const file = raw
|
|
4177
|
-
if (
|
|
4178
|
-
if (/(^|\/)node_modules\//.test(file)) continue;
|
|
4179
|
-
out.add(file);
|
|
4275
|
+
const file = normalizeCoveragePath(raw);
|
|
4276
|
+
if (file !== null) out.add(file);
|
|
4180
4277
|
}
|
|
4181
4278
|
return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
|
|
4182
4279
|
}
|
|
@@ -4277,8 +4374,8 @@ function fileForUrl(url, roots, bases) {
|
|
|
4277
4374
|
function relativeToRepo(file, repoRoot) {
|
|
4278
4375
|
return path10.relative(repoRoot, file).split(path10.sep).join("/");
|
|
4279
4376
|
}
|
|
4280
|
-
function
|
|
4281
|
-
return crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
4377
|
+
function scriptVersion(entry) {
|
|
4378
|
+
return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
4282
4379
|
}
|
|
4283
4380
|
async function loadMap(entry, fetchMap) {
|
|
4284
4381
|
const ref = entry.source ? sourceMappingUrl(entry.source) : null;
|
|
@@ -4293,7 +4390,7 @@ async function loadMap(entry, fetchMap) {
|
|
|
4293
4390
|
} catch {
|
|
4294
4391
|
return null;
|
|
4295
4392
|
}
|
|
4296
|
-
const version =
|
|
4393
|
+
const version = scriptVersion(entry);
|
|
4297
4394
|
let pending = mapCache.get(absolute, version);
|
|
4298
4395
|
if (!pending) {
|
|
4299
4396
|
pending = fetchMap(absolute).then((text) => text ? parseMap(text) : null);
|
|
@@ -4309,29 +4406,34 @@ function parseMap(text) {
|
|
|
4309
4406
|
}
|
|
4310
4407
|
}
|
|
4311
4408
|
async function resolveScript(entry, roots, bases, fetchMap) {
|
|
4312
|
-
const version =
|
|
4409
|
+
const version = scriptVersion(entry);
|
|
4313
4410
|
const cached = scriptCache.get(entry.url, version);
|
|
4314
4411
|
if (cached) return cached;
|
|
4315
|
-
let resolution;
|
|
4316
4412
|
const file = fileForUrl(entry.url, roots.roots, bases);
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
scriptCache.set(entry.url, version, resolution);
|
|
4413
|
+
const lookup = await loadMap(entry, fetchMap);
|
|
4414
|
+
let resolution;
|
|
4415
|
+
if (file) resolution = { kind: "file", file, lookup, source: entry.source };
|
|
4416
|
+
else resolution = lookup ? { kind: "map", lookup, files: [], source: entry.source } : { kind: "none" };
|
|
4417
|
+
if (entry.source !== void 0) scriptCache.set(entry.url, version, resolution);
|
|
4323
4418
|
return resolution;
|
|
4324
4419
|
}
|
|
4420
|
+
function withSource(entry, resolution) {
|
|
4421
|
+
if (entry.source !== void 0 || resolution.source === void 0) return entry;
|
|
4422
|
+
return { ...entry, source: resolution.source };
|
|
4423
|
+
}
|
|
4325
4424
|
async function resolveCodeReach(entries, roots, fetchMap) {
|
|
4326
4425
|
const files = /* @__PURE__ */ new Set();
|
|
4327
4426
|
const bases = viteBases(entries);
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4427
|
+
const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
|
|
4428
|
+
const known = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
|
|
4429
|
+
for (const [i, covered] of scripts.entries()) {
|
|
4430
|
+
const resolution = known[i] ?? await resolveScript(covered, roots, bases, fetchMap);
|
|
4331
4431
|
if (resolution.kind === "file") {
|
|
4432
|
+
const entry = withSource(covered, resolution);
|
|
4332
4433
|
const ran = resolution.lookup ? reachedSources(entry, resolution.lookup).size > 0 : scriptRan(entry);
|
|
4333
4434
|
if (resolution.file && ran) files.add(relativeToRepo(resolution.file, roots.repoRoot));
|
|
4334
4435
|
} else if (resolution.kind === "map") {
|
|
4436
|
+
const entry = withSource(covered, resolution);
|
|
4335
4437
|
for (const index of reachedSources(entry, resolution.lookup)) {
|
|
4336
4438
|
if (resolution.files[index] === void 0) {
|
|
4337
4439
|
const source = resolution.lookup.sources[index] ?? "";
|
|
@@ -4373,23 +4475,85 @@ function isChromiumPage(page) {
|
|
|
4373
4475
|
return false;
|
|
4374
4476
|
}
|
|
4375
4477
|
}
|
|
4478
|
+
var captures = /* @__PURE__ */ new WeakMap();
|
|
4479
|
+
function versionKey(url, hash) {
|
|
4480
|
+
return `${hash} ${url}`;
|
|
4481
|
+
}
|
|
4482
|
+
function onScriptParsed(page, capture, event) {
|
|
4483
|
+
if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
|
|
4484
|
+
capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash });
|
|
4485
|
+
const key = versionKey(event.url, event.hash);
|
|
4486
|
+
if (capture.sources.has(key) || scriptCache.get(event.url, event.hash)) return;
|
|
4487
|
+
const source = internalCall(
|
|
4488
|
+
page,
|
|
4489
|
+
() => capture.cdp.send("Debugger.getScriptSource", { scriptId: event.scriptId })
|
|
4490
|
+
).then(
|
|
4491
|
+
(response) => response.scriptSource,
|
|
4492
|
+
() => {
|
|
4493
|
+
if (capture.sources.get(key) === source) capture.sources.delete(key);
|
|
4494
|
+
return null;
|
|
4495
|
+
}
|
|
4496
|
+
);
|
|
4497
|
+
capture.sources.set(key, source);
|
|
4498
|
+
}
|
|
4499
|
+
async function detach(page, cdp) {
|
|
4500
|
+
try {
|
|
4501
|
+
await internalCall(page, () => cdp.detach());
|
|
4502
|
+
} catch {
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4376
4505
|
async function startCodeReach(page) {
|
|
4377
|
-
if (!isChromiumPage(page) ||
|
|
4506
|
+
if (!isChromiumPage(page) || captures.has(page)) return false;
|
|
4507
|
+
let cdp = null;
|
|
4378
4508
|
try {
|
|
4509
|
+
cdp = await internalCall(page, () => page.context().newCDPSession(page));
|
|
4510
|
+
const capture = { cdp, scripts: /* @__PURE__ */ new Map(), sources: /* @__PURE__ */ new Map() };
|
|
4511
|
+
cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
|
|
4512
|
+
cdp.on("Debugger.paused", () => {
|
|
4513
|
+
internalCall(page, () => capture.cdp.send("Debugger.resume")).catch(() => {
|
|
4514
|
+
});
|
|
4515
|
+
});
|
|
4379
4516
|
await internalCall(
|
|
4380
4517
|
page,
|
|
4381
|
-
() =>
|
|
4518
|
+
() => Promise.all([
|
|
4519
|
+
capture.cdp.send("Debugger.enable"),
|
|
4520
|
+
capture.cdp.send("Debugger.setSkipAllPauses", { skip: true }),
|
|
4521
|
+
capture.cdp.send("Profiler.enable"),
|
|
4522
|
+
capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
|
|
4523
|
+
])
|
|
4382
4524
|
);
|
|
4525
|
+
captures.set(page, capture);
|
|
4383
4526
|
return true;
|
|
4384
4527
|
} catch {
|
|
4528
|
+
if (cdp) await detach(page, cdp);
|
|
4385
4529
|
return false;
|
|
4386
4530
|
}
|
|
4387
4531
|
}
|
|
4388
4532
|
async function stopCodeReach(page) {
|
|
4533
|
+
const capture = captures.get(page);
|
|
4534
|
+
if (!capture) return null;
|
|
4535
|
+
captures.delete(page);
|
|
4536
|
+
const { cdp } = capture;
|
|
4389
4537
|
try {
|
|
4390
|
-
|
|
4538
|
+
const coverage = await internalCall(page, async () => {
|
|
4539
|
+
const taken = await cdp.send("Profiler.takePreciseCoverage");
|
|
4540
|
+
await cdp.send("Profiler.stopPreciseCoverage");
|
|
4541
|
+
return taken;
|
|
4542
|
+
});
|
|
4543
|
+
const entries = [];
|
|
4544
|
+
for (const script of coverage.result) {
|
|
4545
|
+
const parsed2 = capture.scripts.get(script.scriptId);
|
|
4546
|
+
if (!parsed2) continue;
|
|
4547
|
+
const entry = { url: parsed2.url, hash: parsed2.hash, functions: script.functions };
|
|
4548
|
+
const source = await capture.sources.get(versionKey(parsed2.url, parsed2.hash));
|
|
4549
|
+
if (source != null) entry.source = source;
|
|
4550
|
+
entries.push(entry);
|
|
4551
|
+
}
|
|
4552
|
+
return entries;
|
|
4391
4553
|
} catch {
|
|
4392
4554
|
return null;
|
|
4555
|
+
} finally {
|
|
4556
|
+
await detach(page, cdp);
|
|
4393
4557
|
}
|
|
4394
4558
|
}
|
|
4395
4559
|
|
|
@@ -5258,6 +5422,106 @@ async function recordResourceCensus(testInfo) {
|
|
|
5258
5422
|
if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
|
|
5259
5423
|
}
|
|
5260
5424
|
|
|
5425
|
+
// src/internal/capture/page-interaction.ts
|
|
5426
|
+
function readPageInteraction() {
|
|
5427
|
+
const doc = globalThis.document;
|
|
5428
|
+
const SKIPPED_TEXT = ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"];
|
|
5429
|
+
const pathOf = (el) => {
|
|
5430
|
+
const levels = [];
|
|
5431
|
+
for (let node = el; node; ) {
|
|
5432
|
+
const steps = [];
|
|
5433
|
+
let root = null;
|
|
5434
|
+
for (let cur = node; cur; cur = cur.parentElement) {
|
|
5435
|
+
let position = 1;
|
|
5436
|
+
for (let s = cur.previousElementSibling; s; s = s.previousElementSibling) {
|
|
5437
|
+
if (s.localName === cur.localName) position++;
|
|
5438
|
+
}
|
|
5439
|
+
steps.unshift(`${cur.localName}:${position}`);
|
|
5440
|
+
if (!cur.parentElement) root = cur.parentNode;
|
|
5441
|
+
}
|
|
5442
|
+
levels.unshift(steps.join("/"));
|
|
5443
|
+
node = root && root.host ? root.host : null;
|
|
5444
|
+
}
|
|
5445
|
+
return levels.join("|");
|
|
5446
|
+
};
|
|
5447
|
+
const isField = (el) => /^(input|textarea|select)$/.test(el.localName) || !!el.isContentEditable;
|
|
5448
|
+
const label = (el) => {
|
|
5449
|
+
const tag = el.localName;
|
|
5450
|
+
if (el.id) return `${tag}#${el.id}`;
|
|
5451
|
+
for (const attr2 of ["name", "aria-label"]) {
|
|
5452
|
+
const value = el.getAttribute(attr2);
|
|
5453
|
+
if (value) return `${tag}[${attr2}="${value}"]`;
|
|
5454
|
+
}
|
|
5455
|
+
const text = isField(el) ? "" : String(el.textContent || "").replace(/\s+/g, " ").trim();
|
|
5456
|
+
return text ? `${tag} "${text.length > 40 ? `${text.slice(0, 40)}\u2026` : text}"` : tag;
|
|
5457
|
+
};
|
|
5458
|
+
const ref = (el) => ({ path: pathOf(el), label: label(el) });
|
|
5459
|
+
const textOffset = (el, stop, extra) => {
|
|
5460
|
+
const walker = doc.createTreeWalker(el, 4);
|
|
5461
|
+
let offset = 0;
|
|
5462
|
+
for (let text = walker.nextNode(); text; text = walker.nextNode()) {
|
|
5463
|
+
if (stop && (text === stop || stop.contains(text) || stop.compareDocumentPosition(text) & 4)) break;
|
|
5464
|
+
if (!SKIPPED_TEXT.includes(text.parentElement?.tagName)) offset += text.data.length;
|
|
5465
|
+
}
|
|
5466
|
+
return offset + extra;
|
|
5467
|
+
};
|
|
5468
|
+
const textPoint2 = (node, offset) => {
|
|
5469
|
+
if (node.nodeType === 3)
|
|
5470
|
+
return { path: pathOf(node.parentElement), offset: textOffset(node.parentElement, node, offset) };
|
|
5471
|
+
return { path: pathOf(node), offset: textOffset(node, node.childNodes[offset] ?? null, 0) };
|
|
5472
|
+
};
|
|
5473
|
+
const out = {};
|
|
5474
|
+
let active = doc.activeElement;
|
|
5475
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
5476
|
+
if (active && active !== doc.body && active !== doc.documentElement) {
|
|
5477
|
+
let visible = false;
|
|
5478
|
+
try {
|
|
5479
|
+
visible = active.matches(":focus-visible");
|
|
5480
|
+
} catch {
|
|
5481
|
+
}
|
|
5482
|
+
out.focus = { element: ref(active), visible };
|
|
5483
|
+
try {
|
|
5484
|
+
if (typeof active.selectionStart === "number" && typeof active.selectionEnd === "number") {
|
|
5485
|
+
out.focus.selection = [active.selectionStart, active.selectionEnd];
|
|
5486
|
+
}
|
|
5487
|
+
} catch {
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
let scope = doc;
|
|
5491
|
+
for (; ; ) {
|
|
5492
|
+
const chain = scope.querySelectorAll(":hover");
|
|
5493
|
+
const innermost = chain[chain.length - 1];
|
|
5494
|
+
if (!innermost) break;
|
|
5495
|
+
out.hover = ref(innermost);
|
|
5496
|
+
if (!innermost.shadowRoot) break;
|
|
5497
|
+
scope = innermost.shadowRoot;
|
|
5498
|
+
}
|
|
5499
|
+
const selection = doc.getSelection?.();
|
|
5500
|
+
if (selection && selection.rangeCount > 0 && !selection.isCollapsed && !(active && isField(active))) {
|
|
5501
|
+
const range = selection.getRangeAt(0);
|
|
5502
|
+
const start = range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer;
|
|
5503
|
+
const end = range.endContainer.nodeType === 3 ? range.endContainer.parentElement : range.endContainer;
|
|
5504
|
+
if (start?.nodeType === 1 && end?.nodeType === 1) {
|
|
5505
|
+
out.selection = {
|
|
5506
|
+
start: textPoint2(range.startContainer, range.startOffset),
|
|
5507
|
+
end: textPoint2(range.endContainer, range.endOffset),
|
|
5508
|
+
length: String(selection).length
|
|
5509
|
+
};
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
const boxes = [];
|
|
5513
|
+
const roots = [doc];
|
|
5514
|
+
for (let i = 0; i < roots.length && boxes.length < 20; i++) {
|
|
5515
|
+
for (const el of roots[i].querySelectorAll("*")) {
|
|
5516
|
+
if (el.shadowRoot) roots.push(el.shadowRoot);
|
|
5517
|
+
if (el.localName === "input" && el.type === "checkbox" && el.indeterminate && boxes.length < 20)
|
|
5518
|
+
boxes.push(ref(el));
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
if (boxes.length) out.indeterminate = boxes;
|
|
5522
|
+
return out;
|
|
5523
|
+
}
|
|
5524
|
+
|
|
5261
5525
|
// src/internal/capture/capture-fixtures.ts
|
|
5262
5526
|
function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
5263
5527
|
const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
@@ -5291,6 +5555,7 @@ function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
|
5291
5555
|
function createSink() {
|
|
5292
5556
|
return {
|
|
5293
5557
|
networkRequests: [],
|
|
5558
|
+
openResponses: /* @__PURE__ */ new Map(),
|
|
5294
5559
|
consoleEntries: [],
|
|
5295
5560
|
dialogs: [],
|
|
5296
5561
|
pendingHandlers: [],
|
|
@@ -5304,6 +5569,8 @@ function createSink() {
|
|
|
5304
5569
|
stashedPageState: null,
|
|
5305
5570
|
stashedAria: null,
|
|
5306
5571
|
stashedAriaJson: null,
|
|
5572
|
+
failureScreenshot: null,
|
|
5573
|
+
stashedScreenshot: null,
|
|
5307
5574
|
pageInventories: [],
|
|
5308
5575
|
probeItem: null,
|
|
5309
5576
|
probePageErrors: 0,
|
|
@@ -5314,12 +5581,13 @@ function createSink() {
|
|
|
5314
5581
|
userPick: null,
|
|
5315
5582
|
pause: null,
|
|
5316
5583
|
locatorPages: new LocatorPageLog(),
|
|
5317
|
-
codeReach: null
|
|
5584
|
+
codeReach: null,
|
|
5585
|
+
serverCoverage: /* @__PURE__ */ new Map()
|
|
5318
5586
|
};
|
|
5319
5587
|
}
|
|
5320
5588
|
function maybeStartCodeReach(page) {
|
|
5321
5589
|
const sink = currentSink;
|
|
5322
|
-
if (!sink ||
|
|
5590
|
+
if (!sink || !browserCoverageInWorker()) return Promise.resolve(false);
|
|
5323
5591
|
if (sink.codeReach) return sink.codeReach.page === page ? sink.codeReach.started : Promise.resolve(false);
|
|
5324
5592
|
const started = startCodeReach(page);
|
|
5325
5593
|
sink.codeReach = { page, started, stopped: false, files: null };
|
|
@@ -5460,7 +5728,7 @@ var PAGE_STATE_MAX_STORAGE_KEYS = 50;
|
|
|
5460
5728
|
var PAGE_STATE_MAX_COOKIES = 30;
|
|
5461
5729
|
var PAGE_STATE_HISTORY_CAP = 2048;
|
|
5462
5730
|
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) {
|
|
5731
|
+
function buildPageState(raw, cookies, interaction) {
|
|
5464
5732
|
let historyState = raw.historyState;
|
|
5465
5733
|
if (historyState) {
|
|
5466
5734
|
for (const re of TOKEN_MASK_RES) historyState = historyState.replace(re, "[masked]");
|
|
@@ -5486,7 +5754,8 @@ function buildPageState(raw, cookies) {
|
|
|
5486
5754
|
secure: Boolean(c.secure),
|
|
5487
5755
|
...c.sameSite !== void 0 ? { sameSite: String(c.sameSite) } : {},
|
|
5488
5756
|
...typeof c.expires === "number" ? { expires: c.expires } : {}
|
|
5489
|
-
}))
|
|
5757
|
+
})),
|
|
5758
|
+
...sanitizePageInteraction(interaction)
|
|
5490
5759
|
};
|
|
5491
5760
|
}
|
|
5492
5761
|
async function readPageState(page) {
|
|
@@ -5524,7 +5793,8 @@ async function readPageState(page) {
|
|
|
5524
5793
|
} catch {
|
|
5525
5794
|
cookies = null;
|
|
5526
5795
|
}
|
|
5527
|
-
|
|
5796
|
+
const interaction = await page.evaluate(readPageInteraction).catch(() => null);
|
|
5797
|
+
return buildPageState(raw, cookies, interaction);
|
|
5528
5798
|
} catch {
|
|
5529
5799
|
return null;
|
|
5530
5800
|
}
|
|
@@ -5616,6 +5886,8 @@ async function readPageBeforeClose(sink, page) {
|
|
|
5616
5886
|
};
|
|
5617
5887
|
if (status === "failed" || status === "timedOut" || status === "interrupted") {
|
|
5618
5888
|
await sampleAria();
|
|
5889
|
+
if (sink.failureScreenshot)
|
|
5890
|
+
sink.stashedScreenshot = await readFailureScreenshot(page, sink.failureScreenshot) ?? sink.stashedScreenshot;
|
|
5619
5891
|
} else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
|
|
5620
5892
|
await sampleAria();
|
|
5621
5893
|
}
|
|
@@ -6040,59 +6312,91 @@ function instrumentPage(page) {
|
|
|
6040
6312
|
} catch {
|
|
6041
6313
|
}
|
|
6042
6314
|
}
|
|
6043
|
-
|
|
6315
|
+
page.on("response", (response) => {
|
|
6044
6316
|
const sink = currentSink;
|
|
6045
6317
|
if (!sink) return;
|
|
6318
|
+
try {
|
|
6319
|
+
const request = response.request();
|
|
6320
|
+
if (isTrackedRequest(request.url(), request.resourceType())) sink.openResponses.set(request, response);
|
|
6321
|
+
} catch {
|
|
6322
|
+
}
|
|
6323
|
+
});
|
|
6324
|
+
const onRequestEnd = (request, failed) => {
|
|
6325
|
+
const sink = currentSink;
|
|
6326
|
+
if (!sink || RECORDED_OPEN.has(request)) return;
|
|
6046
6327
|
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
|
-
|
|
6328
|
+
const response = sink.openResponses.get(request) ?? null;
|
|
6329
|
+
sink.openResponses.delete(request);
|
|
6330
|
+
recordRequest(sink, request, failed ? "failed" : "finished", response);
|
|
6331
|
+
};
|
|
6332
|
+
page.on("requestfinished", (request) => onRequestEnd(request, false));
|
|
6333
|
+
page.on("requestfailed", (request) => onRequestEnd(request, true));
|
|
6334
|
+
}
|
|
6335
|
+
var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
|
|
6336
|
+
function isTrackedRequest(url, resourceType) {
|
|
6337
|
+
if (url.startsWith("data:") || url.startsWith("blob:")) return false;
|
|
6338
|
+
return TRACKED_REQUEST_TYPES.includes(resourceType);
|
|
6339
|
+
}
|
|
6340
|
+
function recordRequest(sink, request, end, response) {
|
|
6341
|
+
const p = (async () => {
|
|
6342
|
+
try {
|
|
6343
|
+
const url = request.url();
|
|
6344
|
+
const resourceType = request.resourceType();
|
|
6345
|
+
if (!isTrackedRequest(url, resourceType)) return;
|
|
6346
|
+
let failure = end === "failed" ? request.failure()?.errorText ?? "failed" : null;
|
|
6347
|
+
if (failure && CANCELLED_REQUEST.test(failure)) {
|
|
6348
|
+
if (!response) return;
|
|
6349
|
+
failure = null;
|
|
6350
|
+
end = "unfinished";
|
|
6351
|
+
}
|
|
6352
|
+
const timing = request.timing();
|
|
6353
|
+
if (end === "failed") response = null;
|
|
6354
|
+
else if (end === "finished") response ??= await request.response();
|
|
6355
|
+
const entry = {
|
|
6356
|
+
method: request.method(),
|
|
6357
|
+
url,
|
|
6358
|
+
status: response ? response.status() : 0,
|
|
6359
|
+
duration: requestDuration(timing, end),
|
|
6360
|
+
startTime: timing.startTime,
|
|
6361
|
+
resourceType
|
|
6362
|
+
};
|
|
6363
|
+
if (failure) entry.failure = failure;
|
|
6364
|
+
if (response) {
|
|
6365
|
+
if (timing.requestStart < 0 && typeof response.serverAddr === "function") {
|
|
6366
|
+
if (await response.serverAddr().catch(() => void 0) === null) entry.fulfilled = true;
|
|
6367
|
+
}
|
|
6368
|
+
const headers = response.headers();
|
|
6369
|
+
const contentType = headers["content-type"];
|
|
6370
|
+
if (contentType) entry.contentType = contentType.split(";")[0].trim();
|
|
6371
|
+
const logHeader = headers["x-piwi-logs"];
|
|
6372
|
+
if (logHeader) {
|
|
6373
|
+
try {
|
|
6374
|
+
entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
|
|
6375
|
+
} catch {
|
|
6079
6376
|
}
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6377
|
+
}
|
|
6378
|
+
const coverageHeader = headers["x-piwi-coverage"];
|
|
6379
|
+
if (coverageHeader && serverCoverageInWorker()) recordServerCoverageIds(sink, url, coverageHeader);
|
|
6380
|
+
const traceHeader = headers["x-piwi-trace"];
|
|
6381
|
+
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
6382
|
+
try {
|
|
6383
|
+
entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
6384
|
+
} catch {
|
|
6086
6385
|
}
|
|
6087
6386
|
}
|
|
6088
|
-
sink.networkRequests.push(entry);
|
|
6089
|
-
} catch {
|
|
6090
6387
|
}
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6388
|
+
sink.networkRequests.push(entry);
|
|
6389
|
+
} catch {
|
|
6390
|
+
}
|
|
6391
|
+
})();
|
|
6392
|
+
sink.pendingHandlers.push(p);
|
|
6393
|
+
}
|
|
6394
|
+
function recordOpenRequests(sink) {
|
|
6395
|
+
for (const [request, response] of sink.openResponses) {
|
|
6396
|
+
RECORDED_OPEN.add(request);
|
|
6397
|
+
recordRequest(sink, request, "unfinished", response);
|
|
6398
|
+
}
|
|
6399
|
+
sink.openResponses.clear();
|
|
6096
6400
|
}
|
|
6097
6401
|
async function awaitFlakeConditions() {
|
|
6098
6402
|
await currentSink?.flake?.conditions;
|
|
@@ -6159,12 +6463,36 @@ function readRootAriaJson(page) {
|
|
|
6159
6463
|
return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
|
|
6160
6464
|
}
|
|
6161
6465
|
var TRACKED_REQUEST_TYPES = ["fetch", "xhr", "document", "other"];
|
|
6162
|
-
|
|
6466
|
+
var SERVER_COVERAGE_ID = /^[\x21-\x7e]{1,128}$/;
|
|
6467
|
+
function recordServerCoverageIds(sink, url, value) {
|
|
6468
|
+
let origin;
|
|
6469
|
+
try {
|
|
6470
|
+
const parsed2 = new URL(url);
|
|
6471
|
+
if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:") return;
|
|
6472
|
+
origin = parsed2.origin;
|
|
6473
|
+
} catch {
|
|
6474
|
+
return;
|
|
6475
|
+
}
|
|
6476
|
+
for (const id of value.split(/[\s,]+/)) {
|
|
6477
|
+
if (!SERVER_COVERAGE_ID.test(id)) continue;
|
|
6478
|
+
let ids = sink.serverCoverage.get(origin);
|
|
6479
|
+
if (!ids) {
|
|
6480
|
+
ids = /* @__PURE__ */ new Set();
|
|
6481
|
+
sink.serverCoverage.set(origin, ids);
|
|
6482
|
+
}
|
|
6483
|
+
ids.add(id);
|
|
6484
|
+
}
|
|
6485
|
+
}
|
|
6486
|
+
function requestDuration(timing, end) {
|
|
6487
|
+
if (end === "unfinished") {
|
|
6488
|
+
return timing.responseStart >= 0 ? Math.max(0, Math.round(timing.responseStart - timing.requestStart)) : 0;
|
|
6489
|
+
}
|
|
6163
6490
|
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));
|
|
6491
|
+
if (end === "failed" && timing.startTime > 0) return Math.max(0, Math.round(Date.now() - timing.startTime));
|
|
6165
6492
|
return 0;
|
|
6166
6493
|
}
|
|
6167
6494
|
async function flushSink(sink, testInfo) {
|
|
6495
|
+
recordOpenRequests(sink);
|
|
6168
6496
|
await Promise.allSettled(sink.pendingHandlers);
|
|
6169
6497
|
let drainDeadline;
|
|
6170
6498
|
await Promise.race([
|
|
@@ -6193,6 +6521,14 @@ async function flushSink(sink, testInfo) {
|
|
|
6193
6521
|
body: Buffer.from(JSON.stringify(sink.codeReach.files))
|
|
6194
6522
|
});
|
|
6195
6523
|
}
|
|
6524
|
+
if (sink.serverCoverage.size > 0) {
|
|
6525
|
+
const requests = {};
|
|
6526
|
+
for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
|
|
6527
|
+
await testInfo.attach(ATTACHMENT_NAMES.serverCoverageRequests, {
|
|
6528
|
+
contentType: "application/json",
|
|
6529
|
+
body: Buffer.from(JSON.stringify(requests))
|
|
6530
|
+
});
|
|
6531
|
+
}
|
|
6196
6532
|
if (sink.locatorPages.size > 0) {
|
|
6197
6533
|
await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
|
|
6198
6534
|
contentType: "application/json",
|
|
@@ -6230,6 +6566,16 @@ async function flushSink(sink, testInfo) {
|
|
|
6230
6566
|
} catch {
|
|
6231
6567
|
}
|
|
6232
6568
|
}
|
|
6569
|
+
if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
|
|
6570
|
+
const option = sink.failureScreenshot;
|
|
6571
|
+
const shot = (pageReadable ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
|
|
6572
|
+
if (shot) {
|
|
6573
|
+
try {
|
|
6574
|
+
await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
|
|
6575
|
+
} catch {
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
}
|
|
6233
6579
|
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
6234
6580
|
try {
|
|
6235
6581
|
const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
|
|
@@ -6350,12 +6696,13 @@ var piwiFixtures = {
|
|
|
6350
6696
|
// (attach the collected data) at teardown. Runs for every test without being
|
|
6351
6697
|
// requested, so suites that never destructure `page` are still captured.
|
|
6352
6698
|
piwiCapture: [
|
|
6353
|
-
async ({}, use, testInfo) => {
|
|
6699
|
+
async ({ screenshot }, use, testInfo) => {
|
|
6354
6700
|
boxCaptureFrames();
|
|
6355
6701
|
const conflict = labModeConflict();
|
|
6356
6702
|
if (conflict) throw new Error(conflict);
|
|
6357
6703
|
const sink = createSink();
|
|
6358
6704
|
sink.testInfo = testInfo;
|
|
6705
|
+
if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
|
|
6359
6706
|
const pauseSet = currentPauseSet();
|
|
6360
6707
|
if (pauseSet.size) {
|
|
6361
6708
|
const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
|
|
@@ -6461,6 +6808,7 @@ function extendPiwiFixtures(test) {
|
|
|
6461
6808
|
pageMapFetcher,
|
|
6462
6809
|
piwiFixtures,
|
|
6463
6810
|
probeElementAttrs,
|
|
6811
|
+
readPageInteraction,
|
|
6464
6812
|
recordResourceCensus,
|
|
6465
6813
|
resolveCodeReach,
|
|
6466
6814
|
startCodeReach,
|