@piwitests/reporter 0.9.1 → 0.11.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 CHANGED
@@ -39,6 +39,19 @@ Run your tests — results are uploaded automatically:
39
39
  npx playwright test
40
40
  ```
41
41
 
42
+ **Recommended: enable the [capture fixtures](#capture-fixtures)** — one small file unlocks the dashboard's richest features (locator healing, slow-endpoint analysis, Web Vitals, console capture, failure-time ARIA snapshots):
43
+
44
+ ```typescript
45
+ // tests/fixtures.ts
46
+ import { test as base, expect } from '@playwright/test'
47
+ import { piwiFixtures } from '@piwitests/reporter'
48
+
49
+ export const test = base.extend(piwiFixtures)
50
+ export { expect }
51
+ ```
52
+
53
+ Import `test` from this file in your specs instead of `@playwright/test` — see [Capture fixtures](#capture-fixtures) below.
54
+
42
55
  Prefer to wire it up by hand? Add the reporter to the `reporter` array instead:
43
56
 
44
57
  ```typescript
@@ -123,35 +136,42 @@ export default defineConfig({
123
136
  })
124
137
  ```
125
138
 
126
- ## Performance Metrics & Web Vitals
139
+ ## Capture fixtures
127
140
 
128
- To capture network request timing and browser Web Vitals, use the provided fixtures:
141
+ The reporter works without any test-code changes, but the **capture fixtures** observe your tests from the inside and unlock the dashboard's richest features. Extend your `test` with them:
129
142
 
130
143
  ```typescript
131
144
  // tests/fixtures.ts
132
145
  import { test as base, expect } from '@playwright/test'
133
- import { dashboardFixtures } from '@piwitests/reporter'
146
+ import { piwiFixtures } from '@piwitests/reporter'
134
147
 
135
- export const test = base.extend(dashboardFixtures)
148
+ export const test = base.extend(piwiFixtures)
136
149
  export { expect }
137
150
  ```
138
151
 
139
- Or extend the base `test` in one line with `extendDashboardFixtures`:
152
+ Or extend the base `test` in one line with `extendPiwiFixtures`:
140
153
 
141
154
  ```typescript
142
155
  import { test as base } from '@playwright/test'
143
- import { extendDashboardFixtures } from '@piwitests/reporter'
156
+ import { extendPiwiFixtures } from '@piwitests/reporter'
144
157
 
145
- export const test = extendDashboardFixtures(base)
158
+ export const test = extendPiwiFixtures(base)
146
159
  export { expect } from '@playwright/test'
