@piwitests/reporter 0.26.0 → 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.
@@ -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) roleNameCount++;
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
- if (status === "failed" || status === "timedOut" || status === "interrupted") {
2254
- const aria = await ariaSnapshotBestEffort(page.locator(":root"), 1e3);
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,
@@ -507,6 +507,7 @@ var CHAIN_METHODS = [
507
507
  "nth",
508
508
  "last",
509
509
  "filter",
510
+ "visible",
510
511
  "and",
511
512
  "or",
512
513
  "locator",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piwitests/reporter",
3
- "version": "0.26.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",