@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.
Files changed (2) hide show
  1. package/dist/localReport.js +175 -117
  2. package/package.json +1 -1
@@ -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, "&lt;")
@@ -141,41 +152,89 @@ const copyArtifact = (sourcePath, kind, reportDir, usedRelativePaths, testId) =>
141
152
  testId
142
153
  };
143
154
  };
144
- const flattenTests = (node, parentTitles = []) => {
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 currentTests = Array.isArray(node?.tests)
147
- ? node.tests.map((test) => {
148
- const titlePath = [...nextTitles, test.title].filter(Boolean);
149
- const results = Array.isArray(test.results) ? test.results : [];
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.location?.file || "unknown",
159
- test.projectName || "default",
212
+ test?.location?.file || "unknown",
213
+ test?.projectName || "default",
160
214
  titlePath.join(" > ")
161
215
  ].join("::");
162
- return {
163
- id,
164
- title: test.title || "Untitled test",
165
- titlePath,
166
- file: test.location?.file || null,
167
- projectName: test.projectName || null,
168
- status: test.status || lastResult?.status || "unknown",
169
- duration,
170
- errors,
171
- artifacts: []
172
- };
173
- })
174
- : [];
175
- const childTests = Array.isArray(node?.suites)
176
- ? node.suites.flatMap((suite) => flattenTests(suite, nextTitles))
177
- : [];
178
- return [...currentTests, ...childTests];
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
- <section class="test-card">
239
- <div class="test-head">
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
- </div>
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
- </section>
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
- padding: 28px;
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: 12px;
332
- margin-bottom: 12px;
406
+ font-size: 10px;
407
+ margin-bottom: 10px;
333
408
  }
334
409
  h1, h2, h3, h4 { margin: 0; }
335
- h1 { font-size: clamp(32px, 5vw, 52px); line-height: 1; }
410
+ h1 { font-size: clamp(24px, 4vw, 38px); line-height: 1.05; }
336
411
  .hero p {
337
- margin: 14px 0 0;
412
+ margin: 10px 0 0;
338
413
  color: var(--muted);
339
- max-width: 760px;
340
- font-size: 16px;
414
+ max-width: 640px;
415
+ font-size: 14px;
341
416
  line-height: 1.6;
342
417
  }
343
- .summary-grid, .mode-grid {
418
+ .summary-grid {
344
419
  display: grid;
345
420
  gap: 16px;
346
- margin-top: 24px;
421
+ margin-top: 18px;
347
422
  }
348
423
  .summary-grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
349
- .mode-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
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, .mode-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-head {
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-top: 18px;
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
- .test-head { flex-direction: column; }
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>Sentinel Playwright Reporter</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
- <h2>Modes</h2>
525
- <div class="mode-grid">
526
- <div class="mode-card">
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 tests = flattenTests({ suites: reportJson?.suites || [] });
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 walkSuites = (node, parentTitles = []) => {
604
- const nextTitles = node?.title ? [...parentTitles, node.title] : parentTitles;
605
- if (Array.isArray(node?.tests)) {
606
- for (const test of node.tests) {
607
- const titlePath = [...nextTitles, test.title].filter(Boolean);
608
- const testId = [
609
- test.location?.file || "unknown",
610
- test.projectName || "default",
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
- if (Array.isArray(node?.suites)) {
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)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentinelqa/playwright-reporter",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "private": false,
5
5
  "description": "Playwright reporter for CI debugging with optional Sentinel cloud dashboards",
6
6
  "license": "MIT",