147
160
  ```
148
161
 
162
+ Then import `test` from your fixtures file in every spec — a spec that imports `test` from `@playwright/test` directly still runs and reports fine, it just isn't captured.
163
+
149
164
  ### What gets captured
150
165
 
151
- - **Network requests** — method, URL, status, duration, resource type. Aggregated on the dashboard into a *Slow API Endpoints* table grouped by `METHOD + normalized route`.
166
+ - **Network requests** — method, URL, status, duration, resource type (API/document traffic only). Aggregated on the dashboard into a *Slow API Endpoints* table grouped by `METHOD + normalized route`.
167
+ - **Console entries** — `warning`, `error`, and `assert` messages with their source location.
152
168
  - **Browser Web Vitals** — TTFB, DOM Interactive, DOMContentLoaded, Load Complete, First Paint, First Contentful Paint — displayed with color-coded thresholds.
169
+ - **ARIA snapshot** — captured automatically when a test fails, shown as failure evidence and fed to the AI diagnosis.
170
+ - **Locator snapshots** — for each acted-on element, its attributes plus ranked alternative locators, stamped with the call site. These power locator healing; when a failing locator matches nothing, a fresh suggestion is attached as a Playwright annotation.
171
+
172
+ Capture works for the `page` fixture, `browser.newPage()`, `browser.newContext().newPage()`, and popups. Everything is only collected when `collectPerformanceMetrics` is `true` (the default); locator snapshots can be disabled separately with `captureLocators: false`.
153
173
 
154
- Both are only collected when `collectPerformanceMetrics` is `true` (the default).
174
+ Without the fixtures you still get full run history, statuses, errors, traces, reports, streaming, and clustering — the fixtures add the slow-endpoint, Web Vitals, console, ARIA, and locator-healing layers. See the [capture fixtures guide](https://piwitests.github.io/capture-fixtures) for the full feature matrix and composition patterns.
155
175
 
156
176
  ## Authentication
157
177
 
@@ -195,7 +215,7 @@ When `collectCiInfo` is enabled (default), the reporter auto-detects:
195
215
  2. As tests complete, results are streamed in batches to the server
196
216
  3. After all tests finish, HTML reports are compressed and uploaded
197
217
  4. Trace files from test attachments are uploaded
198
- 5. Network request and web vitals data (from fixtures) are included per test case
218
+ 5. Data from the capture fixtures (network requests, console entries, web vitals, ARIA snapshots, locator snapshots) is included per test case
199
219
  6. The server stores everything and makes it available in the dashboard UI
200
220
 
201
221
  ## Requirements
@@ -229,10 +249,10 @@ Everything public — the reporter, config helpers, and the capture fixtures —
229
249
  - Ensure traces are enabled: `use: { trace: 'retain-on-failure' }`
230
250
  - Check the dashboard server is running and accessible at `serverUrl`
231
251
 
232
- ### Network/Web Vitals not appearing
252
+ ### Fixture data not appearing (network, Web Vitals, console, ARIA, locator healing)
233
253
 
234
- - Extend your `test` with `dashboardFixtures` / `extendDashboardFixtures` from `@piwitests/reporter`
235
- - Verify `collectPerformanceMetrics` is not set to `false`
254
+ - Extend your `test` with `piwiFixtures` / `extendPiwiFixtures` from `@piwitests/reporter`, and import `test` from your fixtures file in every spec — not from `@playwright/test` directly
255
+ - Verify `collectPerformanceMetrics` is not set to `false` (and `captureLocators` for locator healing)
236
256
  - Ensure tests navigate to at least one page (`await page.goto(...)`)
237
257
 
238
258
  ### Connection errors
package/dist/index.d.ts CHANGED
@@ -11,5 +11,6 @@ export default PiwiDashboardReporter;
11
11
  export { PiwiDashboardReporter };
12
12
  export { wrapConfig } from './public/config-wrapper.js';
13
13
  export { createGlobalSetup } from './public/global-setup.js';
14
- export { dashboardFixtures, extendDashboardFixtures } from './internal/capture/capture-fixtures.js';
14
+ export { piwiFixtures, extendPiwiFixtures } from './internal/capture/capture-fixtures.js';
15
+ export type { PiwiFixtures } from './internal/capture/capture-fixtures.js';
15
16
  export type { PiwiDashboardOptions, PlaywrightTestConfig } from './public/options.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extendDashboardFixtures = exports.dashboardFixtures = exports.createGlobalSetup = exports.wrapConfig = exports.PiwiDashboardReporter = void 0;
3
+ exports.extendPiwiFixtures = exports.piwiFixtures = exports.createGlobalSetup = exports.wrapConfig = exports.PiwiDashboardReporter = void 0;
4
4
  /**
5
5
  * Public API of `@piwitests/reporter`.
6
6
  *
@@ -20,5 +20,5 @@ var global_setup_js_1 = require("./public/global-setup.js");
20
20
  Object.defineProperty(exports, "createGlobalSetup", { enumerable: true, get: function () { return global_setup_js_1.createGlobalSetup; } });
21
21
  // ── Capture fixtures ─────────────────────────────────────────────────────────
22
22
  var capture_fixtures_js_1 = require("./internal/capture/capture-fixtures.js");
23
- Object.defineProperty(exports, "dashboardFixtures", { enumerable: true, get: function () { return capture_fixtures_js_1.dashboardFixtures; } });
24
- Object.defineProperty(exports, "extendDashboardFixtures", { enumerable: true, get: function () { return capture_fixtures_js_1.extendDashboardFixtures; } });
23
+ Object.defineProperty(exports, "piwiFixtures", { enumerable: true, get: function () { return capture_fixtures_js_1.piwiFixtures; } });
24
+ Object.defineProperty(exports, "extendPiwiFixtures", { enumerable: true, get: function () { return capture_fixtures_js_1.extendPiwiFixtures; } });
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Names of the `testInfo` attachments the dashboard fixtures produce and the
3
3
  * reporter parses. Single source of truth — imported by the producer
4
- * (`fixtures.ts`), the consumers (`reporter.ts` / `file-handler.ts`), and the
5
- * dogfooding `application/tests/fixtures.ts`, so producer and consumer can
4
+ * (`capture-fixtures.ts`), the consumers (`reporter.ts` / `file-handler.ts`), and
5
+ * the dogfooding `application/tests/fixtures.ts`, so producer and consumer can
6
6
  * never drift on a name.
7
7
  */
8
8
  export declare const ATTACHMENT_NAMES: {
@@ -4,8 +4,8 @@ exports.LOCATOR_SUGGESTION_ANNOTATION = exports.INTERNAL_ATTACHMENT_NAMES = expo
4
4
  /**
5
5
  * Names of the `testInfo` attachments the dashboard fixtures produce and the
6
6
  * reporter parses. Single source of truth — imported by the producer
7
- * (`fixtures.ts`), the consumers (`reporter.ts` / `file-handler.ts`), and the
8
- * dogfooding `application/tests/fixtures.ts`, so producer and consumer can
7
+ * (`capture-fixtures.ts`), the consumers (`reporter.ts` / `file-handler.ts`), and
8
+ * the dogfooding `application/tests/fixtures.ts`, so producer and consumer can
9
9
  * never drift on a name.
10
10
  */
11
11
  exports.ATTACHMENT_NAMES = {
@@ -1,4 +1,7 @@
1
- import type { Fixtures, Locator } from '@playwright/test';
1
+ import type { Fixtures, Locator, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestType } from '@playwright/test';
2
+ type FixtureArgs = {
3
+ [key: string]: any;
4
+ };
2
5
  /** Shape returned by the in-page element probe (see `wrapLocator`). */
3
6
  interface CapturedAttrs {
4
7
  tagName: string;
@@ -41,6 +44,17 @@ export declare function ariaSnapshotBestEffort(target: Locator, timeout?: number
41
44
  * Exported for unit testing; still passed directly to `evaluate()` below.
42
45
  */
43
46
  export declare function probeElementAttrs(el: any, keep: string[]): CapturedAttrs;
47
+ /**
48
+ * The fixtures `piwiFixtures` / `extendPiwiFixtures` contribute. The single
49
+ * added fixture is `piwiCapture`: an auto, test-scoped teardown hook that
50
+ * attaches the collected `piwi-*` data. Its name is **reserved** — a user
51
+ * fixture of the same name replaces the capture teardown and silently disables
52
+ * all capture. Exported so `piwiFixtures` and the extended `test` carry it in
53
+ * their types (and a collision surfaces to the type checker).
54
+ */
55
+ export interface PiwiFixtures {
56
+ piwiCapture: void;
57
+ }
44
58
  /**
45
59
  * Playwright fixtures that collect network requests, console entries,
46
60
  * web vitals, ARIA snapshots, and locator interaction data during a test.
@@ -50,9 +64,10 @@ export declare function probeElementAttrs(el: any, keep: string[]): CapturedAttr
50
64
  * `browser.newContext()`. Collected data is attached as `piwi-*`
51
65
  * test-info attachments which the Piwi Dashboard reporter parses on `onTestEnd`.
52
66
  */
53
- export declare const dashboardFixtures: Fixtures;
67
+ export declare const piwiFixtures: Fixtures<PiwiFixtures, {}, PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
54
68
  /**
55
- * Extend a Playwright `test` object with Piwi Dashboard fixtures.
69
+ * Extend a Playwright `test` object with the Piwi capture fixtures. The
70
+ * returned `test` carries the existing fixtures plus {@link PiwiFixtures}.
56
71
  *
57
72
  * Use this instead of importing `@playwright/test` directly from this package
58
73
  * to avoid the "Requiring @playwright/test second time" error caused by
@@ -61,10 +76,10 @@ export declare const dashboardFixtures: Fixtures;
61
76
  * @example
62
77
  * ```ts
