@piwitests/reporter 0.57.0 → 0.58.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/dist/cli/index.js
CHANGED
|
@@ -873,6 +873,8 @@ var PIWI_ENV_KEYS = {
|
|
|
873
873
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
874
874
|
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
875
875
|
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
876
|
+
externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
|
|
877
|
+
externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
|
|
876
878
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
877
879
|
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
878
880
|
leakCheck: "PIWI_LEAK_CHECK",
|
|
@@ -4877,7 +4879,7 @@ Connection:
|
|
|
4877
4879
|
--server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
|
|
4878
4880
|
--api-key <key> API key (env PIWI_API_KEY)
|
|
4879
4881
|
|
|
4880
|
-
Policy (at least one is required):
|
|
4882
|
+
Policy (at least one rule or warn-only flag is required):
|
|
4881
4883
|
--require-tag <tags> Comma-separated; every test carrying the tag must pass
|
|
4882
4884
|
--max-failed <n> Fail when more than n tests failed
|
|
4883
4885
|
--max-new-regressions <n> Fail when more than n tests newly started failing
|
|
@@ -4890,7 +4892,7 @@ Policy (at least one is required):
|
|
|
4890
4892
|
--max-new-leaks <n> Fail when more than n of those were never seen on the base branch
|
|
4891
4893
|
|
|
4892
4894
|
Warn-only (reported, never fails the build):
|
|
4893
|
-
--max-uncovered-changes <n> Warn when more than n changed files
|
|
4895
|
+
--max-uncovered-changes <n> Warn when more than n changed files are uncovered changes: no test reached them
|
|
4894
4896
|
--min-patch-coverage <percent> Warn when fewer of the changed lines ran, in the commit's combined coverage
|
|
4895
4897
|
|
|
4896
4898
|
Other:
|
|
@@ -127,6 +127,8 @@ var PIWI_ENV_KEYS = {
|
|
|
127
127
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
128
128
|
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
129
129
|
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
130
|
+
externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
|
|
131
|
+
externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
|
|
130
132
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
131
133
|
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
132
134
|
leakCheck: "PIWI_LEAK_CHECK",
|
|
@@ -217,6 +219,13 @@ function resolveOptions(raw) {
|
|
|
217
219
|
coverage.browser = coverageBrowser;
|
|
218
220
|
if (coverage.server === void 0 && coverageServer) coverage.server = coverageServer;
|
|
219
221
|
if (coverage.browser !== void 0 || coverage.server !== void 0) mergedRaw.coverage = coverage;
|
|
222
|
+
const externalId = { ...mergedRaw.externalId };
|
|
223
|
+
const externalIdPattern = env[PIWI_ENV_KEYS.externalIdPattern];
|
|
224
|
+
const externalIdOnDuplicate = env[PIWI_ENV_KEYS.externalIdOnDuplicate];
|
|
225
|
+
if (externalId.pattern === void 0 && externalIdPattern) externalId.pattern = externalIdPattern;
|
|
226
|
+
if (externalId.onDuplicate === void 0 && externalIdOnDuplicate)
|
|
227
|
+
externalId.onDuplicate = externalIdOnDuplicate;
|
|
228
|
+
if (Object.keys(externalId).length > 0) mergedRaw.externalId = externalId;
|
|
220
229
|
const opts = { ...DEFAULTS, ...mergedRaw };
|
|
221
230
|
if (env[PIWI_ENV_KEYS.verbose] !== void 0) opts.verbose = env[PIWI_ENV_KEYS.verbose] === "true";
|
|
222
231
|
return opts;
|
package/dist/index.d.ts
CHANGED
|
@@ -129,6 +129,33 @@ interface PiwiDashboardOptions {
|
|
|
129
129
|
*/
|
|
130
130
|
server?: 'off' | 'files' | 'functions' | 'lines';
|
|
131
131
|
};
|
|
132
|
+
/**
|
|
133
|
+
* Match each test to its history on the dashboard by an external ID, the ID the test has in your own test
|
|
134
|
+
* management tool, so a test keeps its history when it is renamed or moved. A test declares its ID with a
|
|
135
|
+
* `piwi:id` annotation (`{ type: 'piwi:id', description: '12526' }`), which wins, or the ID is read from its title
|
|
136
|
+
* with `pattern`. Two tests declaring the same ID are reported before the first test runs, and matched by file,
|
|
137
|
+
* describe path and title instead.
|
|
138
|
+
*/
|
|
139
|
+
externalId?: {
|
|
140
|
+
/**
|
|
141
|
+
* Where the ID sits in a test's own title (not its describe path): `/^(\d+), /` reads `12526` from
|
|
142
|
+
* `'12526, Log in with Email'`. The ID is the first capture group, or the whole match without a group. A string
|
|
143
|
+
* is compiled with `new RegExp(value)`; one that does not compile turns external IDs off for the run, with an
|
|
144
|
+
* error, but never fails it. Can also be set with `PIWI_EXTERNAL_ID_PATTERN`.
|
|
145
|
+
*/
|
|
146
|
+
pattern?: RegExp | string;
|
|
147
|
+
/**
|
|
148
|
+
* Show the title without its ID on the dashboard, next to the ID. The whole match of `pattern` is removed from
|
|
149
|
+
* the title shown; the title stored and matched on stays whole. Defaults to `false`.
|
|
150
|
+
*/
|
|
151
|
+
stripFromTitle?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* What two tests declaring the same ID do: `'report'` lists them in the console and on the run page; `'fail'`
|
|
154
|
+
* also fails the run once its tests have run. Either way the run matches them by file, describe path and title.
|
|
155
|
+
* Defaults to `'report'`. Can also be set with `PIWI_EXTERNAL_ID_ON_DUPLICATE`.
|
|
156
|
+
*/
|
|
157
|
+
onDuplicate?: 'report' | 'fail';
|
|
158
|
+
};
|
|
132
159
|
/**
|
|
133
160
|
* Capture server-side spans for each API/document request the test makes,
|
|
134
161
|
* read from the `X-Piwi-Trace` response header emitted by a Piwi
|
|
@@ -457,6 +484,8 @@ declare class PiwiDashboardReporter {
|
|
|
457
484
|
private instanceId;
|
|
458
485
|
private shardInfo;
|
|
459
486
|
private metadata;
|
|
487
|
+
/** Each planned test's external ID and the IDs two tests share, settled in `onBegin`. */
|
|
488
|
+
private externalIds;
|
|
460
489
|
private enabled;
|
|
461
490
|
/**
|
|
462
491
|
* True under `playwright test --list`, where Playwright still constructs this
|
|
@@ -515,6 +544,13 @@ declare class PiwiDashboardReporter {
|
|
|
515
544
|
printsToStdio(): boolean;
|
|
516
545
|
/** Playwright reporter hook: called once at the start of the test run */
|
|
517
546
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
547
|
+
/**
|
|
548
|
+
* Settle each planned test's external ID before any test runs, name the IDs two tests share, and record them on
|
|
549
|
+
* the run so the dashboard lists them too.
|
|
550
|
+
*/
|
|
551
|
+
private planExternalIds;
|
|
552
|
+
/** The external ID fields of a test's payload: `null` tells the dashboard the test has none to match on. */
|
|
553
|
+
private externalIdFields;
|
|
518
554
|
/** Playwright reporter hook: called when an individual test begins */
|
|
519
555
|
onTestBegin(test: TestCase, result: TestResult): void;
|
|
520
556
|
/** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
|
|
@@ -554,8 +590,14 @@ declare class PiwiDashboardReporter {
|
|
|
554
590
|
* interrupted) is resolved once from the overall run status by the caller.
|
|
555
591
|
*/
|
|
556
592
|
private materializeUnrunTests;
|
|
557
|
-
/**
|
|
558
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Playwright reporter hook: called when the full test run finishes. Fails the run when two tests share an external
|
|
595
|
+
* ID and `externalId.onDuplicate` is `'fail'`.
|
|
596
|
+
*/
|
|
597
|
+
onEnd(result: FullResult): Promise<{
|
|
598
|
+
status?: FullResult['status'];
|
|
599
|
+
} | void>;
|
|
600
|
+
private finishRun;
|
|
559
601
|
}
|
|
560
602
|
|
|
561
603
|
interface ResolveSelectionResult {
|
package/dist/index.js
CHANGED
|
@@ -128,6 +128,8 @@ var PIWI_ENV_KEYS = {
|
|
|
128
128
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
129
129
|
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
130
130
|
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
131
|
+
externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
|
|
132
|
+
externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
|
|
131
133
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
132
134
|
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
133
135
|
leakCheck: "PIWI_LEAK_CHECK",
|
|
@@ -247,6 +249,13 @@ function resolveOptions(raw) {
|
|
|
247
249
|
coverage.browser = coverageBrowser;
|
|
248
250
|
if (coverage.server === void 0 && coverageServer) coverage.server = coverageServer;
|
|
249
251
|
if (coverage.browser !== void 0 || coverage.server !== void 0) mergedRaw.coverage = coverage;
|
|
252
|
+
const externalId = { ...mergedRaw.externalId };
|
|
253
|
+
const externalIdPattern = env[PIWI_ENV_KEYS.externalIdPattern];
|
|
254
|
+
const externalIdOnDuplicate = env[PIWI_ENV_KEYS.externalIdOnDuplicate];
|
|
255
|
+
if (externalId.pattern === void 0 && externalIdPattern) externalId.pattern = externalIdPattern;
|
|
256
|
+
if (externalId.onDuplicate === void 0 && externalIdOnDuplicate)
|
|
257
|
+
externalId.onDuplicate = externalIdOnDuplicate;
|
|
258
|
+
if (Object.keys(externalId).length > 0) mergedRaw.externalId = externalId;
|
|
250
259
|
const opts = { ...DEFAULTS, ...mergedRaw };
|
|
251
260
|
if (env[PIWI_ENV_KEYS.verbose] !== void 0) opts.verbose = env[PIWI_ENV_KEYS.verbose] === "true";
|
|
252
261
|
return opts;
|
|
@@ -652,6 +661,9 @@ function toWireTestCase(tc) {
|
|
|
652
661
|
tags: rest.tags ?? null,
|
|
653
662
|
locks: rest.locks ?? null,
|
|
654
663
|
testMeta: rest.testMeta ?? null,
|
|
664
|
+
// Kept `undefined` when unset (a `begin` event): `null` would tell the dashboard the test has no ID.
|
|
665
|
+
externalId: rest.externalId,
|
|
666
|
+
displayTitle: rest.displayTitle ?? null,
|
|
655
667
|
locatorSnapshots: rest.locatorSnapshots || null,
|
|
656
668
|
locatorPages: rest.locatorPages || null,
|
|
657
669
|
codeReach: rest.codeReach || null,
|
|
@@ -6102,6 +6114,82 @@ function parseTestMetadata(annotations) {
|
|
|
6102
6114
|
}
|
|
6103
6115
|
return Object.keys(meta).length > 0 ? meta : null;
|
|
6104
6116
|
}
|
|
6117
|
+
var MAX_EXTERNAL_ID_CHARS = 120;
|
|
6118
|
+
function normalizeExternalId(value) {
|
|
6119
|
+
if (typeof value !== "string") return void 0;
|
|
6120
|
+
const trimmed = value.trim();
|
|
6121
|
+
if (!trimmed || trimmed.length > MAX_EXTERNAL_ID_CHARS) return void 0;
|
|
6122
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
6123
|
+
const code = trimmed.charCodeAt(i);
|
|
6124
|
+
if (code < 32 || code === 127) return void 0;
|
|
6125
|
+
}
|
|
6126
|
+
return trimmed;
|
|
6127
|
+
}
|
|
6128
|
+
function parseExternalId(annotations) {
|
|
6129
|
+
if (!Array.isArray(annotations)) return void 0;
|
|
6130
|
+
let id;
|
|
6131
|
+
for (const entry of annotations) {
|
|
6132
|
+
if (!entry || typeof entry !== "object") continue;
|
|
6133
|
+
const { type, description } = entry;
|
|
6134
|
+
if (typeof type !== "string" || type.trim().toLowerCase() !== `${PIWI_ANNOTATION_PREFIX}id`) continue;
|
|
6135
|
+
const value = normalizeExternalId(description);
|
|
6136
|
+
if (value) id = value;
|
|
6137
|
+
}
|
|
6138
|
+
return id;
|
|
6139
|
+
}
|
|
6140
|
+
function compileExternalIdPattern(pattern) {
|
|
6141
|
+
if (typeof pattern === "string") return new RegExp(pattern);
|
|
6142
|
+
return new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, ""));
|
|
6143
|
+
}
|
|
6144
|
+
function readExternalIdFromTitle(title, pattern) {
|
|
6145
|
+
const match = pattern.exec(title);
|
|
6146
|
+
if (!match) return null;
|
|
6147
|
+
const externalId = normalizeExternalId(match.length > 1 ? match[1] : match[0]);
|
|
6148
|
+
if (!externalId) return null;
|
|
6149
|
+
const rest = (title.slice(0, match.index) + title.slice(match.index + match[0].length)).trim();
|
|
6150
|
+
return rest ? { externalId, displayTitle: rest } : { externalId };
|
|
6151
|
+
}
|
|
6152
|
+
var MAX_EXTERNAL_ID_DUPLICATES = 50;
|
|
6153
|
+
var MAX_EXTERNAL_ID_DUPLICATE_TESTS = 10;
|
|
6154
|
+
function duplicateTestKey(test) {
|
|
6155
|
+
return `${test.filePath}\0${(test.suitePath ?? []).join("")}\0${test.title}`;
|
|
6156
|
+
}
|
|
6157
|
+
function readDuplicateTest(raw) {
|
|
6158
|
+
if (!raw || typeof raw !== "object") return null;
|
|
6159
|
+
const { filePath: filePath2, suitePath, title, line } = raw;
|
|
6160
|
+
if (typeof filePath2 !== "string" || typeof title !== "string") return null;
|
|
6161
|
+
const test = { filePath: filePath2, title };
|
|
6162
|
+
if (Array.isArray(suitePath) && suitePath.every((s) => typeof s === "string") && suitePath.length > 0)
|
|
6163
|
+
test.suitePath = suitePath;
|
|
6164
|
+
if (typeof line === "number" && Number.isFinite(line)) test.line = line;
|
|
6165
|
+
return test;
|
|
6166
|
+
}
|
|
6167
|
+
function mergeExternalIdDuplicates(...lists) {
|
|
6168
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6169
|
+
for (const list of lists) {
|
|
6170
|
+
if (!Array.isArray(list)) continue;
|
|
6171
|
+
for (const entry of list) {
|
|
6172
|
+
if (!entry || typeof entry !== "object") continue;
|
|
6173
|
+
const externalId = normalizeExternalId(entry.externalId);
|
|
6174
|
+
const tests = entry.tests;
|
|
6175
|
+
if (!externalId || !Array.isArray(tests)) continue;
|
|
6176
|
+
const read = tests.map(readDuplicateTest).filter((test) => test !== null);
|
|
6177
|
+
if (read.length === 0) continue;
|
|
6178
|
+
let known = byId.get(externalId);
|
|
6179
|
+
if (!known) {
|
|
6180
|
+
if (byId.size >= MAX_EXTERNAL_ID_DUPLICATES) continue;
|
|
6181
|
+
known = /* @__PURE__ */ new Map();
|
|
6182
|
+
byId.set(externalId, known);
|
|
6183
|
+
}
|
|
6184
|
+
for (const test of read) {
|
|
6185
|
+
if (known.size >= MAX_EXTERNAL_ID_DUPLICATE_TESTS) break;
|
|
6186
|
+
const key = duplicateTestKey(test);
|
|
6187
|
+
if (!known.has(key)) known.set(key, test);
|
|
6188
|
+
}
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
return [...byId].map(([externalId, tests]) => ({ externalId, tests: [...tests.values()] }));
|
|
6192
|
+
}
|
|
6105
6193
|
|
|
6106
6194
|
// src/internal/collect/test-meta.ts
|
|
6107
6195
|
function collectTestTags(test) {
|
|
@@ -6115,6 +6203,84 @@ function collectTestMetadata(annotations) {
|
|
|
6115
6203
|
return parseTestMetadata(annotations);
|
|
6116
6204
|
}
|
|
6117
6205
|
|
|
6206
|
+
// src/internal/collect/external-ids.ts
|
|
6207
|
+
function naturalKey(test) {
|
|
6208
|
+
return `${test.filePath}\0${test.suitePath.join("")}\0${test.title}`;
|
|
6209
|
+
}
|
|
6210
|
+
function duplicateTest(test) {
|
|
6211
|
+
return {
|
|
6212
|
+
filePath: test.filePath,
|
|
6213
|
+
...test.suitePath.length ? { suitePath: test.suitePath } : {},
|
|
6214
|
+
title: test.title,
|
|
6215
|
+
line: test.line
|
|
6216
|
+
};
|
|
6217
|
+
}
|
|
6218
|
+
function planExternalIds(tests, options = {}) {
|
|
6219
|
+
let pattern = null;
|
|
6220
|
+
let patternError;
|
|
6221
|
+
if (options.pattern !== void 0 && options.pattern !== "") {
|
|
6222
|
+
try {
|
|
6223
|
+
pattern = compileExternalIdPattern(options.pattern);
|
|
6224
|
+
} catch (error) {
|
|
6225
|
+
patternError = error instanceof Error ? error.message : String(error);
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
const found = /* @__PURE__ */ new Map();
|
|
6229
|
+
const holders = /* @__PURE__ */ new Map();
|
|
6230
|
+
for (const test of tests) {
|
|
6231
|
+
let id = null;
|
|
6232
|
+
const declared = parseExternalId(test.annotations);
|
|
6233
|
+
if (declared) id = { externalId: declared };
|
|
6234
|
+
else if (pattern) {
|
|
6235
|
+
const read = readExternalIdFromTitle(test.title, pattern);
|
|
6236
|
+
if (read) id = options.stripFromTitle ? read : { externalId: read.externalId };
|
|
6237
|
+
}
|
|
6238
|
+
if (!id) continue;
|
|
6239
|
+
found.set(test.testId, id);
|
|
6240
|
+
let keys = holders.get(id.externalId);
|
|
6241
|
+
if (!keys) holders.set(id.externalId, keys = /* @__PURE__ */ new Map());
|
|
6242
|
+
const key = naturalKey(test);
|
|
6243
|
+
if (!keys.has(key)) keys.set(key, test);
|
|
6244
|
+
}
|
|
6245
|
+
const duplicates = [];
|
|
6246
|
+
const duplicateIds = /* @__PURE__ */ new Set();
|
|
6247
|
+
for (const [externalId, keys] of holders) {
|
|
6248
|
+
if (keys.size < 2) continue;
|
|
6249
|
+
duplicateIds.add(externalId);
|
|
6250
|
+
duplicates.push({ externalId, tests: [...keys.values()].map(duplicateTest) });
|
|
6251
|
+
}
|
|
6252
|
+
const byTestId = /* @__PURE__ */ new Map();
|
|
6253
|
+
for (const [testId, id] of found) if (!duplicateIds.has(id.externalId)) byTestId.set(testId, id);
|
|
6254
|
+
return {
|
|
6255
|
+
byTestId,
|
|
6256
|
+
duplicates: mergeExternalIdDuplicates(duplicates),
|
|
6257
|
+
...patternError ? { patternError } : {}
|
|
6258
|
+
};
|
|
6259
|
+
}
|
|
6260
|
+
function formatExternalIdDuplicates(duplicates, onDuplicate) {
|
|
6261
|
+
if (duplicates.length === 0) return [];
|
|
6262
|
+
const lines = [];
|
|
6263
|
+
for (const { externalId, tests } of duplicates) {
|
|
6264
|
+
lines.push(
|
|
6265
|
+
`External ID ${externalId} is declared by ${tests.length} tests; this run matches them by file, describe path and title:`
|
|
6266
|
+
);
|
|
6267
|
+
for (const test of tests) {
|
|
6268
|
+
const where = test.line != null ? `${test.filePath}:${test.line}` : test.filePath;
|
|
6269
|
+
lines.push(` ${where} ${[...test.suitePath ?? [], test.title].join(" \u203A ")}`);
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
lines.push(
|
|
6273
|
+
onDuplicate === "fail" ? "Give each test its own ID: the run fails while two tests share one (externalId.onDuplicate: fail)." : "Give each test its own ID, or set externalId.onDuplicate: 'fail' to fail the run instead."
|
|
6274
|
+
);
|
|
6275
|
+
return lines;
|
|
6276
|
+
}
|
|
6277
|
+
function resolveOnDuplicate(raw) {
|
|
6278
|
+
if (raw === void 0 || raw === null || raw === "") return { onDuplicate: "report" };
|
|
6279
|
+
const value = String(raw).trim().toLowerCase();
|
|
6280
|
+
if (value === "report" || value === "fail") return { onDuplicate: value };
|
|
6281
|
+
return { onDuplicate: "report", invalid: String(raw) };
|
|
6282
|
+
}
|
|
6283
|
+
|
|
6118
6284
|
// ../core/src/error-text.ts
|
|
6119
6285
|
function joinErrorMessages(errors) {
|
|
6120
6286
|
if (!errors || errors.length === 0) return null;
|
|
@@ -10632,6 +10798,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10632
10798
|
this.instanceId = "";
|
|
10633
10799
|
this.shardInfo = null;
|
|
10634
10800
|
this.metadata = {};
|
|
10801
|
+
/** Each planned test's external ID and the IDs two tests share, settled in `onBegin`. */
|
|
10802
|
+
this.externalIds = null;
|
|
10635
10803
|
this.isFullRun = true;
|
|
10636
10804
|
this.filterDetails = null;
|
|
10637
10805
|
/** Configured `maxFailures` (0 = unlimited) — disambiguates an interrupted run's unrun reason. */
|
|
@@ -10780,6 +10948,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10780
10948
|
this.metadata = flakeRunMetadata(stamp, this.metadata);
|
|
10781
10949
|
}
|
|
10782
10950
|
this.plannedTests = suite.allTests();
|
|
10951
|
+
this.planExternalIds();
|
|
10783
10952
|
this.shardInfo = resolveShardInfo(config);
|
|
10784
10953
|
if (this.shardInfo) {
|
|
10785
10954
|
this.logger.info(`Shard ${this.shardInfo.current}/${this.shardInfo.total} detected`);
|
|
@@ -10824,6 +10993,44 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10824
10993
|
this.plannedTests.length
|
|
10825
10994
|
);
|
|
10826
10995
|
}
|
|
10996
|
+
/**
|
|
10997
|
+
* Settle each planned test's external ID before any test runs, name the IDs two tests share, and record them on
|
|
10998
|
+
* the run so the dashboard lists them too.
|
|
10999
|
+
*/
|
|
11000
|
+
planExternalIds() {
|
|
11001
|
+
const options = this.options.externalId ?? {};
|
|
11002
|
+
const plan = planExternalIds(
|
|
11003
|
+
this.plannedTests.map((test) => ({
|
|
11004
|
+
testId: test.id,
|
|
11005
|
+
filePath: testFile(test, this.testPathBase),
|
|
11006
|
+
suitePath: this.metadataCollector.getSuiteInfo(test).suitePath ?? [],
|
|
11007
|
+
title: test.title,
|
|
11008
|
+
line: test.location.line,
|
|
11009
|
+
annotations: test.annotations ?? []
|
|
11010
|
+
})),
|
|
11011
|
+
options
|
|
11012
|
+
);
|
|
11013
|
+
if (plan.patternError) {
|
|
11014
|
+
this.logger.error(
|
|
11015
|
+
`externalId.pattern is not a valid regular expression, so no ID is read from titles: ${plan.patternError}`
|
|
11016
|
+
);
|
|
11017
|
+
}
|
|
11018
|
+
const { onDuplicate, invalid } = resolveOnDuplicate(options.onDuplicate);
|
|
11019
|
+
if (invalid !== void 0) {
|
|
11020
|
+
this.logger.warn(
|
|
11021
|
+
`externalId.onDuplicate is "${invalid}"; expected 'report' or 'fail', so duplicates are reported.`
|
|
11022
|
+
);
|
|
11023
|
+
}
|
|
11024
|
+
for (const line of formatExternalIdDuplicates(plan.duplicates, onDuplicate)) this.logger.warn(line);
|
|
11025
|
+
if (plan.duplicates.length > 0) this.metadata = { ...this.metadata, externalIdDuplicates: plan.duplicates };
|
|
11026
|
+
this.externalIds = plan;
|
|
11027
|
+
}
|
|
11028
|
+
/** The external ID fields of a test's payload: `null` tells the dashboard the test has none to match on. */
|
|
11029
|
+
externalIdFields(test) {
|
|
11030
|
+
if (!this.externalIds) return {};
|
|
11031
|
+
const id = this.externalIds.byTestId.get(test.id);
|
|
11032
|
+
return { externalId: id?.externalId ?? null, displayTitle: id?.displayTitle ?? null };
|
|
11033
|
+
}
|
|
10827
11034
|
/** Playwright reporter hook: called when an individual test begins */
|
|
10828
11035
|
onTestBegin(test, result) {
|
|
10829
11036
|
this.lastTestWatch?.begin(test.id);
|
|
@@ -10951,6 +11158,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
10951
11158
|
tags: tags.length ? tags : null,
|
|
10952
11159
|
locks: locks.length ? locks : null,
|
|
10953
11160
|
testMeta: collectTestMetadata(annotations),
|
|
11161
|
+
...this.externalIdFields(test),
|
|
10954
11162
|
// An annotation-less skip reclassified to `didnotrun` is a serial-group
|
|
10955
11163
|
// cascade: an earlier test failed and Playwright skipped the rest.
|
|
10956
11164
|
didNotRunReason: status === "didnotrun" ? "previous-failure" : null,
|
|
@@ -11153,6 +11361,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11153
11361
|
tags: tags.length ? tags : null,
|
|
11154
11362
|
locks: locks.length ? locks : null,
|
|
11155
11363
|
testMeta: collectTestMetadata(declaredAnnotations),
|
|
11364
|
+
...this.externalIdFields(test),
|
|
11156
11365
|
didNotRunReason: reason,
|
|
11157
11366
|
expectedStatus: test.expectedStatus ?? null
|
|
11158
11367
|
};
|
|
@@ -11164,8 +11373,23 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
11164
11373
|
}
|
|
11165
11374
|
}
|
|
11166
11375
|
}
|
|
11167
|
-
/**
|
|
11376
|
+
/**
|
|
11377
|
+
* Playwright reporter hook: called when the full test run finishes. Fails the run when two tests share an external
|
|
11378
|
+
* ID and `externalId.onDuplicate` is `'fail'`.
|
|
11379
|
+
*/
|
|
11168
11380
|
async onEnd(result) {
|
|
11381
|
+
const duplicates = this.externalIds?.duplicates.length ?? 0;
|
|
11382
|
+
const failsOnDuplicates = duplicates > 0 && resolveOnDuplicate(this.options.externalId?.onDuplicate).onDuplicate === "fail";
|
|
11383
|
+
const overridden = failsOnDuplicates && result?.status === "passed";
|
|
11384
|
+
await this.finishRun(overridden ? { ...result, status: "failed" } : result);
|
|
11385
|
+
if (failsOnDuplicates) {
|
|
11386
|
+
this.logger.error(
|
|
11387
|
+
`Failing the run: ${duplicates} external ID${duplicates === 1 ? " is" : "s are"} declared by more than one test.`
|
|
11388
|
+
);
|
|
11389
|
+
return { status: "failed" };
|
|
11390
|
+
}
|
|
11391
|
+
}
|
|
11392
|
+
async finishRun(result) {
|
|
11169
11393
|
if (this.listMode || !this.enabled) return;
|
|
11170
11394
|
if (this.startTime === null) {
|
|
11171
11395
|
this.logger.debug("The run stopped before any test began \u2014 nothing to report.");
|
|
@@ -2648,6 +2648,8 @@ var PIWI_ENV_KEYS = {
|
|
|
2648
2648
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
2649
2649
|
coverageBrowser: "PIWI_COVERAGE_BROWSER",
|
|
2650
2650
|
coverageServer: "PIWI_COVERAGE_SERVER",
|
|
2651
|
+
externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
|
|
2652
|
+
externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
|
|
2651
2653
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
2652
2654
|
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
2653
2655
|
leakCheck: "PIWI_LEAK_CHECK",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.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",
|
|
@@ -21,8 +21,9 @@ view, or the reporter CLI with `PIWI_DASHBOARD_URL` / `PIWI_API_KEY` /
|
|
|
21
21
|
## Steps
|
|
22
22
|
|
|
23
23
|
1. **Find the gaps.** On a change, call `get_change_coverage` for the current
|
|
24
|
-
branch first — the
|
|
25
|
-
|
|
24
|
+
branch first — the files whose `reach` is `unreached` (the uncovered
|
|
25
|
+
changes) and the `lines.notRun` of the files it did reach are what the
|
|
26
|
+
change left untested. Otherwise call `list_scenario_gaps` for the project. Filter to what you are
|
|
26
27
|
working on with `class`, `feature`, `minScore` or `pr`. Read the classes:
|
|
27
28
|
- **blind-spot** — no trusted test reaches it (a route, a page, a control, a
|
|
28
29
|
new error path).
|