@piwitests/reporter 0.26.1 → 0.27.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/README.md +20 -286
- package/dist/cli/index.js +95 -22
- package/dist/global-setup-module.js +75 -7
- package/dist/index.d.ts +35 -0
- package/dist/index.js +1473 -153
- package/dist/internal/capture/attachments.d.ts +2 -0
- package/dist/internal/capture/attachments.js +2 -0
- package/dist/internal/capture/capture-fixtures.d.ts +8 -1
- package/dist/internal/capture/capture-fixtures.js +156 -4
- package/dist/internal/capture/locator-healing.js +1 -0
- package/package.json +1 -1
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
declare const ATTACHMENT_NAMES: {
|
|
9
9
|
readonly locators: "piwi-locators";
|
|
10
10
|
readonly ariaSnapshot: "piwi-aria-snapshot";
|
|
11
|
+
readonly ariaSnapshotJson: "piwi-aria-snapshot-json";
|
|
11
12
|
readonly console: "piwi-console";
|
|
13
|
+
readonly dialogs: "piwi-dialogs";
|
|
12
14
|
readonly network: "piwi-network";
|
|
13
15
|
readonly webVitals: "piwi-web-vitals";
|
|
14
16
|
readonly locatorSuggestion: "piwi-locator-suggestion";
|
|
@@ -29,7 +29,9 @@ module.exports = __toCommonJS(attachments_exports);
|
|
|
29
29
|
var ATTACHMENT_NAMES = {
|
|
30
30
|
locators: "piwi-locators",
|
|
31
31
|
ariaSnapshot: "piwi-aria-snapshot",
|
|
32
|
+
ariaSnapshotJson: "piwi-aria-snapshot-json",
|
|
32
33
|
console: "piwi-console",
|
|
34
|
+
dialogs: "piwi-dialogs",
|
|
33
35
|
network: "piwi-network",
|
|
34
36
|
webVitals: "piwi-web-vitals",
|
|
35
37
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
@@ -93,6 +93,13 @@ declare const CAPTURED_ATTRS_ARG: ProbeArg;
|
|
|
93
93
|
* - < 1.49: `locator.ariaSnapshot` does not exist — returns null up front.
|
|
94
94
|
*/
|
|
95
95
|
declare function ariaSnapshotBestEffort(target: Locator, timeout?: number): Promise<string | null>;
|
|
96
|
+
/**
|
|
97
|
+
* The aria tree as JSON (Playwright ≥ 1.63's `ariaSnapshotJSON()`), stringified,
|
|
98
|
+
* or null on any older Playwright or on failure. Feature-detected the same way
|
|
99
|
+
* as `ariaSnapshotBestEffort`, so an older Playwright without the method simply
|
|
100
|
+
* yields null and the capture proceeds with the YAML alone.
|
|
101
|
+
*/
|
|
102
|
+
declare function ariaSnapshotJSONBestEffort(target: Locator, timeout?: number): Promise<string | null>;
|
|
96
103
|
/**
|
|
97
104
|
* The fixtures `piwiFixtures` / `extendPiwiFixtures` contribute. The single
|
|
98
105
|
* added fixture is `piwiCapture`: an auto, test-scoped teardown hook that
|
|
@@ -132,4 +139,4 @@ declare const piwiFixtures: Fixtures<PiwiFixtures, {}, PlaywrightTestArgs & Play
|
|
|
132
139
|
*/
|
|
133
140
|
declare function extendPiwiFixtures<TestArgs extends FixtureArgs, WorkerArgs extends FixtureArgs>(test: TestType<TestArgs, WorkerArgs>): TestType<TestArgs & PiwiFixtures, WorkerArgs>;
|
|
134
141
|
|
|
135
|
-
export { CAPTURED_ATTRS_ARG, type PageState, type PiwiFixtures, type RawPageState, type RawVitalEntry, ariaSnapshotBestEffort, buildPageState, computeCoreVitals, extendPiwiFixtures, piwiFixtures };
|
|
142
|
+
export { CAPTURED_ATTRS_ARG, type PageState, type PiwiFixtures, type RawPageState, type RawVitalEntry, ariaSnapshotBestEffort, ariaSnapshotJSONBestEffort, buildPageState, computeCoreVitals, extendPiwiFixtures, piwiFixtures };
|
|
@@ -32,6 +32,7 @@ var capture_fixtures_exports = {};
|
|
|
32
32
|
__export(capture_fixtures_exports, {
|
|
33
33
|
CAPTURED_ATTRS_ARG: () => CAPTURED_ATTRS_ARG,
|
|
34
34
|
ariaSnapshotBestEffort: () => ariaSnapshotBestEffort,
|
|
35
|
+
ariaSnapshotJSONBestEffort: () => ariaSnapshotJSONBestEffort,
|
|
35
36
|
buildPageState: () => buildPageState,
|
|
36
37
|
computeCoreVitals: () => computeCoreVitals,
|
|
37
38
|
extendPiwiFixtures: () => extendPiwiFixtures,
|
|
@@ -169,15 +170,24 @@ function probeElementAttrs(el, arg) {
|
|
|
169
170
|
let index = -1;
|
|
170
171
|
let levelCount = 0;
|
|
171
172
|
let roleNameCount = 0;
|
|
173
|
+
let visibleRoleNameCount = 0;
|
|
172
174
|
for (let i = 0; i < nodes.length; i++) {
|
|
173
175
|
const n = nodes[i];
|
|
174
176
|
if (roleOf(n) !== targetRole) continue;
|
|
175
177
|
if (n === el) index = roleCountAll;
|
|
176
178
|
roleCountAll++;
|
|
177
179
|
if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
|
|
178
|
-
if (targetName != null && nameOf(n) === targetName)
|
|
180
|
+
if (targetName != null && nameOf(n) === targetName) {
|
|
181
|
+
roleNameCount++;
|
|
182
|
+
const node = n;
|
|
183
|
+
const box = typeof node.getBoundingClientRect === "function" ? node.getBoundingClientRect() : null;
|
|
184
|
+
if (node.offsetParent != null || !!box && box.width > 0 && box.height > 0) visibleRoleNameCount++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (targetName != null) {
|
|
188
|
+
selectorCounts.roleName = roleNameCount;
|
|
189
|
+
selectorCounts.visibleRoleName = visibleRoleNameCount;
|
|
179
190
|
}
|
|
180
|
-
if (targetName != null) selectorCounts.roleName = roleNameCount;
|
|
181
191
|
if (index !== -1) {
|
|
182
192
|
rolePosition = {
|
|
183
193
|
role: targetRole,
|
|
@@ -1488,6 +1498,7 @@ var CHAIN_METHODS = [
|
|
|
1488
1498
|
"nth",
|
|
1489
1499
|
"last",
|
|
1490
1500
|
"filter",
|
|
1501
|
+
"visible",
|
|
1491
1502
|
"and",
|
|
1492
1503
|
"or",
|
|
1493
1504
|
"locator",
|
|
@@ -1727,7 +1738,9 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
1727
1738
|
var ATTACHMENT_NAMES = {
|
|
1728
1739
|
locators: "piwi-locators",
|
|
1729
1740
|
ariaSnapshot: "piwi-aria-snapshot",
|
|
1741
|
+
ariaSnapshotJson: "piwi-aria-snapshot-json",
|
|
1730
1742
|
console: "piwi-console",
|
|
1743
|
+
dialogs: "piwi-dialogs",
|
|
1731
1744
|
network: "piwi-network",
|
|
1732
1745
|
webVitals: "piwi-web-vitals",
|
|
1733
1746
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
@@ -2018,6 +2031,52 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
2018
2031
|
return true;
|
|
2019
2032
|
}
|
|
2020
2033
|
|
|
2034
|
+
// src/internal/support/aria-sampling.ts
|
|
2035
|
+
var path3 = __toESM(require("path"));
|
|
2036
|
+
var os2 = __toESM(require("os"));
|
|
2037
|
+
var fs = __toESM(require("fs"));
|
|
2038
|
+
|
|
2039
|
+
// src/internal/support/instance-id.ts
|
|
2040
|
+
var crypto = __toESM(require("crypto"));
|
|
2041
|
+
var os = __toESM(require("os"));
|
|
2042
|
+
function hashForProject(projectName) {
|
|
2043
|
+
return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// src/internal/support/aria-sampling.ts
|
|
2047
|
+
function ariaSampleIdentity(filePath, title) {
|
|
2048
|
+
return `${filePath}\0${title}`;
|
|
2049
|
+
}
|
|
2050
|
+
function getAriaSampleFilePath(projectName) {
|
|
2051
|
+
return path3.join(os2.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
|
|
2052
|
+
}
|
|
2053
|
+
var cachedSets = /* @__PURE__ */ new Map();
|
|
2054
|
+
function loadAriaSampleSet(projectName) {
|
|
2055
|
+
if (cachedSets.has(projectName)) return cachedSets.get(projectName);
|
|
2056
|
+
let set = null;
|
|
2057
|
+
try {
|
|
2058
|
+
const raw = fs.readFileSync(getAriaSampleFilePath(projectName), "utf8");
|
|
2059
|
+
const parsed = JSON.parse(raw);
|
|
2060
|
+
if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
|
|
2061
|
+
set = new Set(parsed.identities.filter((x) => typeof x === "string"));
|
|
2062
|
+
}
|
|
2063
|
+
} catch {
|
|
2064
|
+
set = null;
|
|
2065
|
+
}
|
|
2066
|
+
cachedSets.set(projectName, set);
|
|
2067
|
+
return set;
|
|
2068
|
+
}
|
|
2069
|
+
function relativeTestFile(file) {
|
|
2070
|
+
return path3.relative(process.cwd(), file).split(path3.sep).join("/");
|
|
2071
|
+
}
|
|
2072
|
+
function isDueForAriaSample(testInfo) {
|
|
2073
|
+
const projectName = process.env.PIWI_PROJECT_NAME;
|
|
2074
|
+
if (!projectName) return false;
|
|
2075
|
+
const set = loadAriaSampleSet(projectName);
|
|
2076
|
+
if (!set || set.size === 0) return false;
|
|
2077
|
+
return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2021
2080
|
// src/internal/capture/capture-fixtures.ts
|
|
2022
2081
|
function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
2023
2082
|
const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
@@ -2052,6 +2111,7 @@ function createSink() {
|
|
|
2052
2111
|
return {
|
|
2053
2112
|
networkRequests: [],
|
|
2054
2113
|
consoleEntries: [],
|
|
2114
|
+
dialogs: [],
|
|
2055
2115
|
pendingHandlers: [],
|
|
2056
2116
|
capturedLocators: [],
|
|
2057
2117
|
capturePromises: [],
|
|
@@ -2062,6 +2122,7 @@ function createSink() {
|
|
|
2062
2122
|
stashedWebVitals: null,
|
|
2063
2123
|
stashedPageState: null,
|
|
2064
2124
|
stashedAria: null,
|
|
2125
|
+
stashedAriaJson: null,
|
|
2065
2126
|
pickOffered: false,
|
|
2066
2127
|
userPick: null
|
|
2067
2128
|
};
|
|
@@ -2250,9 +2311,17 @@ async function stashPageState(sink, closing) {
|
|
|
2250
2311
|
if (pageState) sink.stashedPageState = pageState;
|
|
2251
2312
|
}
|
|
2252
2313
|
const status = sink.testInfo?.status;
|
|
2253
|
-
|
|
2254
|
-
const
|
|
2314
|
+
const sampleAria = async () => {
|
|
2315
|
+
const root = page.locator(":root");
|
|
2316
|
+
const aria = await ariaSnapshotBestEffort(root, 1e3);
|
|
2255
2317
|
if (aria) sink.stashedAria = aria;
|
|
2318
|
+
const ariaJson = await ariaSnapshotJSONBestEffort(root, 1e3);
|
|
2319
|
+
if (ariaJson) sink.stashedAriaJson = ariaJson;
|
|
2320
|
+
};
|
|
2321
|
+
if (status === "failed" || status === "timedOut" || status === "interrupted") {
|
|
2322
|
+
await sampleAria();
|
|
2323
|
+
} else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
|
|
2324
|
+
await sampleAria();
|
|
2256
2325
|
}
|
|
2257
2326
|
}
|
|
2258
2327
|
async function maybeOpenPicker(sink, closing) {
|
|
@@ -2283,6 +2352,7 @@ var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
|
|
|
2283
2352
|
var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
|
|
2284
2353
|
var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
|
|
2285
2354
|
var ACTION_METHOD_SET = new Set(ACTION_METHODS);
|
|
2355
|
+
var LOCATOR_METHOD_SET = new Set(LOCATOR_METHODS);
|
|
2286
2356
|
var CAPTURED_ATTRS_ARG = {
|
|
2287
2357
|
keep: [...CAPTURED_ATTRIBUTES],
|
|
2288
2358
|
tagRoles: TAG_TO_ROLE,
|
|
@@ -2335,6 +2405,16 @@ async function ariaSnapshotBestEffort(target, timeout) {
|
|
|
2335
2405
|
}
|
|
2336
2406
|
}
|
|
2337
2407
|
}
|
|
2408
|
+
async function ariaSnapshotJSONBestEffort(target, timeout) {
|
|
2409
|
+
const fn = target.ariaSnapshotJSON;
|
|
2410
|
+
if (typeof fn !== "function") return null;
|
|
2411
|
+
try {
|
|
2412
|
+
const tree = await fn.call(target, timeout != null ? { timeout } : {});
|
|
2413
|
+
return tree == null ? null : JSON.stringify(tree);
|
|
2414
|
+
} catch {
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2338
2418
|
function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
2339
2419
|
const probe = probeElement(page, target);
|
|
2340
2420
|
const settledProbe = probe.then(
|
|
@@ -2469,6 +2549,20 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
2469
2549
|
}
|
|
2470
2550
|
});
|
|
2471
2551
|
}
|
|
2552
|
+
function wrapFrameLocator(page, frameLocator) {
|
|
2553
|
+
return new Proxy(frameLocator, {
|
|
2554
|
+
get(target, prop) {
|
|
2555
|
+
const original = Reflect.get(target, prop);
|
|
2556
|
+
if (typeof original !== "function") return original;
|
|
2557
|
+
const fn = original;
|
|
2558
|
+
if (!LOCATOR_METHOD_SET.has(prop)) return original;
|
|
2559
|
+
return (...args) => {
|
|
2560
|
+
if (currentSink) currentSink.lastActivePage = page;
|
|
2561
|
+
return wrapLocator(page, fn.apply(target, args), String(prop), args);
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2472
2566
|
function instrumentPage(page) {
|
|
2473
2567
|
if (!page || INSTRUMENTED_PAGES.has(page)) return;
|
|
2474
2568
|
INSTRUMENTED_PAGES.add(page);
|
|
@@ -2496,6 +2590,13 @@ function instrumentPage(page) {
|
|
|
2496
2590
|
return wrapLocator(page, original(...args), method, args);
|
|
2497
2591
|
};
|
|
2498
2592
|
}
|
|
2593
|
+
const originalFrameLocator = typeof page.frameLocator === "function" ? page.frameLocator.bind(page) : null;
|
|
2594
|
+
if (originalFrameLocator) {
|
|
2595
|
+
page.frameLocator = (...args) => {
|
|
2596
|
+
const frame = originalFrameLocator(...args);
|
|
2597
|
+
return args.length === 0 ? wrapFrameLocator(page, frame) : frame;
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2499
2600
|
if (typeof page.on === "function") {
|
|
2500
2601
|
page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
|
|
2501
2602
|
}
|
|
@@ -2514,6 +2615,24 @@ function instrumentPage(page) {
|
|
|
2514
2615
|
});
|
|
2515
2616
|
}
|
|
2516
2617
|
});
|
|
2618
|
+
if (typeof page.on === "function") {
|
|
2619
|
+
try {
|
|
2620
|
+
page.on("dialogclosed", (dialog) => {
|
|
2621
|
+
const sink = currentSink;
|
|
2622
|
+
if (!sink) return;
|
|
2623
|
+
try {
|
|
2624
|
+
sink.dialogs.push({
|
|
2625
|
+
type: typeof dialog.type === "function" ? dialog.type() : null,
|
|
2626
|
+
message: typeof dialog.message === "function" ? dialog.message() : null,
|
|
2627
|
+
defaultValue: typeof dialog.defaultValue === "function" ? dialog.defaultValue() || null : null,
|
|
2628
|
+
closedAt: Date.now()
|
|
2629
|
+
});
|
|
2630
|
+
} catch {
|
|
2631
|
+
}
|
|
2632
|
+
});
|
|
2633
|
+
} catch {
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2517
2636
|
page.on("requestfinished", (request) => {
|
|
2518
2637
|
const sink = currentSink;
|
|
2519
2638
|
if (!sink) return;
|
|
@@ -2640,6 +2759,13 @@ async function flushSink(sink, testInfo) {
|
|
|
2640
2759
|
contentType: "text/plain",
|
|
2641
2760
|
body: snapshot
|
|
2642
2761
|
});
|
|
2762
|
+
const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
|
|
2763
|
+
if (snapshotJson) {
|
|
2764
|
+
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
2765
|
+
contentType: "application/json",
|
|
2766
|
+
body: snapshotJson
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2643
2769
|
const failed = sink.failedLocators[sink.failedLocators.length - 1];
|
|
2644
2770
|
const suggestion = failed ? suggestLocatorsFromAria(failed, snapshot) : null;
|
|
2645
2771
|
if (suggestion) {
|
|
@@ -2656,6 +2782,25 @@ async function flushSink(sink, testInfo) {
|
|
|
2656
2782
|
} catch {
|
|
2657
2783
|
}
|
|
2658
2784
|
}
|
|
2785
|
+
if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
|
|
2786
|
+
try {
|
|
2787
|
+
const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
|
|
2788
|
+
if (snapshot) {
|
|
2789
|
+
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
|
|
2790
|
+
contentType: "text/plain",
|
|
2791
|
+
body: snapshot
|
|
2792
|
+
});
|
|
2793
|
+
const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
|
|
2794
|
+
if (snapshotJson) {
|
|
2795
|
+
await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
|
|
2796
|
+
contentType: "application/json",
|
|
2797
|
+
body: snapshotJson
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
} catch {
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2659
2804
|
if (sink.userPick) {
|
|
2660
2805
|
const pick = sink.userPick;
|
|
2661
2806
|
testInfo.annotations.push({
|
|
@@ -2679,6 +2824,12 @@ async function flushSink(sink, testInfo) {
|
|
|
2679
2824
|
body: Buffer.from(JSON.stringify(sink.consoleEntries))
|
|
2680
2825
|
});
|
|
2681
2826
|
}
|
|
2827
|
+
if (sink.dialogs.length > 0) {
|
|
2828
|
+
await testInfo.attach(ATTACHMENT_NAMES.dialogs, {
|
|
2829
|
+
contentType: "application/json",
|
|
2830
|
+
body: Buffer.from(JSON.stringify(sink.dialogs))
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2682
2833
|
if (sink.networkRequests.length > 0) {
|
|
2683
2834
|
await testInfo.attach(ATTACHMENT_NAMES.network, {
|
|
2684
2835
|
contentType: "application/json",
|
|
@@ -2744,6 +2895,7 @@ function extendPiwiFixtures(test) {
|
|
|
2744
2895
|
0 && (module.exports = {
|
|
2745
2896
|
CAPTURED_ATTRS_ARG,
|
|
2746
2897
|
ariaSnapshotBestEffort,
|
|
2898
|
+
ariaSnapshotJSONBestEffort,
|
|
2747
2899
|
buildPageState,
|
|
2748
2900
|
computeCoreVitals,
|
|
2749
2901
|
extendPiwiFixtures,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
|
|
5
5
|
"url": "https://github.com/PiwiTests/platform",
|
|
6
6
|
"homepage": "https://piwitests.dev",
|