63
78
  * import { test as base } from '@playwright/test';
64
- * import { extendDashboardFixtures } from '@piwitests/reporter';
79
+ * import { extendPiwiFixtures } from '@piwitests/reporter';
65
80
  *
66
- * export const test = extendDashboardFixtures(base);
81
+ * export const test = extendPiwiFixtures(base);
67
82
  * ```
68
83
  */
69
- export declare function extendDashboardFixtures<T>(test: T): T;
84
+ export declare function extendPiwiFixtures<TestArgs extends FixtureArgs, WorkerArgs extends FixtureArgs>(test: TestType<TestArgs, WorkerArgs>): TestType<TestArgs & PiwiFixtures, WorkerArgs>;
70
85
  export {};
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.dashboardFixtures = void 0;
3
+ exports.piwiFixtures = void 0;
4
4
  exports.ariaSnapshotBestEffort = ariaSnapshotBestEffort;
5
5
  exports.probeElementAttrs = probeElementAttrs;
6
- exports.extendDashboardFixtures = extendDashboardFixtures;
6
+ exports.extendPiwiFixtures = extendPiwiFixtures;
7
7
  const node_zlib_1 = require("node:zlib");
8
8
  const locator_healing_js_1 = require("./locator-healing.js");
9
9
  const attachments_js_1 = require("./attachments.js");
@@ -16,6 +16,9 @@ function createSink() {
16
16
  capturePromises: [],
17
17
  failedLocators: [],
18
18
  lastActivePage: null,
19
+ testInfo: null,
20
+ stashedWebVitals: null,
21
+ stashedAria: null,
19
22
  };
20
23
  }
21
24
  /**
@@ -25,6 +28,100 @@ function createSink() {
25
28
  * test (auth setup, teardown) is intentionally not captured.
26
29
  */
27
30
  let currentSink = null;
31
+ /**
32
+ * Element probes whose protocol call is still in flight. Closing a page,
33
+ * context, or browser while a probe is mid-flight makes Playwright's
34
+ * connection dispatcher throw a global "Object with guid handle@… was not
35
+ * bound in the connection" error, which fails whichever test happens to be
36
+ * running. The close wrappers drain this set (bounded) before closing.
37
+ */
38
+ const PENDING_PROBES = new Set();
39
+ async function drainPendingProbes(capMs) {
40
+ if (PENDING_PROBES.size === 0)
41
+ return;
42
+ let cap;
43
+ await Promise.race([
44
+ Promise.allSettled(PENDING_PROBES),
45
+ new Promise((resolve) => {
46
+ cap = setTimeout(resolve, capMs);
47
+ }),
48
+ ]);
49
+ clearTimeout(cap);
50
+ }
51
+ function isPageClosed(page) {
52
+ try {
53
+ return typeof page.isClosed === 'function' ? page.isClosed() : false;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ function pageContext(page) {
60
+ try {
61
+ return typeof page.context === 'function' ? page.context() : null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ /** Read navigation/paint timings from a page — null when unavailable or the page is gone. */
68
+ async function readWebVitals(page) {
69
+ try {
70
+ // Runs in the browser, so the perf-entry reads stay `any` (no DOM lib);
71
+ // the callback return type pins the result to WebVitals.
72
+ return await page.evaluate(() => {
73
+ const navEntries = performance.getEntriesByType('navigation');
74
+ const paintEntries = performance.getEntriesByType('paint');
75
+ const nav = navEntries[0];
76
+ const navigation = nav
77
+ ? {
78
+ url: nav.name,
79
+ ttfb: Math.round(nav.responseStart - nav.fetchStart),
80
+ domInteractive: Math.round(nav.domInteractive - nav.fetchStart),
81
+ domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.fetchStart),
82
+ loadComplete: Math.round(nav.loadEventEnd - nav.fetchStart),
83
+ transferSize: nav.transferSize || 0,
84
+ encodedBodySize: nav.encodedBodySize || 0,
85
+ decodedBodySize: nav.decodedBodySize || 0,
86
+ }
87
+ : null;
88
+ const paint = {};
89
+ for (const entry of paintEntries) {
90
+ const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
91
+ paint[key] = Math.round(entry.startTime);
92
+ }
93
+ if (!navigation && Object.keys(paint).length === 0)
94
+ return null;
95
+ return { navigation, paint };
96
+ });
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ /**
103
+ * Take the page-dependent teardown reads (web vitals; ARIA snapshot when the
104
+ * test failed) while the last active page is still open. Called by the close
105
+ * wrappers just before a close that would take that page with it — flushSink
106
+ * runs too late for a live read on the standard test page.
107
+ */
108
+ async function stashPageState(sink, closing) {
109
+ const page = sink.lastActivePage;
110
+ if (!page || isPageClosed(page))
111
+ return;
112
+ const belongsToClosing = closing.page === page || (closing.context !== undefined && pageContext(page) === closing.context);
113
+ if (!belongsToClosing)
114
+ return;
115
+ const vitals = await readWebVitals(page);
116
+ if (vitals)
117
+ sink.stashedWebVitals = vitals;
118
+ const status = sink.testInfo?.status;
119
+ if (status === 'failed' || status === 'timedOut' || status === 'interrupted') {
120
+ const aria = await ariaSnapshotBestEffort(page.locator(':root'), 1000);
121
+ if (aria)
122
+ sink.stashedAria = aria;
123
+ }
124
+ }
28
125
  // Idempotency guards: a page/context/browser can be reached through several
29
126
  // paths (browser patch, context patch, the `page` fixture, popup events), and
30
127
  // must be wrapped exactly once.
@@ -204,14 +301,23 @@ function wrapLocator(page, locator, originMethod, originArgs) {
204
301
  sink.failedLocators.push({ method: originMethod, args: originArgs });
205
302
  throw error;
206
303
  }
207
- // Fire-and-forget: capture element data without blocking the test.
208
- // evaluate() can hang when page navigates (element detaches), so
209
- // race it against a 500ms deadline and never throw.
304
+ // Fire-and-forget: capture element data without blocking the test. The
305
+ // snapshot wait below is bounded by a 500ms deadline (evaluate can hang
306
+ // when the page navigates), but the probe's underlying protocol call is
307
+ // tracked in PENDING_PROBES so the close wrappers can drain it — and in
308
+ // capturePromises so flushSink outwaits it — even when the deadline
309
+ // abandons it. An evaluate still in flight when its page closes crashes
310
+ // the connection dispatcher with a global "not bound" error.
311
+ const probe = target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
312
+ const settledProbe = probe.then(() => undefined, () => undefined);
313
+ PENDING_PROBES.add(settledProbe);
314
+ settledProbe.then(() => PENDING_PROBES.delete(settledProbe));
315
+ sink.capturePromises.push(settledProbe);
210
316
  const resolveAttrs = (async () => {
211
317
  let deadline;
212
318
  try {
213
319
  const attrs = await Promise.race([
214
- target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG),
320
+ probe,
215
321
  new Promise((_, reject) => {
216
322
  deadline = setTimeout(() => reject(new Error('locator capture timeout')), 500);
217
323
  }),
@@ -265,6 +371,24 @@ function instrumentPage(page) {
265
371
  if (!page || INSTRUMENTED_PAGES.has(page))
266
372
  return;
267
373
  INSTRUMENTED_PAGES.add(page);
374
+ // A page reached through the `page` fixture safety net may live in a context
375
+ // the browser patch never saw — instrument it so its close is wrapped too.
376
+ const ctx = pageContext(page);
377
+ if (ctx)
378
+ instrumentContext(ctx);
379
+ // Drain in-flight probes before a user-initiated close (the guard tolerates
380
+ // page-like test fakes without a close method), and preserve the
381
+ // page-dependent teardown reads while the page can still serve them.
382
+ if (typeof page.close === 'function') {
383
+ const originalClose = page.close.bind(page);
384
+ page.close = async (...args) => {
385
+ await drainPendingProbes(1000);
386
+ const sink = currentSink;
387
+ if (sink)
388
+ await stashPageState(sink, { page });
389
+ return originalClose(...args);
390
+ };
391
+ }
268
392
  // Opt-out: skipped when PIWI_CAPTURE_LOCATORS=false (set automatically when
269
393
  // the reporter's collectPerformanceMetrics / captureLocators is disabled),
270
394
  // so the per-action DOM read + ARIA snapshot cost is never paid when unused.
@@ -363,6 +487,21 @@ function instrumentContext(context) {
363
487
  instrumentPage(page);
364
488
  return page;
365
489
  };
490
+ // The built-in context fixture closes here at test teardown — BEFORE the
491
+ // auto capture fixture flushes. Drain in-flight probes (an evaluate crossing
492
+ // the close crashes the connection with a global "not bound" error) and take
493
+ // the page-dependent reads (web vitals, failure ARIA snapshot) while the
494
+ // test's page is still open.
495
+ if (typeof context.close === 'function') {
496
+ const originalClose = context.close.bind(context);
497
+ context.close = async (...args) => {
498
+ await drainPendingProbes(1000);
499
+ const sink = currentSink;
500
+ if (sink)
501
+ await stashPageState(sink, { context });
502
+ return originalClose(...args);
503
+ };
504
+ }
366
505
  // Popups and pages the context opens on its own (idempotent with the above).
367
506
  context.on('page', (page) => instrumentPage(page));
368
507
  }
@@ -389,6 +528,15 @@ function patchBrowser(browser) {
389
528
  instrumentContext(context);
390
529
  return context;
391
530
  };
531
+ // Worker shutdown closes the browser; a probe still in flight would crash
532
+ // the connection dispatcher.
533
+ if (typeof browser.close === 'function') {
534
+ const originalClose = browser.close.bind(browser);
535
+ browser.close = async (...args) => {
536
+ await drainPendingProbes(1000);
537
+ return originalClose(...args);
538
+ };
539
+ }
392
540
  }
393
541
  /**
394
542
  * Drain in-flight capture work and attach the collected `piwi-*` data
@@ -422,9 +570,13 @@ async function flushSink(sink, testInfo) {
422
570
  });
423
571
  }
424
572
  const page = sink.lastActivePage;
425
- if (page && testInfo.status !== 'passed' && testInfo.status !== 'skipped') {
573
+ const pageReadable = page !== null && !isPageClosed(page);
574
+ if (testInfo.status !== 'passed' && testInfo.status !== 'skipped') {
426
575
  try {
427
- const snapshot = await ariaSnapshotBestEffort(page.locator(':root'));
576
+ // Prefer a live read; fall back to the snapshot the close wrappers
577
+ // stashed — the standard test page is already closed when this auto
578
+ // fixture tears down.
579
+ const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(':root')) : null) ?? sink.stashedAria;
428
580
  if (snapshot) {
429
581
  await testInfo.attach(attachments_js_1.ATTACHMENT_NAMES.ariaSnapshot, {
430
582
  contentType: 'text/plain',
@@ -465,45 +617,14 @@ async function flushSink(sink, testInfo) {
465
617
  body: Buffer.from(JSON.stringify(sink.networkRequests)),
466
618
  });
467
619
  }
468
- if (page) {
469
- try {
470
- // Runs in the browser, so the perf-entry reads stay `any` (no DOM lib);
471
- // the callback return type pins `webVitals` to WebVitals.
472
- const webVitals = await page.evaluate(() => {
473
- const navEntries = performance.getEntriesByType('navigation');
474
- const paintEntries = performance.getEntriesByType('paint');
475
- const nav = navEntries[0];
476
- const navigation = nav
477
- ? {
478
- url: nav.name,
479
- ttfb: Math.round(nav.responseStart - nav.fetchStart),
480
- domInteractive: Math.round(nav.domInteractive - nav.fetchStart),
481
- domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.fetchStart),
482
- loadComplete: Math.round(nav.loadEventEnd - nav.fetchStart),
483
- transferSize: nav.transferSize || 0,
484
- encodedBodySize: nav.encodedBodySize || 0,
485
- decodedBodySize: nav.decodedBodySize || 0,
486
- }
487
- : null;
488
- const paint = {};
489
- for (const entry of paintEntries) {
490
- const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
491
- paint[key] = Math.round(entry.startTime);
492
- }
493
- if (!navigation && Object.keys(paint).length === 0)
494
- return null;
495
- return { navigation, paint };
496
- });
497
- if (webVitals) {
498
- await testInfo.attach(attachments_js_1.ATTACHMENT_NAMES.webVitals, {
499
- contentType: 'application/json',
500
- body: Buffer.from(JSON.stringify(webVitals)),
501
- });
502
- }
503
- }
504
- catch {
505
- /* ignore */
506
- }
620
+ // Live read when the page still exists (e.g. a browser.newPage the test left
621
+ // open); otherwise the vitals the close wrappers stashed before the page went.
622
+ const webVitals = (pageReadable ? await readWebVitals(page) : null) ?? sink.stashedWebVitals;
623
+ if (webVitals) {
624
+ await testInfo.attach(attachments_js_1.ATTACHMENT_NAMES.webVitals, {
625
+ contentType: 'application/json',
626
+ body: Buffer.from(JSON.stringify(webVitals)),
627
+ });
507
628
  }
508
629
  }
509
630
  /**
@@ -515,7 +636,7 @@ async function flushSink(sink, testInfo) {
515
636
  * `browser.newContext()`. Collected data is attached as `piwi-*`
516
637
  * test-info attachments which the Piwi Dashboard reporter parses on `onTestEnd`.
517
638
  */
518
- exports.dashboardFixtures = {
639
+ exports.piwiFixtures = {
519
640
  // Worker-scoped: patch the shared browser so every page/context created from
520
641
  // it — including by user fixtures that take `browser` directly — is captured.
521
642
  browser: [
@@ -535,9 +656,10 @@ exports.dashboardFixtures = {
535
656
  // Auto, test-scoped: open a capture sink for the running test and flush it
536
657
  // (attach the collected data) at teardown. Runs for every test without being
537
658
  // requested, so suites that never destructure `page` are still captured.
538
- piwiDashboardCapture: [
659
+ piwiCapture: [
539
660
  async ({}, use, testInfo) => {
540
661
  const sink = createSink();
662
+ sink.testInfo = testInfo;
541
663
  currentSink = sink;
542
664
  try {
543
665
  await use();
@@ -551,7 +673,8 @@ exports.dashboardFixtures = {
551
673
  ],
552
674
  };
553
675
  /**
554
- * Extend a Playwright `test` object with Piwi Dashboard fixtures.
676
+ * Extend a Playwright `test` object with the Piwi capture fixtures. The
677
+ * returned `test` carries the existing fixtures plus {@link PiwiFixtures}.
555
678
  *
556
679
  * Use this instead of importing `@playwright/test` directly from this package
557
680
  * to avoid the "Requiring @playwright/test second time" error caused by
@@ -560,11 +683,11 @@ exports.dashboardFixtures = {
560
683
  * @example
561
684
  * ```ts
