@sentinelqa/playwright-reporter 0.1.11 → 0.1.13
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.js +175 -117
- package/package.json +1 -1
package/dist/localReport.js
CHANGED
|
@@ -20,6 +20,17 @@ const ARTIFACT_EXTENSIONS = {
|
|
|
20
20
|
report: [".html", ".json"],
|
|
21
21
|
attachment: []
|
|
22
22
|
};
|
|
23
|
+
const normalizeTestStatus = (status) => {
|
|
24
|
+
if (!status)
|
|
25
|
+
return "unknown";
|
|
26
|
+
if (status === "expected")
|
|
27
|
+
return "passed";
|
|
28
|
+
if (status === "unexpected")
|
|
29
|
+
return "failed";
|
|
30
|
+
if (status === "flaky")
|
|
31
|
+
return "passed";
|
|
32
|
+
return status;
|
|
33
|
+
};
|
|
23
34
|
const escapeHtml = (value) => value
|
|
24
35
|
.replace(/&/g, "&")
|
|
25
36
|
.replace(/</g, "<")
|
|
@@ -141,41 +152,89 @@ const copyArtifact = (sourcePath, kind, reportDir, usedRelativePaths, testId) =>
|
|
|
141
152
|
testId
|
|
142
153
|
};
|
|
143
154
|
};
|
|
144
|
-
const
|
|
155
|
+
const createReportTest = (test, titlePath) => {
|
|
156
|
+
const results = Array.isArray(test?.results) ? test.results : [];
|
|
157
|
+
const lastResult = results.length > 0 ? results[results.length - 1] : null;
|
|
158
|
+
const errors = results.flatMap((result) => Array.isArray(result?.errors)
|
|
159
|
+
? result.errors
|
|
160
|
+
.map((error) => error?.message || error?.stack || String(error || ""))
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
: []);
|
|
163
|
+
const duration = results.reduce((total, result) => total + (Number(result?.duration) || 0), 0);
|
|
164
|
+
const id = [
|
|
165
|
+
test?.location?.file || "unknown",
|
|
166
|
+
test?.projectName || "default",
|
|
167
|
+
titlePath.join(" > ")
|
|
168
|
+
].join("::");
|
|
169
|
+
return {
|
|
170
|
+
id,
|
|
171
|
+
title: test?.title || titlePath[titlePath.length - 1] || "Untitled test",
|
|
172
|
+
titlePath,
|
|
173
|
+
file: test?.location?.file || null,
|
|
174
|
+
projectName: test?.projectName || null,
|
|
175
|
+
status: normalizeTestStatus(test?.status || lastResult?.status || "unknown"),
|
|
176
|
+
duration,
|
|
177
|
+
errors,
|
|
178
|
+
artifacts: []
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
const collectTests = (node, parentTitles = []) => {
|
|
182
|
+
const nextTitles = node?.title ? [...parentTitles, node.title] : parentTitles;
|
|
183
|
+
const collected = [];
|
|
184
|
+
if (Array.isArray(node?.tests)) {
|
|
185
|
+
for (const test of node.tests) {
|
|
186
|
+
collected.push(createReportTest(test, [...nextTitles, test?.title].filter(Boolean)));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(node?.specs)) {
|
|
190
|
+
for (const spec of node.specs) {
|
|
191
|
+
const specTitles = [...nextTitles, spec?.title].filter(Boolean);
|
|
192
|
+
const specTests = Array.isArray(spec?.tests) ? spec.tests : [];
|
|
193
|
+
for (const test of specTests) {
|
|
194
|
+
collected.push(createReportTest(test, specTitles));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(node?.suites)) {
|
|
199
|
+
for (const suite of node.suites) {
|
|
200
|
+
collected.push(...collectTests(suite, nextTitles));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return collected;
|
|
204
|
+
};
|
|
205
|
+
const collectTestRefs = (node, parentTitles = []) => {
|
|
145
206
|
const nextTitles = node?.title ? [...parentTitles, node.title] : parentTitles;
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const lastResult = results.length > 0 ? results[results.length - 1] : null;
|
|
151
|
-
const errors = results.flatMap((result) => Array.isArray(result?.errors)
|
|
152
|
-
? result.errors
|
|
153
|
-
.map((error) => error?.message || error?.stack || String(error || ""))
|
|
154
|
-
.filter(Boolean)
|
|
155
|
-
: []);
|
|
156
|
-
const duration = results.reduce((total, result) => total + (Number(result?.duration) || 0), 0);
|
|
207
|
+
const refs = [];
|
|
208
|
+
if (Array.isArray(node?.tests)) {
|
|
209
|
+
for (const test of node.tests) {
|
|
210
|
+
const titlePath = [...nextTitles, test?.title].filter(Boolean);
|
|
157
211
|
const id = [
|
|
158
|
-
test
|
|
159
|
-
test
|
|
212
|
+
test?.location?.file || "unknown",
|
|
213
|
+
test?.projectName || "default",
|
|
160
214
|
titlePath.join(" > ")
|
|
161
215
|
].join("::");
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
216
|
+
refs.push({ id, resultList: Array.isArray(test?.results) ? test.results : [] });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(node?.specs)) {
|
|
220
|
+
for (const spec of node.specs) {
|
|
221
|
+
const titlePath = [...nextTitles, spec?.title].filter(Boolean);
|
|
222
|
+
for (const test of Array.isArray(spec?.tests) ? spec.tests : []) {
|
|
223
|
+
const id = [
|
|
224
|
+
test?.location?.file || "unknown",
|
|
225
|
+
test?.projectName || "default",
|
|
226
|
+
titlePath.join(" > ")
|
|
227
|
+
].join("::");
|
|
228
|
+
refs.push({ id, resultList: Array.isArray(test?.results) ? test.results : [] });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (Array.isArray(node?.suites)) {
|
|
233
|
+
for (const suite of node.suites) {
|
|
234
|
+
refs.push(...collectTestRefs(suite, nextTitles));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return refs;
|
|
179
238
|
};
|
|
180
239
|
const summarizeTests = (tests) => {
|
|
181
240
|
return tests.reduce((summary, test) => {
|
|
@@ -235,8 +294,8 @@ const renderTestCard = (test) => {
|
|
|
235
294
|
? test.artifacts.map((artifact) => renderArtifact(artifact)).join("\n")
|
|
236
295
|
: `<div class="empty-state">No test-linked artifacts were detected for this result.</div>`;
|
|
237
296
|
return `
|
|
238
|
-
<
|
|
239
|
-
<
|
|
297
|
+
<details class="test-card">
|
|
298
|
+
<summary class="test-summary">
|
|
240
299
|
<div>
|
|
241
300
|
<div class="status-pill ${statusClass}">${escapeHtml(test.status)}</div>
|
|
242
301
|
<h3>${escapeHtml(test.titlePath.join(" > ") || test.title)}</h3>
|
|
@@ -246,7 +305,7 @@ const renderTestCard = (test) => {
|
|
|
246
305
|
${projectLine}
|
|
247
306
|
<div class="meta-item">Duration: ${escapeHtml(formatDuration(test.duration))}</div>
|
|
248
307
|
</div>
|
|
249
|
-
</
|
|
308
|
+
</summary>
|
|
250
309
|
<div class="panel">
|
|
251
310
|
<h4>Error</h4>
|
|
252
311
|
${errorBlock}
|
|
@@ -257,7 +316,7 @@ const renderTestCard = (test) => {
|
|
|
257
316
|
${artifactMarkup}
|
|
258
317
|
</div>
|
|
259
318
|
</div>
|
|
260
|
-
</
|
|
319
|
+
</details>
|
|
261
320
|
`;
|
|
262
321
|
};
|
|
263
322
|
const renderAdditionalArtifacts = (artifacts) => {
|
|
@@ -318,41 +377,56 @@ const buildHtml = (tests, summary, extraArtifacts) => {
|
|
|
318
377
|
padding: 40px 20px 80px;
|
|
319
378
|
}
|
|
320
379
|
.hero {
|
|
321
|
-
|
|
380
|
+
position: relative;
|
|
381
|
+
padding: 22px;
|
|
322
382
|
border: 1px solid var(--panel-border);
|
|
323
383
|
border-radius: 24px;
|
|
324
384
|
background: rgba(13, 17, 23, 0.88);
|
|
325
385
|
backdrop-filter: blur(12px);
|
|
326
386
|
}
|
|
387
|
+
.hero-badge {
|
|
388
|
+
position: absolute;
|
|
389
|
+
top: 18px;
|
|
390
|
+
right: 18px;
|
|
391
|
+
display: inline-flex;
|
|
392
|
+
align-items: center;
|
|
393
|
+
padding: 6px 10px;
|
|
394
|
+
border-radius: 999px;
|
|
395
|
+
border: 1px solid rgba(125, 211, 252, 0.28);
|
|
396
|
+
background: rgba(125, 211, 252, 0.08);
|
|
397
|
+
color: var(--accent);
|
|
398
|
+
font-size: 11px;
|
|
399
|
+
letter-spacing: 0.06em;
|
|
400
|
+
text-transform: uppercase;
|
|
401
|
+
}
|
|
327
402
|
.eyebrow {
|
|
328
403
|
color: var(--accent);
|
|
329
404
|
text-transform: uppercase;
|
|
330
405
|
letter-spacing: 0.18em;
|
|
331
|
-
font-size:
|
|
332
|
-
margin-bottom:
|
|
406
|
+
font-size: 10px;
|
|
407
|
+
margin-bottom: 10px;
|
|
333
408
|
}
|
|
334
409
|
h1, h2, h3, h4 { margin: 0; }
|
|
335
|
-
h1 { font-size: clamp(
|
|
410
|
+
h1 { font-size: clamp(24px, 4vw, 38px); line-height: 1.05; }
|
|
336
411
|
.hero p {
|
|
337
|
-
margin:
|
|
412
|
+
margin: 10px 0 0;
|
|
338
413
|
color: var(--muted);
|
|
339
|
-
max-width:
|
|
340
|
-
font-size:
|
|
414
|
+
max-width: 640px;
|
|
415
|
+
font-size: 14px;
|
|
341
416
|
line-height: 1.6;
|
|
342
417
|
}
|
|
343
|
-
.summary-grid
|
|
418
|
+
.summary-grid {
|
|
344
419
|
display: grid;
|
|
345
420
|
gap: 16px;
|
|
346
|
-
margin-top:
|
|
421
|
+
margin-top: 18px;
|
|
347
422
|
}
|
|
348
423
|
.summary-grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
|
|
349
|
-
.
|
|
350
|
-
.summary-card, .mode-card, .section-shell, .test-card {
|
|
424
|
+
.summary-card, .section-shell, .test-card {
|
|
351
425
|
border: 1px solid var(--panel-border);
|
|
352
426
|
border-radius: 20px;
|
|
353
427
|
background: var(--panel);
|
|
354
428
|
}
|
|
355
|
-
.summary-card
|
|
429
|
+
.summary-card {
|
|
356
430
|
padding: 18px;
|
|
357
431
|
}
|
|
358
432
|
.summary-label {
|
|
@@ -377,27 +451,21 @@ const buildHtml = (tests, summary, extraArtifacts) => {
|
|
|
377
451
|
color: var(--muted);
|
|
378
452
|
line-height: 1.6;
|
|
379
453
|
}
|
|
380
|
-
.diagram {
|
|
381
|
-
margin-top: 18px;
|
|
382
|
-
padding: 18px;
|
|
383
|
-
border-radius: 16px;
|
|
384
|
-
background: rgba(125, 211, 252, 0.06);
|
|
385
|
-
border: 1px dashed rgba(125, 211, 252, 0.3);
|
|
386
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
387
|
-
white-space: pre-line;
|
|
388
|
-
color: #d7e2f0;
|
|
389
|
-
}
|
|
390
|
-
.mode-card h3 { font-size: 18px; }
|
|
391
|
-
.mode-card p { color: var(--muted); line-height: 1.6; }
|
|
392
454
|
.test-card {
|
|
393
|
-
padding: 20px;
|
|
394
455
|
margin-top: 18px;
|
|
456
|
+
overflow: hidden;
|
|
395
457
|
}
|
|
396
|
-
.test-
|
|
458
|
+
.test-summary {
|
|
397
459
|
display: flex;
|
|
398
460
|
justify-content: space-between;
|
|
399
461
|
gap: 20px;
|
|
400
462
|
align-items: flex-start;
|
|
463
|
+
list-style: none;
|
|
464
|
+
padding: 20px;
|
|
465
|
+
cursor: pointer;
|
|
466
|
+
}
|
|
467
|
+
.test-summary::-webkit-details-marker {
|
|
468
|
+
display: none;
|
|
401
469
|
}
|
|
402
470
|
.status-pill {
|
|
403
471
|
display: inline-flex;
|
|
@@ -417,9 +485,10 @@ const buildHtml = (tests, summary, extraArtifacts) => {
|
|
|
417
485
|
gap: 6px;
|
|
418
486
|
color: var(--muted);
|
|
419
487
|
font-size: 14px;
|
|
488
|
+
text-align: right;
|
|
420
489
|
}
|
|
421
490
|
.panel {
|
|
422
|
-
margin
|
|
491
|
+
margin: 0 20px 18px;
|
|
423
492
|
padding: 16px;
|
|
424
493
|
background: rgba(13, 17, 23, 0.74);
|
|
425
494
|
border: 1px solid rgba(39, 48, 66, 0.9);
|
|
@@ -480,13 +549,27 @@ const buildHtml = (tests, summary, extraArtifacts) => {
|
|
|
480
549
|
border-radius: 14px;
|
|
481
550
|
padding: 16px;
|
|
482
551
|
}
|
|
552
|
+
.failed-list-head {
|
|
553
|
+
display: flex;
|
|
554
|
+
justify-content: space-between;
|
|
555
|
+
gap: 12px;
|
|
556
|
+
align-items: center;
|
|
557
|
+
}
|
|
558
|
+
.failed-count {
|
|
559
|
+
color: var(--muted);
|
|
560
|
+
font-size: 14px;
|
|
561
|
+
}
|
|
483
562
|
footer {
|
|
484
563
|
margin-top: 28px;
|
|
485
564
|
color: var(--muted);
|
|
486
565
|
font-size: 14px;
|
|
487
566
|
}
|
|
488
567
|
@media (max-width: 720px) {
|
|
489
|
-
.
|
|
568
|
+
.hero-badge {
|
|
569
|
+
position: static;
|
|
570
|
+
margin-bottom: 12px;
|
|
571
|
+
}
|
|
572
|
+
.test-summary { flex-direction: column; }
|
|
490
573
|
.meta-stack { min-width: 0; }
|
|
491
574
|
}
|
|
492
575
|
</style>
|
|
@@ -494,8 +577,9 @@ const buildHtml = (tests, summary, extraArtifacts) => {
|
|
|
494
577
|
<body>
|
|
495
578
|
<div class="page">
|
|
496
579
|
<header class="hero">
|
|
580
|
+
<a class="hero-badge" href="${SENTINEL_URL}" target="_blank" rel="noreferrer">Powered by sentinelqa.com</a>
|
|
497
581
|
<div class="eyebrow">Playwright Reporter for CI Debugging</div>
|
|
498
|
-
<h1>
|
|
582
|
+
<h1>Playwright Reporter</h1>
|
|
499
583
|
<p>
|
|
500
584
|
A Playwright reporter that collects traces, screenshots, videos, and logs into
|
|
501
585
|
a single debugging report. Designed to make CI failures easier to diagnose.
|
|
@@ -521,33 +605,10 @@ const buildHtml = (tests, summary, extraArtifacts) => {
|
|
|
521
605
|
</header>
|
|
522
606
|
|
|
523
607
|
<section class="section-shell">
|
|
524
|
-
<
|
|
525
|
-
|
|
526
|
-
<div class="
|
|
527
|
-
<h3>Local Mode</h3>
|
|
528
|
-
<p>Generates debugging reports locally with traces, screenshots, videos, and logs copied into this report folder.</p>
|
|
529
|
-
</div>
|
|
530
|
-
<div class="mode-card">
|
|
531
|
-
<h3>Cloud Mode</h3>
|
|
532
|
-
<p>Uploads CI runs to Sentinel for team debugging, CI history, shareable links, and AI-generated failure summaries.</p>
|
|
533
|
-
</div>
|
|
608
|
+
<div class="failed-list-head">
|
|
609
|
+
<h2>Failed Tests</h2>
|
|
610
|
+
<div class="failed-count">${failedTests.length} failed</div>
|
|
534
611
|
</div>
|
|
535
|
-
</section>
|
|
536
|
-
|
|
537
|
-
<section class="section-shell">
|
|
538
|
-
<h2>Flow</h2>
|
|
539
|
-
<p>This reporter keeps the setup local-first and only adds Sentinel Cloud when a token is configured.</p>
|
|
540
|
-
<div class="diagram">Playwright CI
|
|
541
|
-
↓
|
|
542
|
-
Sentinel Reporter
|
|
543
|
-
↓
|
|
544
|
-
Artifacts Collected
|
|
545
|
-
↓
|
|
546
|
-
Local Debug Page OR Sentinel Cloud</div>
|
|
547
|
-
</section>
|
|
548
|
-
|
|
549
|
-
<section class="section-shell">
|
|
550
|
-
<h2>Failed Tests</h2>
|
|
551
612
|
${failedTests.length > 0
|
|
552
613
|
? failedTests.map((test) => renderTestCard(test)).join("\n")
|
|
553
614
|
: `<div class="empty-state">No failed tests were found in this run. The local report still includes collected artifacts below.</div>`}
|
|
@@ -559,6 +620,19 @@ Local Debug Page OR Sentinel Cloud</div>
|
|
|
559
620
|
${renderAdditionalArtifacts(extraArtifacts)}
|
|
560
621
|
</section>
|
|
561
622
|
|
|
623
|
+
<section class="section-shell">
|
|
624
|
+
<h2>Optional: Sentinel Cloud</h2>
|
|
625
|
+
<div class="artifact-list">
|
|
626
|
+
<div class="artifact-link">Upload runs to Sentinel Cloud for:</div>
|
|
627
|
+
<div class="artifact-link">• CI history</div>
|
|
628
|
+
<div class="artifact-link">• shareable run links</div>
|
|
629
|
+
<div class="artifact-link">• AI failure summaries</div>
|
|
630
|
+
<div class="artifact-link">
|
|
631
|
+
<a href="${SENTINEL_URL}" target="_blank" rel="noreferrer">More on sentinelqa.com</a>
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
</section>
|
|
635
|
+
|
|
562
636
|
<footer>
|
|
563
637
|
Generated by <a href="${SENTINEL_URL}" target="_blank" rel="noreferrer">Sentinel Playwright Reporter</a>.
|
|
564
638
|
</footer>
|
|
@@ -586,7 +660,8 @@ function generateLocalDebugReport(options) {
|
|
|
586
660
|
]);
|
|
587
661
|
const reportJsonRaw = fs_1.default.readFileSync(options.playwrightJsonPath, "utf8");
|
|
588
662
|
const reportJson = JSON.parse(reportJsonRaw);
|
|
589
|
-
const
|
|
663
|
+
const reportRoot = { suites: reportJson?.suites || [] };
|
|
664
|
+
const tests = collectTests(reportRoot);
|
|
590
665
|
const testsById = new Map(tests.map((test) => [test.id, test]));
|
|
591
666
|
const claimedSourcePaths = new Set();
|
|
592
667
|
const attachArtifactToTest = (sourcePath, testId) => {
|
|
@@ -600,34 +675,17 @@ function generateLocalDebugReport(options) {
|
|
|
600
675
|
return;
|
|
601
676
|
}
|
|
602
677
|
};
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
for (const
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
titlePath.join(" > ")
|
|
612
|
-
].join("::");
|
|
613
|
-
const resultList = Array.isArray(test.results) ? test.results : [];
|
|
614
|
-
for (const result of resultList) {
|
|
615
|
-
const attachments = Array.isArray(result?.attachments) ? result.attachments : [];
|
|
616
|
-
for (const attachment of attachments) {
|
|
617
|
-
const resolvedPath = resolveExistingFile(attachment?.path, baseDirs);
|
|
618
|
-
if (!resolvedPath)
|
|
619
|
-
continue;
|
|
620
|
-
attachArtifactToTest(resolvedPath, testId);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
678
|
+
for (const testRef of collectTestRefs(reportRoot)) {
|
|
679
|
+
for (const result of testRef.resultList) {
|
|
680
|
+
const attachments = Array.isArray(result?.attachments) ? result.attachments : [];
|
|
681
|
+
for (const attachment of attachments) {
|
|
682
|
+
const resolvedPath = resolveExistingFile(attachment?.path, baseDirs);
|
|
683
|
+
if (!resolvedPath)
|
|
684
|
+
continue;
|
|
685
|
+
attachArtifactToTest(resolvedPath, testRef.id);
|
|
623
686
|
}
|
|
624
687
|
}
|
|
625
|
-
|
|
626
|
-
for (const suite of node.suites)
|
|
627
|
-
walkSuites(suite, nextTitles);
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
walkSuites({ suites: reportJson?.suites || [] });
|
|
688
|
+
}
|
|
631
689
|
const extraArtifacts = [];
|
|
632
690
|
for (const sourceDir of sourceDirs) {
|
|
633
691
|
for (const filePath of listFilesRecursive(sourceDir)) {
|