@sentinelqa/playwright-reporter 0.1.28 → 0.1.29
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/localReport.d.ts +2 -0
- package/dist/localReport.js +51 -42
- package/dist/quickDiagnosis.d.ts +1 -0
- package/dist/quickDiagnosis.js +34 -16
- package/package.json +1 -1
package/dist/localReport.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ type CopiedArtifact = {
|
|
|
21
21
|
};
|
|
22
22
|
type ReportTest = {
|
|
23
23
|
id: string;
|
|
24
|
+
matchKey: string;
|
|
24
25
|
title: string;
|
|
25
26
|
titlePath: string[];
|
|
26
27
|
file: string | null;
|
|
@@ -51,6 +52,7 @@ type RunSnapshot = {
|
|
|
51
52
|
totals: LocalReportSummary;
|
|
52
53
|
tests: Array<{
|
|
53
54
|
id: string;
|
|
55
|
+
matchKey: string;
|
|
54
56
|
title: string;
|
|
55
57
|
status: string;
|
|
56
58
|
signal: string | null;
|
package/dist/localReport.js
CHANGED
|
@@ -95,6 +95,23 @@ const safeSlug = (value) => {
|
|
|
95
95
|
.replace(/^-+|-+$/g, "")
|
|
96
96
|
.slice(0, 64) || "artifact");
|
|
97
97
|
};
|
|
98
|
+
const buildTitlePath = (baseTitles, test) => {
|
|
99
|
+
const title = typeof test?.title === "string" ? test.title : null;
|
|
100
|
+
if (!title)
|
|
101
|
+
return baseTitles.filter(Boolean);
|
|
102
|
+
if (baseTitles[baseTitles.length - 1] === title)
|
|
103
|
+
return baseTitles.filter(Boolean);
|
|
104
|
+
return [...baseTitles, title].filter(Boolean);
|
|
105
|
+
};
|
|
106
|
+
const buildTestIdentity = (test, titlePath) => {
|
|
107
|
+
const file = test?.location?.file || "unknown";
|
|
108
|
+
const project = test?.projectName || "default";
|
|
109
|
+
const joined = titlePath.join(" > ");
|
|
110
|
+
return {
|
|
111
|
+
id: [file, project, joined].join("::"),
|
|
112
|
+
matchKey: [file, joined].join("::")
|
|
113
|
+
};
|
|
114
|
+
};
|
|
98
115
|
const formatDuration = (durationMs) => {
|
|
99
116
|
if (!Number.isFinite(durationMs) || durationMs <= 0)
|
|
100
117
|
return "0 ms";
|
|
@@ -233,7 +250,7 @@ const copyArtifact = (sourcePath, kind, reportDir, usedRelativePaths, testId) =>
|
|
|
233
250
|
testId
|
|
234
251
|
};
|
|
235
252
|
};
|
|
236
|
-
const createReportTest = (test, titlePath
|
|
253
|
+
const createReportTest = (test, titlePath) => {
|
|
237
254
|
const results = Array.isArray(test?.results) ? test.results : [];
|
|
238
255
|
const lastResult = results.length > 0 ? results[results.length - 1] : null;
|
|
239
256
|
const errors = results.flatMap((result) => Array.isArray(result?.errors)
|
|
@@ -242,30 +259,31 @@ const createReportTest = (test, titlePath, diagnosisById) => {
|
|
|
242
259
|
.filter(Boolean)
|
|
243
260
|
: []);
|
|
244
261
|
const duration = results.reduce((total, result) => total + (Number(result?.duration) || 0), 0);
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
titlePath.join(" > ")
|
|
249
|
-
].join("::");
|
|
262
|
+
const identity = buildTestIdentity(test, titlePath);
|
|
263
|
+
const status = normalizeTestStatus(test?.status || lastResult?.status || "unknown");
|
|
264
|
+
const primaryError = errors[0] || "";
|
|
250
265
|
return {
|
|
251
|
-
id,
|
|
266
|
+
id: identity.id,
|
|
267
|
+
matchKey: identity.matchKey,
|
|
252
268
|
title: test?.title || titlePath[titlePath.length - 1] || "Untitled test",
|
|
253
269
|
titlePath,
|
|
254
270
|
file: test?.location?.file || null,
|
|
255
271
|
projectName: test?.projectName || null,
|
|
256
|
-
status
|
|
272
|
+
status,
|
|
257
273
|
duration,
|
|
258
274
|
errors,
|
|
259
|
-
diagnosis:
|
|
275
|
+
diagnosis: ["failed", "timedOut", "interrupted"].includes(status) && primaryError
|
|
276
|
+
? (0, quickDiagnosis_1.parseFailureFacts)(test?.title || titlePath[titlePath.length - 1] || "Untitled test", titlePath, primaryError, status)
|
|
277
|
+
: null,
|
|
260
278
|
artifacts: []
|
|
261
279
|
};
|
|
262
280
|
};
|
|
263
|
-
const collectTests = (node,
|
|
281
|
+
const collectTests = (node, parentTitles = []) => {
|
|
264
282
|
const nextTitles = node?.title ? [...parentTitles, node.title] : parentTitles;
|
|
265
283
|
const collected = [];
|
|
266
284
|
if (Array.isArray(node?.tests)) {
|
|
267
285
|
for (const test of node.tests) {
|
|
268
|
-
collected.push(createReportTest(test,
|
|
286
|
+
collected.push(createReportTest(test, buildTitlePath(nextTitles, test)));
|
|
269
287
|
}
|
|
270
288
|
}
|
|
271
289
|
if (Array.isArray(node?.specs)) {
|
|
@@ -273,13 +291,13 @@ const collectTests = (node, diagnosisById, parentTitles = []) => {
|
|
|
273
291
|
const specTitles = [...nextTitles, spec?.title].filter(Boolean);
|
|
274
292
|
const specTests = Array.isArray(spec?.tests) ? spec.tests : [];
|
|
275
293
|
for (const test of specTests) {
|
|
276
|
-
collected.push(createReportTest(test, specTitles,
|
|
294
|
+
collected.push(createReportTest(test, buildTitlePath(specTitles, test)));
|
|
277
295
|
}
|
|
278
296
|
}
|
|
279
297
|
}
|
|
280
298
|
if (Array.isArray(node?.suites)) {
|
|
281
299
|
for (const suite of node.suites) {
|
|
282
|
-
collected.push(...collectTests(suite,
|
|
300
|
+
collected.push(...collectTests(suite, nextTitles));
|
|
283
301
|
}
|
|
284
302
|
}
|
|
285
303
|
return collected;
|
|
@@ -289,12 +307,8 @@ const collectTestRefs = (node, parentTitles = []) => {
|
|
|
289
307
|
const refs = [];
|
|
290
308
|
if (Array.isArray(node?.tests)) {
|
|
291
309
|
for (const test of node.tests) {
|
|
292
|
-
const titlePath =
|
|
293
|
-
const id =
|
|
294
|
-
test?.location?.file || "unknown",
|
|
295
|
-
test?.projectName || "default",
|
|
296
|
-
titlePath.join(" > ")
|
|
297
|
-
].join("::");
|
|
310
|
+
const titlePath = buildTitlePath(nextTitles, test);
|
|
311
|
+
const id = buildTestIdentity(test, titlePath).id;
|
|
298
312
|
refs.push({ id, resultList: Array.isArray(test?.results) ? test.results : [] });
|
|
299
313
|
}
|
|
300
314
|
}
|
|
@@ -302,11 +316,7 @@ const collectTestRefs = (node, parentTitles = []) => {
|
|
|
302
316
|
for (const spec of node.specs) {
|
|
303
317
|
const titlePath = [...nextTitles, spec?.title].filter(Boolean);
|
|
304
318
|
for (const test of Array.isArray(spec?.tests) ? spec.tests : []) {
|
|
305
|
-
const id =
|
|
306
|
-
test?.location?.file || "unknown",
|
|
307
|
-
test?.projectName || "default",
|
|
308
|
-
titlePath.join(" > ")
|
|
309
|
-
].join("::");
|
|
319
|
+
const id = buildTestIdentity(test, buildTitlePath(titlePath, test)).id;
|
|
310
320
|
refs.push({ id, resultList: Array.isArray(test?.results) ? test.results : [] });
|
|
311
321
|
}
|
|
312
322
|
}
|
|
@@ -331,14 +341,6 @@ const summarizeTests = (tests) => {
|
|
|
331
341
|
return summary;
|
|
332
342
|
}, { total: 0, failed: 0, passed: 0, skipped: 0 });
|
|
333
343
|
};
|
|
334
|
-
const buildDiagnosisMap = (playwrightJsonPath) => {
|
|
335
|
-
const failures = (0, quickDiagnosis_1.collectFailureFacts)(playwrightJsonPath);
|
|
336
|
-
const map = new Map();
|
|
337
|
-
for (const failure of failures) {
|
|
338
|
-
map.set(failure.titlePath.join(" > "), failure);
|
|
339
|
-
}
|
|
340
|
-
return map;
|
|
341
|
-
};
|
|
342
344
|
const getFailureTests = (tests) => tests.filter((test) => ["failed", "timedOut", "interrupted"].includes(test.status));
|
|
343
345
|
const groupSimilarFailures = (tests) => {
|
|
344
346
|
const groups = new Map();
|
|
@@ -356,7 +358,9 @@ const groupSimilarFailures = (tests) => {
|
|
|
356
358
|
}
|
|
357
359
|
groups.get(key).tests.push(test);
|
|
358
360
|
}
|
|
359
|
-
return Array.from(groups.values())
|
|
361
|
+
return Array.from(groups.values())
|
|
362
|
+
.filter((group) => group.tests.length > 1)
|
|
363
|
+
.sort((a, b) => b.tests.length - a.tests.length);
|
|
360
364
|
};
|
|
361
365
|
const buildRunSnapshot = (tests, summary) => ({
|
|
362
366
|
generatedAt: new Date().toISOString(),
|
|
@@ -365,6 +369,7 @@ const buildRunSnapshot = (tests, summary) => ({
|
|
|
365
369
|
totals: summary,
|
|
366
370
|
tests: tests.map((test) => ({
|
|
367
371
|
id: test.id,
|
|
372
|
+
matchKey: test.matchKey,
|
|
368
373
|
title: test.titlePath.join(" > ") || test.title,
|
|
369
374
|
status: test.status,
|
|
370
375
|
signal: test.diagnosis?.signal || null,
|
|
@@ -407,18 +412,21 @@ const buildRunDiff = (tests, snapshot) => {
|
|
|
407
412
|
|| readSnapshot(path_1.default.resolve(process.cwd(), ".sentinel", "latest.json"));
|
|
408
413
|
if (!previous || previous.generatedAt === snapshot.generatedAt)
|
|
409
414
|
return null;
|
|
410
|
-
const
|
|
415
|
+
const currentById = new Map(tests.map((test) => [test.id, test]));
|
|
416
|
+
const currentByMatchKey = new Map(tests.map((test) => [test.matchKey, test]));
|
|
411
417
|
const currentFailures = getFailureTests(tests);
|
|
412
418
|
const previousFailures = previous.tests.filter((test) => ["failed", "timedOut", "interrupted"].includes(test.status));
|
|
413
419
|
const previousFailureIds = new Set(previousFailures.map((test) => test.id));
|
|
420
|
+
const previousFailureMatchKeys = new Set(previousFailures.map((test) => (typeof test.matchKey === "string" ? test.matchKey : test.id)));
|
|
414
421
|
return {
|
|
415
422
|
label: previous.branch === snapshot.branch ? `Compared to previous ${snapshot.branch} run` : "Compared to previous run",
|
|
416
|
-
newFailures: currentFailures.filter((test) => !previousFailureIds.has(test.id)),
|
|
423
|
+
newFailures: currentFailures.filter((test) => !previousFailureIds.has(test.id) && !previousFailureMatchKeys.has(test.matchKey)),
|
|
417
424
|
fixedTests: previousFailures.filter((test) => {
|
|
418
|
-
const current =
|
|
425
|
+
const current = currentById.get(test.id) ||
|
|
426
|
+
currentByMatchKey.get(typeof test.matchKey === "string" ? test.matchKey : test.id);
|
|
419
427
|
return current && !["failed", "timedOut", "interrupted"].includes(current.status);
|
|
420
428
|
}),
|
|
421
|
-
stillFailing: currentFailures.filter((test) => previousFailureIds.has(test.id))
|
|
429
|
+
stillFailing: currentFailures.filter((test) => previousFailureIds.has(test.id) || previousFailureMatchKeys.has(test.matchKey))
|
|
422
430
|
};
|
|
423
431
|
};
|
|
424
432
|
const renderArtifact = (artifact) => {
|
|
@@ -599,7 +607,9 @@ const renderFailureDigest = (tests) => {
|
|
|
599
607
|
.map((test) => {
|
|
600
608
|
const diagnosis = test.diagnosis;
|
|
601
609
|
const title = escapeHtml(test.title);
|
|
602
|
-
const summary = escapeHtml(diagnosis
|
|
610
|
+
const summary = escapeHtml(diagnosis
|
|
611
|
+
? (0, quickDiagnosis_1.describeFailure)(diagnosis)
|
|
612
|
+
: (test.errors[0]?.split(/\r?\n/)[0]?.trim() || "Open the failure details to inspect the exact Playwright error."));
|
|
603
613
|
const debugSummary = escapeHtml(diagnosis
|
|
604
614
|
? (0, quickDiagnosis_1.buildDebugSummary)(diagnosis)
|
|
605
615
|
: `Test: ${test.title}\nDiagnosis: Review trace and error details in the expanded card.`);
|
|
@@ -634,7 +644,7 @@ const renderFailureDigest = (tests) => {
|
|
|
634
644
|
};
|
|
635
645
|
const renderSimilarFailureGroups = (groups) => {
|
|
636
646
|
if (!groups.length) {
|
|
637
|
-
return `<div class="empty-state">No failure
|
|
647
|
+
return `<div class="empty-state">No repeated failure fingerprint was detected in this run.</div>`;
|
|
638
648
|
}
|
|
639
649
|
return groups
|
|
640
650
|
.map((group) => `
|
|
@@ -1223,7 +1233,7 @@ const buildHtml = (tests, summary, extraArtifacts, runDiff) => {
|
|
|
1223
1233
|
<h2>Similar Failures</h2>
|
|
1224
1234
|
<div class="failed-count">${similarGroups.length} groups</div>
|
|
1225
1235
|
</div>
|
|
1226
|
-
<p>Failures are grouped
|
|
1236
|
+
<p>Failures are grouped only when they share the same fingerprint, so one repeated issue is easier to spot.</p>
|
|
1227
1237
|
${renderSimilarFailureGroups(similarGroups)}
|
|
1228
1238
|
</section>
|
|
1229
1239
|
|
|
@@ -1400,8 +1410,7 @@ function generateLocalDebugReport(options) {
|
|
|
1400
1410
|
const reportJsonRaw = fs_1.default.readFileSync(options.playwrightJsonPath, "utf8");
|
|
1401
1411
|
const reportJson = JSON.parse(reportJsonRaw);
|
|
1402
1412
|
const reportRoot = { suites: reportJson?.suites || [] };
|
|
1403
|
-
const
|
|
1404
|
-
const tests = collectTests(reportRoot, diagnosisById);
|
|
1413
|
+
const tests = collectTests(reportRoot);
|
|
1405
1414
|
const testsById = new Map(tests.map((test) => [test.id, test]));
|
|
1406
1415
|
const claimedSourcePaths = new Set();
|
|
1407
1416
|
const attachArtifactToTest = (sourcePath, testId) => {
|
package/dist/quickDiagnosis.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type FailureFacts = {
|
|
|
15
15
|
status: string;
|
|
16
16
|
};
|
|
17
17
|
export declare const collectFailureFacts: (playwrightJsonPath: string) => FailureFacts[];
|
|
18
|
+
export declare const parseFailureFacts: (title: string, titlePath: string[], message: string, status: string) => FailureFacts;
|
|
18
19
|
export declare const describeFailure: (failure: FailureFacts) => string;
|
|
19
20
|
export declare const buildDebugSummary: (failure: FailureFacts) => string;
|
|
20
21
|
export declare const buildSimilarityKey: (failure: FailureFacts) => string;
|
package/dist/quickDiagnosis.js
CHANGED
|
@@ -3,8 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.buildQuickDiagnosis = exports.summarizeSignal = exports.buildSimilarityKey = exports.buildDebugSummary = exports.describeFailure = exports.collectFailureFacts = void 0;
|
|
6
|
+
exports.buildQuickDiagnosis = exports.summarizeSignal = exports.buildSimilarityKey = exports.buildDebugSummary = exports.describeFailure = exports.parseFailureFacts = exports.collectFailureFacts = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const normalizeMessageFingerprint = (message) => stripAnsi(message)
|
|
9
|
+
.split(/\r?\n/)
|
|
10
|
+
.map((line) => line.trim())
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.slice(0, 3)
|
|
13
|
+
.join(" | ")
|
|
14
|
+
.replace(/\b\d+ms\b/gi, "<ms>")
|
|
15
|
+
.replace(/:\d+:\d+/g, ":<line>:<col>")
|
|
16
|
+
.replace(/\s+/g, " ")
|
|
17
|
+
.slice(0, 200);
|
|
8
18
|
const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
9
19
|
const toMessage = (result) => {
|
|
10
20
|
const direct = result.error?.message ||
|
|
@@ -120,24 +130,26 @@ const collectFailureFacts = (playwrightJsonPath) => {
|
|
|
120
130
|
const raw = node_fs_1.default.readFileSync(playwrightJsonPath, "utf8");
|
|
121
131
|
const parsed = JSON.parse(raw);
|
|
122
132
|
const failedCases = flattenFailedCases(parsed);
|
|
123
|
-
return failedCases.map((failed) => (
|
|
124
|
-
title: shortenTitle(failed.title),
|
|
125
|
-
titlePath: failed.title.split(" > ").filter(Boolean),
|
|
126
|
-
message: failed.message,
|
|
127
|
-
signal: failed.signal,
|
|
128
|
-
locator: extractLocator(failed.message),
|
|
129
|
-
expected: extractExpected(failed.message),
|
|
130
|
-
received: extractReceived(failed.message),
|
|
131
|
-
timeoutMs: extractTimeoutMs(failed.message),
|
|
132
|
-
lastUrl: extractLastUrl(failed.message),
|
|
133
|
-
status: "failed"
|
|
134
|
-
}));
|
|
133
|
+
return failedCases.map((failed) => (0, exports.parseFailureFacts)(shortenTitle(failed.title), failed.title.split(" > ").filter(Boolean), failed.message, "failed"));
|
|
135
134
|
}
|
|
136
135
|
catch {
|
|
137
136
|
return [];
|
|
138
137
|
}
|
|
139
138
|
};
|
|
140
139
|
exports.collectFailureFacts = collectFailureFacts;
|
|
140
|
+
const parseFailureFacts = (title, titlePath, message, status) => ({
|
|
141
|
+
title,
|
|
142
|
+
titlePath,
|
|
143
|
+
message,
|
|
144
|
+
signal: classifySignal(message),
|
|
145
|
+
locator: extractLocator(message),
|
|
146
|
+
expected: extractExpected(message),
|
|
147
|
+
received: extractReceived(message),
|
|
148
|
+
timeoutMs: extractTimeoutMs(message),
|
|
149
|
+
lastUrl: extractLastUrl(message),
|
|
150
|
+
status
|
|
151
|
+
});
|
|
152
|
+
exports.parseFailureFacts = parseFailureFacts;
|
|
141
153
|
const describeFailure = (failure) => {
|
|
142
154
|
if (failure.signal === "assertion_mismatch" && failure.locator && failure.expected && failure.received) {
|
|
143
155
|
return `${failure.locator} showed "${failure.received}" instead of "${failure.expected}" before timeout.`;
|
|
@@ -179,9 +191,15 @@ const buildDebugSummary = (failure) => {
|
|
|
179
191
|
};
|
|
180
192
|
exports.buildDebugSummary = buildDebugSummary;
|
|
181
193
|
const buildSimilarityKey = (failure) => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
if (failure.locator || failure.expected || failure.received) {
|
|
195
|
+
return [
|
|
196
|
+
failure.signal,
|
|
197
|
+
failure.locator || "unknown-locator",
|
|
198
|
+
failure.expected || "unknown-expected",
|
|
199
|
+
failure.received || "unknown-received"
|
|
200
|
+
].join("|");
|
|
201
|
+
}
|
|
202
|
+
return `${failure.signal}|${normalizeMessageFingerprint(failure.message)}`;
|
|
185
203
|
};
|
|
186
204
|
exports.buildSimilarityKey = buildSimilarityKey;
|
|
187
205
|
exports.summarizeSignal = signalSummary;
|