562
685
  * import { test as base } from '@playwright/test';
563
- * import { extendDashboardFixtures } from '@piwitests/reporter';
686
+ * import { extendPiwiFixtures } from '@piwitests/reporter';
564
687
  *
565
- * export const test = extendDashboardFixtures(base);
688
+ * export const test = extendPiwiFixtures(base);
566
689
  * ```
567
690
  */
568
- function extendDashboardFixtures(test) {
569
- return test.extend(exports.dashboardFixtures);
691
+ function extendPiwiFixtures(test) {
692
+ return test.extend(exports.piwiFixtures);
570
693
  }
@@ -72,8 +72,9 @@ export interface LocatorSnapshot {
72
72
  export declare function dedupeSnapshotsByLocation(snaps: LocatorSnapshot[]): LocatorSnapshot[];
73
73
  /**
74
74
  * Page-level locator-building methods wrapped by the capture proxy. Imported by
75
- * both `reporter/src/fixtures.ts` and the dogfooding `application/tests/fixtures.ts`
76
- * so the two stay in sync (a prior drift missed `scrollIntoViewIfNeeded`).
75
+ * both the capture fixtures (`capture-fixtures.ts`) and the dogfooding
76
+ * `application/tests/fixtures.ts` so the two stay in sync (a prior drift missed
77
+ * `scrollIntoViewIfNeeded`).
77
78
  */
78
79
  export declare const LOCATOR_METHODS: string[];
79
80
  /**
@@ -73,8 +73,9 @@ function dedupeSnapshotsByLocation(snaps) {
73
73
  // ── Playwright method surface (shared with the fixture proxy) ────────────────
74
74
  /**
75
75
  * Page-level locator-building methods wrapped by the capture proxy. Imported by
76
- * both `reporter/src/fixtures.ts` and the dogfooding `application/tests/fixtures.ts`
77
- * so the two stay in sync (a prior drift missed `scrollIntoViewIfNeeded`).
76
+ * both the capture fixtures (`capture-fixtures.ts`) and the dogfooding
77
+ * `application/tests/fixtures.ts` so the two stay in sync (a prior drift missed
78
+ * `scrollIntoViewIfNeeded`).
78
79
  */
79
80
  exports.LOCATOR_METHODS = [
80
81
  'getByRole',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piwitests/reporter",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Playwright reporter for sending test results to Piwi Dashboard",
5
5
  "url": "https://github.com/PiwiTests/platform",
6
6
  "homepage": "https://piwitests.github.io",
@@ -19,7 +19,8 @@
19
19
  "types": "./dist/index.d.ts",
20
20
  "import": "./dist/index.js",
21
21
  "require": "./dist/index.js"
22
- }
22
+ },
23
+ "./package.json": "./package.json"
23
24
  },
24
25
  "keywords": [
25
26
  "playwright",