@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.
- package/README.md +22 -286
- package/dist/cli/index.js +95 -22
- package/dist/global-setup-module.js +96 -7
- package/dist/index.d.ts +57 -2
- package/dist/index.js +1700 -171
- 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
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
214
|
-
*
|
|
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;
|