@piwitests/reporter 0.26.1 → 0.28.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.
@@ -35,7 +35,7 @@ __export(global_setup_module_exports, {
35
35
  module.exports = __toCommonJS(global_setup_module_exports);
36
36
 
37
37
  // src/public/global-setup.ts
38
- var path3 = __toESM(require("path"));
38
+ var path4 = __toESM(require("path"));
39
39
 
40
40
  // src/internal/support/errors.ts
41
41
  function errorMessage(error) {
@@ -44,7 +44,7 @@ function errorMessage(error) {
44
44
  }
45
45
 
46
46
  // src/public/global-setup.ts
47
- var fs3 = __toESM(require("fs"));
47
+ var fs4 = __toESM(require("fs"));
48
48
 
49
49
  // src/internal/config/desktop.ts
50
50
  var fs = __toESM(require("fs"));
@@ -78,9 +78,12 @@ var DEFAULTS = {
78
78
  captureLocators: true,
79
79
  capturePageState: true,
80
80
  captureServerTraces: true,
81
+ sampleAriaOnPass: true,
82
+ defaultCapture: true,
81
83
  streaming: true,
82
84
  streamingBatchSize: 5,
83
85
  streamingBatchDelay: 2e3,
86
+ maxStreamBufferBytes: 100 * 1024 * 1024,
84
87
  failOnFlakyTests: false,
85
88
  username: null,
86
89
  password: null,
@@ -100,6 +103,7 @@ var PIWI_ENV_KEYS = {
100
103
  streaming: "PIWI_STREAMING",
101
104
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
102
105
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
106
+ maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
103
107
  liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
104
108
  failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
105
109
  uploadTraces: "PIWI_UPLOAD_TRACES",
@@ -107,6 +111,8 @@ var PIWI_ENV_KEYS = {
107
111
  captureLocators: "PIWI_CAPTURE_LOCATORS",
108
112
  capturePageState: "PIWI_CAPTURE_PAGE_STATE",
109
113
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
114
+ sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
115
+ defaultCapture: "PIWI_DEFAULT_CAPTURE",
110
116
  inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
111
117
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
112
118
  outputFile: "PIWI_OUTPUT_FILE",
@@ -136,6 +142,7 @@ var ENV_FALLBACK_SPECS = [
136
142
  { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
137
143
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
138
144
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
145
+ { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
139
146
  { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
140
147
  { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
141
148
  { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
@@ -143,6 +150,8 @@ var ENV_FALLBACK_SPECS = [
143
150
  { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
144
151
  { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
145
152
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
153
+ { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
154
+ { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
146
155
  { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
147
156
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
148
157
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
@@ -298,6 +307,29 @@ var HttpClient = class {
298
307
  this.logger.debug("Logged in successfully");
299
308
  return cookie;
300
309
  }
310
+ /**
311
+ * Send a JSON GET request, returning the parsed body, or `null` on any non-2xx
312
+ * status or parse failure. Unlike `postJSON` this never throws — its callers
313
+ * treat a missing or unreachable endpoint as "feature unavailable".
314
+ */
315
+ async getJSON(pathname, auth) {
316
+ let res;
317
+ try {
318
+ res = await this.request("GET", pathname, { auth });
319
+ } catch (error) {
320
+ this.logger.debug(`GET ${pathname} failed: ${error.message}`);
321
+ return null;
322
+ }
323
+ if (res.status < 200 || res.status >= 300) {
324
+ this.logger.debug(`GET ${pathname} returned ${res.status}`);
325
+ return null;
326
+ }
327
+ try {
328
+ return JSON.parse(res.text);
329
+ } catch {
330
+ return null;
331
+ }
332
+ }
301
333
  /** Send a JSON POST request. `auth` can be an API key (prefix `pd_`) or a session cookie string. */
302
334
  async postJSON(pathname, payload, auth) {
303
335
  const body = JSON.stringify(payload);
@@ -348,7 +380,7 @@ var HttpClient = class {
348
380
  {
349
381
  hostname: url.hostname,
350
382
  port: url.port || (url.protocol === "https:" ? 443 : 80),
351
- path: url.pathname,
383
+ path: url.pathname + url.search,
352
384
  method,
353
385
  headers
354
386
  },
@@ -360,6 +392,12 @@ var HttpClient = class {
360
392
  res.on("end", () => {
361
393
  resolve2({ status: res.statusCode ?? 0, text: data, headers: res.headers });
362
394
  });
395
+ res.on("error", reject);
396
+ res.on("close", () => {
397
+ if (!res.complete) {
398
+ reject(new Error(`Connection to ${pathname} closed before the response completed`));
399
+ }
400
+ });
363
401
  }
364
402
  );
365
403
  req.on("error", reject);
@@ -423,6 +461,29 @@ function getSetupFilePath(projectName) {
423
461
  return path2.join(os3.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
424
462
  }
425
463
 
464
+ // src/internal/support/aria-sampling.ts
465
+ var path3 = __toESM(require("path"));
466
+ var os4 = __toESM(require("os"));
467
+ var fs3 = __toESM(require("fs"));
468
+ function ariaSampleIdentity(filePath, title) {
469
+ return `${filePath}\0${title}`;
470
+ }
471
+ function getAriaSampleFilePath(projectName) {
472
+ return path3.join(os4.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
473
+ }
474
+ function writeAriaSampleFile(projectName, identities) {
475
+ try {
476
+ fs3.writeFileSync(getAriaSampleFilePath(projectName), JSON.stringify({ projectName, identities }));
477
+ } catch {
478
+ }
479
+ }
480
+ function clearAriaSampleFile(projectName) {
481
+ try {
482
+ fs3.rmSync(getAriaSampleFilePath(projectName), { force: true });
483
+ } catch {
484
+ }
485
+ }
486
+
426
487
  // src/internal/support/run-mode.ts
427
488
  var PW_UI_FLAGS = ["--ui", "--ui-host", "--ui-port"];
428
489
  function isUiMode(argv = process.argv) {
@@ -431,18 +492,25 @@ function isUiMode(argv = process.argv) {
431
492
  const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
432
493
  return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
433
494
  }
495
+ var PW_LIST_FLAGS = ["--list"];
496
+ function isListMode(argv = process.argv) {
497
+ const args = argv.slice(2);
498
+ const testIdx = args.indexOf("test");
499
+ const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
500
+ return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
501
+ }
434
502
 
435
503
  // src/public/global-setup.ts
436
504
  function createGlobalSetup(options, userSetup) {
437
505
  return async function globalSetupFn(config) {
438
- const piwiReporterPath = path3.resolve(__dirname, "./index.js");
506
+ const piwiReporterPath = path4.resolve(__dirname, "./index.js");
439
507
  let inlineReporterOptions = {};
440
508
  if (Array.isArray(config?.reporter)) {
441
509
  for (const r of config.reporter) {
442
510
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
443
511
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
444
512
  try {
445
- return path3.resolve(require.resolve(r[0])) === piwiReporterPath;
513
+ return path4.resolve(require.resolve(r[0])) === piwiReporterPath;
446
514
  } catch {
447
515
  return false;
448
516
  }
@@ -460,6 +528,11 @@ function createGlobalSetup(options, userSetup) {
460
528
  if (userSetup) return userSetup(config);
461
529
  return;
462
530
  }
531
+ if (isListMode()) {
532
+ logger.debug("List mode detected \u2014 skipping run registration.");
533
+ if (userSetup) return userSetup(config);
534
+ return;
535
+ }
463
536
  if (opts.enabled === false || !opts.serverUrl) {
464
537
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
465
538
  if (userSetup) return userSetup(config);
@@ -469,7 +542,7 @@ function createGlobalSetup(options, userSetup) {
469
542
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
470
543
  if (r[0].toLowerCase().includes("piwi")) return true;
471
544
  try {
472
- return path3.resolve(require.resolve(r[0])) === piwiReporterPath;
545
+ return path4.resolve(require.resolve(r[0])) === piwiReporterPath;
473
546
  } catch {
474
547
  return false;
475
548
  }
@@ -501,7 +574,7 @@ function createGlobalSetup(options, userSetup) {
501
574
  auth
502
575
  );
503
576
  if (response?.runId && response?.setupToken) {
504
- fs3.writeFileSync(
577
+ fs4.writeFileSync(
505
578
  getSetupFilePath(opts.projectName),
506
579
  JSON.stringify({
507
580
  runId: response.runId,
@@ -511,6 +584,22 @@ function createGlobalSetup(options, userSetup) {
511
584
  );
512
585
  logger.debug(`Global setup: initializing run #${response.runId}`);
513
586
  }
587
+ if (opts.projectName) clearAriaSampleFile(opts.projectName);
588
+ if (opts.sampleAriaOnPass !== false && opts.projectName) {
589
+ const menu = await httpClient.getJSON("/api/projects/menu", auth);
590
+ const projectId = menu?.items?.find(
591
+ (p) => p.name.toLowerCase() === opts.projectName.toLowerCase()
592
+ )?.id;
593
+ if (projectId != null) {
594
+ const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
595
+ const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
596
+ if (tests) {
597
+ const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
598
+ writeAriaSampleFile(opts.projectName, identities);
599
+ logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
600
+ }
601
+ }
602
+ }
514
603
  } catch (error) {
515
604
  logger.warn(`Could not register global setup: ${errorMessage(error)}`);
516
605
  }
package/dist/index.d.ts CHANGED
@@ -58,6 +58,33 @@ interface PiwiDashboardOptions {
58
58
  * `false`. Can also be forced off with `PIWI_CAPTURE_SERVER_TRACES=false`.
59
59
  */
60
60
  captureServerTraces?: boolean;
61
+ /**
62
+ * Sample the ARIA snapshot at the end of a *passing* test, so a later failure
63
+ * can be diffed against the page as it last looked when green. Rate-limited by
64
+ * the server: at run start the reporter asks which tests are due a fresh
65
+ * sample (their newest green snapshot is older than a day, or missing) and
66
+ * captures only those, so steady-state runs pay nothing. Rides the existing
67
+ * capture fixtures — no snapshot is taken without them. Defaults to `true`.
68
+ * Set to `false` (or `PIWI_SAMPLE_ARIA_ON_PASS=false`) to never sample on pass.
69
+ */
70
+ sampleAriaOnPass?: boolean;
71
+ /**
72
+ * When installed via `wrapConfig`, default Playwright's own `screenshot` and
73
+ * `trace` options on the top-level `use` block so a failing test keeps a
74
+ * screenshot (`'only-on-failure'`) and a trace (`'retain-on-failure'`) even
75
+ * without the capture fixtures — the trace alone unlocks the DOM snapshot,
76
+ * full call stack, full network with bodies and the visual diff. On Playwright
77
+ * 1.63 or later the trace default also turns on the per-action aria tree
78
+ * (`snapshots: { dom: true, aria: true }`), which adds the accessibility tree
79
+ * before and after each action at negligible size. The `screen` snapshot kind
80
+ * (a PNG per action, the trace's biggest cost) stays opt-in — set it yourself
81
+ * with `use: { trace: { mode: 'retain-on-failure', snapshots: { dom: true,
82
+ * aria: true, screen: true } } }`. Only fills options the config leaves unset;
83
+ * an explicit value (including `'off'`) and per-project `use` blocks are never
84
+ * touched. Defaults to `true`. Set to `false` (or `PIWI_DEFAULT_CAPTURE=false`)
85
+ * to opt out and let Playwright's own defaults stand.
86
+ */
87
+ defaultCapture?: boolean;
61
88
  /**
62
89
  * Open Piwi's own failure-time overlay on the failing page — for inspecting
63
90
  * the page and picking a locator for any element (click an element → confirm
@@ -88,6 +115,18 @@ interface PiwiDashboardOptions {
88
115
  streamingBatchSize?: number;
89
116
  /** Max delay (ms) before flushing pending events during streaming. Defaults to `2000`. */
90
117
  streamingBatchDelay?: number;
118
+ /**
119
+ * Byte budget for the in-memory stream buffer (the queue of events waiting to
120
+ * reach the dashboard, and the crash-recovery file it writes if delivery
121
+ * fails). When the server is unreachable or a huge suite outruns delivery, the
122
+ * queue is capped here instead of growing without bound: the lowest-value
123
+ * events are shed first — live step progress, then per-test `begin` markers,
124
+ * and only as a last resort test results (the final counts and the end-of-run
125
+ * batch submit stay complete regardless). Defaults to `104857600` (100 MB).
126
+ * Set to `0` to disable the cap (unbounded, the pre-`0.28` behavior). Can also
127
+ * be set with `PIWI_MAX_STREAM_BUFFER_BYTES`.
128
+ */
129
+ maxStreamBufferBytes?: number;
91
130
  /**
92
131
  * Fail the run when any test was flaky (passed only after a retry). Forwarded
93
132
  * to Playwright's native `failOnFlakyTests` config option (Playwright 1.52+)
@@ -210,14 +249,23 @@ declare function createGlobalSetup(options?: PiwiDashboardOptions, userSetup?: (
210
249
  *
211
250
  * The `globalSetup` field is set to a `string` (or `string[]` if the user
212
251
  * already has a global setup) referencing the Piwi global setup module,
213
- * which registers the run on the server. The original setup path(s) are
214
- * preserved and executed first.
252
+ * which registers the run on the server. Piwi's module is chained first so the
253
+ * run registers before the user's own setup, appearing as "initializing" on the
254
+ * dashboard while that setup runs; the original setup path(s) are preserved and
255
+ * executed after it.
215
256
  *
216
257
  * Playwright options required in `globalSetup` are forwarded via `PIWI_*`
217
258
  * environment variables (see `applyOptionsToEnv` in `config.ts` for the
218
259
  * supported set — `serverUrl`, `projectName`, `verbose`, `apiKey`,
219
260
  * `username`, `password`, `environment`, `label`, `runLabel`).
220
261
  *
262
+ * The top-level `use` block's `screenshot` and `trace` are defaulted to
263
+ * `'only-on-failure'` / `'retain-on-failure'` when unset so failure evidence is
264
+ * captured without the fixtures; on Playwright 1.63 or later `trace` also turns
265
+ * on the per-action aria tree (`snapshots: { dom: true, aria: true }`). An
266
+ * explicit value (including `'off'`) is kept. Opt out with `defaultCapture:
267
+ * false` (or `PIWI_DEFAULT_CAPTURE=false`).
268
+ *
221
269
  * @param config The user's Playwright config.
222
270
  * @param piwiOptions Optional Piwi Dashboard options (serverUrl, projectName, …).
223
271
  */
@@ -254,6 +302,12 @@ declare class PiwiDashboardReporter {
254
302
  private shardInfo;
255
303
  private metadata;
256
304
  private enabled;
305
+ /**
306
+ * True under `playwright test --list`, where Playwright still constructs this
307
+ * reporter and fires `onBegin`/`onEnd` but runs no tests. Registering and
308
+ * finalizing a run would create an empty phantom run and upload an empty report.
309
+ */
310
+ private readonly listMode;
257
311
  /** True when the server URL and API key came from the desktop app, not from config. */
258
312
  private viaDesktopApp;
259
313
  private isFullRun;
@@ -267,6 +321,7 @@ declare class PiwiDashboardReporter {
267
321
  private streamManager;
268
322
  private recovery;
269
323
  private submitter;
324
+ private readonly failureLinks;
270
325
  private readonly logger;
271
326
  static wrapConfig: typeof wrapConfig;
272
327
  static createGlobalSetup: typeof createGlobalSetup;