@sentinelqa/playwright-reporter 0.1.29 → 0.1.32

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 CHANGED
@@ -1,38 +1,53 @@
1
1
  # Playwright Reporter
2
2
 
3
+ After every failed run, Sentinel prints a shareable debugging link:
4
+
5
+ 👉 https://sentinelqa.com/run/abc123
6
+
7
+ Open it to inspect failures instantly or share it in Slack, PRs, or GitHub issues.
8
+
3
9
  [![npm](https://img.shields.io/npm/v/@sentinelqa/playwright-reporter)](https://www.npmjs.com/package/@sentinelqa/playwright-reporter)
4
10
  [![downloads](https://img.shields.io/npm/dm/@sentinelqa/playwright-reporter)](https://www.npmjs.com/package/@sentinelqa/playwright-reporter)
5
11
  [![license](https://img.shields.io/npm/l/@sentinelqa/playwright-reporter)](./LICENSE)
6
12
 
7
- A Playwright reporter that aggregates traces, screenshots, videos, and logs
8
- into a single debugging report for failed tests.
13
+ From failed CI run root cause in seconds.
14
+ Get a shareable Playwright debugging link with traces, screenshots, and failure context — no setup required.
9
15
 
10
- Works locally out of the box with no account required.
16
+ Works with no account or API key required.
11
17
 
12
- Optionally upload runs to Sentinel Cloud for CI history and AI failure analysis.
18
+ Use it to get a shareable hosted run link from CI or local development, then upgrade to Sentinel Cloud for richer history and intelligence.
13
19
 
14
20
  ![Sentinel Report Example](./docs/screenshot.png)
21
+ ![Run-to-Run Diff](./docs/run_diff.png)
22
+ ![CLI Quick Diagnosis](./docs/CLI.png)
15
23
 
16
24
  ## Features
17
25
 
18
- - Aggregates Playwright traces, screenshots, videos, and logs
19
- - Generates a local HTML debugging report
20
- - Prints a deterministic quick diagnosis in the terminal after failed runs
21
- - Adds a failure digest to the local HTML report
22
- - Groups similar failures so repeated symptoms are easy to spot
23
- - Lets you copy debug summaries for Slack, Jira, and GitHub issues
24
- - Compares the current run to the previous run on the same branch
26
+ - Free hosted debugging links by default, with no account or API key required
27
+ - Public run page that opens on unified failures across the run
28
+ - Within-run failure grouping so repeated failures collapse into one issue
29
+ - Public failure pages with screenshots, evidence, parsed errors and light summaries
30
+ - Copyable share actions for Slack, PRs, and debugging handoff
31
+ - Deterministic quick diagnosis in the terminal after failed runs
32
+ - Playwright traces, screenshots, videos and logs uploaded automatically
33
+ - 48-hour public share links on the free hosted flow
25
34
  - Works with existing Playwright reporter setup
26
- - Optional Sentinel Cloud integration
27
- - CI run history and AI debugging summaries in cloud mode
35
+ - Optional live failure capture for richer Sentinel Cloud analysis
36
+ - CI run history, retention, and deeper AI debugging in Sentinel Cloud
28
37
 
29
38
  ## Why this exists
30
39
 
31
- Debugging Playwright CI failures often means downloading traces,
32
- screenshots, and videos separately.
40
+ Debugging Playwright failures usually means downloading traces, screenshots, and logs separately from CI.
41
+
42
+ Reporter uploads those artifacts into a single hosted Sentinel run page so you can open one link, inspect failures fast, and share that link with the rest of the team.
33
43
 
34
- Reporter aggregates everything into one debugging report
35
- so you can quickly understand what failed.
44
+ ## Why teams use the free version
45
+
46
+ - Drop one wrapper into `playwright.config.ts` and keep running `npx playwright test`
47
+ - Get a hosted Sentinel debugging link automatically on failed runs
48
+ - Share one public URL in Slack, PRs, or GitHub issues instead of passing around raw CI artifacts
49
+ - See unified failures, grouped failure patterns, screenshots, and evidence in one place
50
+ - Let teammates inspect the failure without needing your CI system or local machine
36
51
 
37
52
  ## Requirements
38
53
 
@@ -45,8 +60,8 @@ so you can quickly understand what failed.
45
60
 
46
61
  - best for free and local users
47
62
  - zero-friction setup
48
- - local HTML report works exactly as today
49
- - cloud upload works when configured
63
+ - hosted Sentinel report link is generated automatically
64
+ - no `SENTINEL_TOKEN` required
50
65
  - AI summaries use trace and reporter evidence, but are less precise than live page capture
51
66
 
52
67
  Install:
@@ -85,58 +100,59 @@ Run your Playwright tests:
85
100
  npx playwright test
86
101
  ```
87
102
 
88
- If tests fail and `SENTINEL_TOKEN` is not set, Sentinel generates:
103
+ If tests fail, Sentinel uploads a hosted debugging report and prints the shareable link in the terminal.
104
+
105
+ Open the hosted report to inspect:
89
106
 
90
- - `sentinel-report/index.html`
91
- - `sentinel-debug.html`
107
+ - failed tests across jobs
108
+ - within-run grouped failures
109
+ - screenshots and videos
110
+ - trace links
111
+ - parsed failure details
112
+ - light summaries
113
+ - shareable public debugging page
92
114
 
93
- Open the report to inspect:
115
+ The free hosted public flow is designed for distribution:
94
116
 
95
- - failure digest
96
- - similar failure groups
97
- - run-to-run diff
98
- - failed tests
99
- - screenshots
100
- - videos
101
- - trace files
102
- - logs
117
+ - one shareable debugging link per run
118
+ - public read-only pages
119
+ - fast enough to use in CI comments and Slack threads
120
+ - clear upgrade path into a full Sentinel workspace when teams want history, retention, and deeper analysis
103
121
 
104
122
  ## Modes
105
123
 
106
- ### Local mode
124
+ ### Free hosted mode
107
125
 
108
- If `SENTINEL_TOKEN` is not set, the reporter generates a local HTML debugging report.
126
+ If `SENTINEL_TOKEN` is not set, the reporter uploads the run to a hosted public Sentinel report and prints the shareable URL.
109
127
 
110
- ### Cloud mode
128
+ This free public flow includes:
111
129
 
112
- If `SENTINEL_TOKEN` is set in CI, the reporter uploads the run to Sentinel instead of generating the local HTML report.
130
+ - hosted run page
131
+ - hosted failure pages
132
+ - grouped failures inside the run
133
+ - light summaries
134
+ - copy/share actions
135
+ - 48-hour share links
113
136
 
114
- ```bash
115
- SENTINEL_TOKEN=your_project_ingest_token npx playwright test
116
- ```
117
-
118
- For intentional uploads outside CI, also set `SENTINEL_UPLOAD_LOCAL=1` and provide the usual commit and run metadata expected by the uploader.
137
+ ### Workspace mode
119
138
 
120
- Example:
139
+ If `SENTINEL_TOKEN` is set, the reporter uploads into your Sentinel workspace instead of the free hosted public flow.
121
140
 
122
141
  ```bash
123
- SENTINEL_TOKEN=your_project_ingest_token \
124
- SENTINEL_UPLOAD_LOCAL=1 \
125
- GITHUB_SHA=abc123 \
126
- GITHUB_REF_NAME=main \
127
- GITHUB_RUN_ID=local-dev \
128
- npx playwright test
142
+ SENTINEL_TOKEN=your_project_ingest_token npx playwright test
129
143
  ```
130
144
 
145
+ For local runs outside CI, Sentinel will use your local git metadata automatically when available.
146
+
131
147
  ## What `withSentinel()` does
132
148
 
133
149
  - Preserves your existing reporter configuration
134
150
  - Injects a Playwright JSON reporter if one is missing
135
- - Reuses your existing Playwright HTML reporter path when configured
136
151
  - Sets sensible artifact defaults:
137
152
  - trace: `retain-on-failure`
138
153
  - screenshot: `only-on-failure`
139
154
  - video: `retain-on-failure`
155
+ - Uploads the run to hosted Sentinel at the end of the test run
140
156
 
141
157
  ## Recommended Cloud Setup
142
158
 
@@ -173,7 +189,7 @@ Use this cloud setup when you want:
173
189
  - richer DOM-aware diagnosis
174
190
  - more reliable code patches grounded in real page state
175
191
 
176
- Free and local-only users do not need this. The standard `withSentinel()` setup remains the simplest path and continues to generate the local report the same way as before.
192
+ Free and local-only users do not need this. The standard `withSentinel()` setup remains the simplest path and will upload a hosted Sentinel report automatically.
177
193
 
178
194
  ## Options
179
195
 
@@ -185,9 +201,6 @@ withSentinel(config, {
185
201
  testResultsDir: "test-results",
186
202
  artifactDirs: ["tmp/extra-artifacts"],
187
203
  verbose: true,
188
- localReportDir: "sentinel-report",
189
- localReportFileName: "index.html",
190
- localRedirectFileName: "sentinel-debug.html",
191
204
  });
192
205
  ```
193
206
 
@@ -200,6 +213,10 @@ Sentinel Cloud adds:
200
213
  - AI-generated failure summaries
201
214
  - flaky test detection
202
215
  - shareable run links
216
+ - longer retention
217
+ - compare against previous runs
218
+ - recurring failure history
219
+ - richer fix suggestions and team workflows
203
220
 
204
221
  Free for up to 100 CI runs per month.
205
222
  Create an account at [sentinelqa.com](https://sentinelqa.com).
package/dist/index.d.ts CHANGED
@@ -23,9 +23,6 @@ export type SentinelPlaywrightOptions = {
23
23
  testResultsDir?: string;
24
24
  artifactDirs?: string[];
25
25
  verbose?: boolean;
26
- localReportDir?: string;
27
- localReportFileName?: string;
28
- localRedirectFileName?: string;
29
26
  };
30
27
  export declare function withSentinel(config: PlaywrightConfig, options?: SentinelPlaywrightOptions): PlaywrightConfig;
31
28
  export declare function resolveSentinelPaths(config: PlaywrightConfig, options?: SentinelPlaywrightOptions): SentinelResolvedPaths;
package/dist/index.js CHANGED
@@ -105,10 +105,7 @@ function withSentinel(config, options = {}) {
105
105
  playwrightReportDir,
106
106
  testResultsDir,
107
107
  artifactDirs,
108
- verbose: options.verbose ?? false,
109
- localReportDir: options.localReportDir,
110
- localReportFileName: options.localReportFileName,
111
- localRedirectFileName: options.localRedirectFileName
108
+ verbose: options.verbose ?? false
112
109
  };
113
110
  const sentinelIndex = reporters.findIndex((entry) => getReporterName(entry) === sentinelReporterPath);
114
111
  if (sentinelIndex !== -1) {
@@ -362,6 +362,20 @@ const groupSimilarFailures = (tests) => {
362
362
  .filter((group) => group.tests.length > 1)
363
363
  .sort((a, b) => b.tests.length - a.tests.length);
364
364
  };
365
+ const groupFailureDigest = (tests) => {
366
+ const groups = new Map();
367
+ for (const test of getFailureTests(tests)) {
368
+ const summary = test.diagnosis
369
+ ? (0, quickDiagnosis_1.describeFailure)(test.diagnosis)
370
+ : (test.errors[0]?.split(/\r?\n/)[0]?.trim() || "Open the failure details to inspect the exact Playwright error.");
371
+ const key = summary;
372
+ if (!groups.has(key)) {
373
+ groups.set(key, { key, summary, tests: [] });
374
+ }
375
+ groups.get(key).tests.push(test);
376
+ }
377
+ return Array.from(groups.values()).sort((a, b) => b.tests.length - a.tests.length);
378
+ };
365
379
  const buildRunSnapshot = (tests, summary) => ({
366
380
  generatedAt: new Date().toISOString(),
367
381
  branch: getCurrentBranch(),
@@ -601,24 +615,30 @@ const renderFailureDigest = (tests) => {
601
615
  if (!failedTests.length) {
602
616
  return `<div class="empty-state">No failed tests were detected in this run.</div>`;
603
617
  }
618
+ const digestGroups = groupFailureDigest(tests);
604
619
  return `
605
620
  <div class="digest-grid">
606
- ${failedTests
607
- .map((test) => {
608
- const diagnosis = test.diagnosis;
609
- const title = escapeHtml(test.title);
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."));
613
- const debugSummary = escapeHtml(diagnosis
614
- ? (0, quickDiagnosis_1.buildDebugSummary)(diagnosis)
615
- : `Test: ${test.title}\nDiagnosis: Review trace and error details in the expanded card.`);
621
+ ${digestGroups
622
+ .map((group) => {
623
+ const primary = group.tests[0];
624
+ const diagnosis = primary.diagnosis;
625
+ const debugSummary = escapeHtml(group.tests.length > 1
626
+ ? [
627
+ `Grouped failure summary: ${group.summary}`,
628
+ ...group.tests.map((test) => `- ${test.title}`)
629
+ ].join("\n")
630
+ : diagnosis
631
+ ? (0, quickDiagnosis_1.buildDebugSummary)(diagnosis)
632
+ : `Test: ${primary.title}\nDiagnosis: Review trace and error details in the expanded card.`);
633
+ const uniqueLocators = Array.from(new Set(group.tests.map((test) => test.diagnosis?.locator).filter(Boolean)));
616
634
  return `
617
635
  <article class="digest-card">
618
636
  <div class="digest-head">
619
637
  <div>
620
- <span class="artifact-kind">${escapeHtml(test.status)}</span>
621
- <h3>${title}</h3>
638
+ <span class="artifact-kind">${escapeHtml(primary.status)}</span>
639
+ <h3>${group.tests.length > 1
640
+ ? `${group.tests.length} tests share this failure`
641
+ : escapeHtml(primary.title)}</h3>
622
642
  </div>
623
643
  <button
624
644
  type="button"
@@ -629,12 +649,20 @@ const renderFailureDigest = (tests) => {
629
649
  Copy summary
630
650
  </button>
631
651
  </div>
632
- <p>${summary}</p>
652
+ <p>${escapeHtml(group.summary)}</p>
633
653
  <div class="fact-row">
634
- ${diagnosis?.locator ? `<span class="fact-chip">Locator: ${escapeHtml(diagnosis.locator)}</span>` : ""}
635
- ${diagnosis?.expected ? `<span class="fact-chip">Expected: ${escapeHtml(diagnosis.expected)}</span>` : ""}
636
- ${diagnosis?.received ? `<span class="fact-chip">Observed: ${escapeHtml(diagnosis.received)}</span>` : ""}
654
+ ${diagnosis?.expected && diagnosis?.received
655
+ ? `<span class="fact-chip">Expected: ${escapeHtml(diagnosis.expected)}</span><span class="fact-chip">Observed: ${escapeHtml(diagnosis.received)}</span>`
656
+ : ""}
657
+ ${uniqueLocators.length === 1
658
+ ? `<span class="fact-chip">Locator: ${escapeHtml(uniqueLocators[0])}</span>`
659
+ : uniqueLocators.length > 1
660
+ ? `<span class="fact-chip">${uniqueLocators.length} locators involved</span>`
661
+ : ""}
637
662
  </div>
663
+ ${group.tests.length > 1
664
+ ? `<ul class="group-list">${group.tests.map((test) => `<li>${escapeHtml(test.title)}</li>`).join("\n")}</ul>`
665
+ : ""}
638
666
  </article>
639
667
  `;
640
668
  })
@@ -28,11 +28,11 @@ const toMessage = (result) => {
28
28
  };
29
29
  const classifySignal = (message) => {
30
30
  const lower = message.toLowerCase();
31
- if (/timeout|timed out|waiting for/.test(lower))
32
- return "timeout";
33
31
  if (/expected substring|expected string|received string|tohavetext|tocontaintext/.test(lower)) {
34
32
  return "assertion_mismatch";
35
33
  }
34
+ if (/timeout|timed out|waiting for/.test(lower))
35
+ return "timeout";
36
36
  if (/resolved to 0 elements|locator.*not found|never appeared|strict mode violation/.test(lower)) {
37
37
  return "locator_not_found";
38
38
  }
@@ -5,9 +5,6 @@ type ReporterOptions = {
5
5
  testResultsDir: string;
6
6
  artifactDirs?: string[];
7
7
  verbose?: boolean;
8
- localReportDir?: string;
9
- localReportFileName?: string;
10
- localRedirectFileName?: string;
11
8
  };
12
9
  declare class SentinelReporter {
13
10
  private failedCount;
@@ -16,7 +13,6 @@ declare class SentinelReporter {
16
13
  constructor(options: ReporterOptions);
17
14
  onBegin(config: any, suite: any): void;
18
15
  onTestEnd(test: any, result: any): Promise<void>;
19
- private printLocalReport;
20
16
  onEnd(): Promise<void>;
21
17
  }
22
18
  export = SentinelReporter;
package/dist/reporter.js CHANGED
@@ -1,33 +1,16 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- const path_1 = __importDefault(require("path"));
6
- const url_1 = require("url");
7
2
  const node_1 = require("@sentinelqa/uploader/node");
8
3
  const env_1 = require("./env");
9
- const localReport_1 = require("./localReport");
10
4
  const quickDiagnosis_1 = require("./quickDiagnosis");
11
5
  const { sentinelCaptureFailureContextFromReporter } = require("@sentinelqa/uploader/playwright");
12
- const pluralize = (count, singular, plural) => {
13
- return count === 1 ? singular : plural;
14
- };
15
- const formatTerminalLink = (label, target) => {
16
- if (!process.stdout.isTTY)
17
- return label;
18
- return `\u001B]8;;${target}\u0007${label}\u001B]8;;\u0007`;
19
- };
20
6
  const colorize = (value, code) => {
21
7
  if (!process.stdout.isTTY)
22
8
  return value;
23
9
  return `\u001b[${code}m${value}\u001b[0m`;
24
10
  };
25
- const bold = (value) => colorize(value, "1");
26
11
  const green = (value) => colorize(value, "32");
27
- const cyan = (value) => colorize(value, "36");
28
12
  const yellow = (value) => colorize(value, "33");
29
13
  const dim = (value) => colorize(value, "2");
30
- const magenta = (value) => colorize(value, "35");
31
14
  class SentinelReporter {
32
15
  constructor(options) {
33
16
  this.failedCount = 0;
@@ -59,75 +42,21 @@ class SentinelReporter {
59
42
  this.failedCount += 1;
60
43
  }
61
44
  }
62
- printLocalReport(localReport) {
63
- const localReportPath = localReport.htmlPath;
64
- const relativeReportPath = path_1.default
65
- .relative(process.cwd(), localReportPath)
66
- .replace(/\\/g, "/");
67
- const displayPath = relativeReportPath.startsWith(".")
68
- ? relativeReportPath
69
- : `./${relativeReportPath}`;
70
- const openCommand = `open ${displayPath}`;
45
+ async onEnd() {
71
46
  console.log("");
72
47
  console.log(green("✔ Artifacts collected"));
73
- console.log(green("✔ Sentinel HTML debugging report created"));
74
- console.log("");
75
- console.log(bold("Report"));
76
- console.log(` ${cyan(formatTerminalLink(displayPath, (0, url_1.pathToFileURL)(localReportPath).href))}`);
77
- console.log("");
78
- console.log(bold("Open"));
79
- console.log(` ${cyan(openCommand)}`);
80
- console.log("");
81
48
  const quickDiagnosis = (0, quickDiagnosis_1.buildQuickDiagnosis)(this.options.playwrightJsonPath);
82
49
  if (quickDiagnosis?.lines.length) {
50
+ console.log("");
83
51
  console.log(yellow("Quick diagnosis"));
84
52
  for (const line of quickDiagnosis.lines) {
85
53
  console.log(` ${dim(line)}`);
86
54
  }
87
- console.log("");
88
- }
89
- if (localReport.runDiff) {
90
- console.log(yellow("Run-to-run diff"));
91
- console.log(` ${dim(`New failures: ${localReport.runDiff.newFailures.length}`)}`);
92
- console.log(` ${dim(`Fixed since last run: ${localReport.runDiff.fixedTests.length}`)}`);
93
- console.log(` ${dim(`Still failing: ${localReport.runDiff.stillFailing.length}`)}`);
94
- console.log("");
95
- }
96
- console.log(yellow("Tip"));
97
- console.log(` ${dim("Want full AI analysis, shareable run links, and CI history?")}`);
98
- console.log(` ${dim("Try Sentinel Cloud Beta free:")} ${cyan(formatTerminalLink("https://sentinelqa.com", "https://sentinelqa.com"))}`);
99
- console.log("");
100
- console.log(` ${magenta("★ If this reporter helped you debug faster,")}`);
101
- console.log(` ${dim("consider starring the project:")}`);
102
- console.log(` ${cyan(formatTerminalLink("https://github.com/adnangradascevic/playwright-reporter", "https://github.com/adnangradascevic/playwright-reporter"))}`);
103
- }
104
- async onEnd() {
105
- const hasSentinelToken = Boolean(process.env.SENTINEL_TOKEN);
106
- const hasCiEnv = (0, node_1.hasSupportedCiEnv)(process.env);
107
- const localUploadEnabled = (0, node_1.isLocalUploadEnabled)(process.env);
108
- if (!hasSentinelToken || (!hasCiEnv && !localUploadEnabled)) {
109
- const localReport = (0, localReport_1.generateLocalDebugReport)({
110
- playwrightJsonPath: this.options.playwrightJsonPath,
111
- playwrightReportDir: this.options.playwrightReportDir,
112
- testResultsDir: this.options.testResultsDir,
113
- artifactDirs: this.options.artifactDirs || [],
114
- reportDir: this.options.localReportDir,
115
- reportFileName: this.options.localReportFileName,
116
- redirectFileName: this.options.localRedirectFileName
117
- });
118
- this.printLocalReport(localReport);
119
- console.log("");
120
- if (hasSentinelToken && !hasCiEnv && !localUploadEnabled) {
121
- console.log("Sentinel upload skipped for this local run.");
122
- console.log("To upload local runs, set SENTINEL_UPLOAD_LOCAL=1 and provide the required CI metadata.");
123
- console.log("");
124
- }
125
- return;
126
55
  }
127
56
  console.log("");
128
- console.log("Uploading failure artifacts to Sentinel...");
57
+ console.log("Uploading hosted debugging report to Sentinel...");
129
58
  console.log("");
130
- const exitCode = await (0, node_1.runSentinelUpload)({
59
+ const upload = await (0, node_1.runSentinelUpload)({
131
60
  playwrightJsonPath: this.options.playwrightJsonPath,
132
61
  playwrightReportDir: this.options.playwrightReportDir,
133
62
  testResultsDir: this.options.testResultsDir,
@@ -137,11 +66,19 @@ class SentinelReporter {
137
66
  SENTINEL_REPORTER_PROJECT: this.options.project || undefined
138
67
  }
139
68
  });
140
- if (exitCode !== 0) {
141
- throw new Error(`Sentinel upload failed with exit code ${exitCode}`);
69
+ if (upload.exitCode !== 0) {
70
+ throw new Error(`Sentinel upload failed with exit code ${upload.exitCode}`);
142
71
  }
143
72
  console.log("");
144
- console.log("✔ Uploaded run to Sentinel");
73
+ console.log("✔ Hosted report uploaded to Sentinel");
74
+ if (upload.shareRunUrl || upload.internalRunUrl) {
75
+ console.log("");
76
+ console.log("Sentinel report");
77
+ console.log(` ${upload.shareRunUrl || upload.internalRunUrl}`);
78
+ if (upload.shareLabel) {
79
+ console.log(` ${dim(upload.shareLabel)}`);
80
+ }
81
+ }
145
82
  }
146
83
  }
147
84
  module.exports = SentinelReporter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentinelqa/playwright-reporter",
3
- "version": "0.1.29",
3
+ "version": "0.1.32",
4
4
  "private": false,
5
5
  "description": "Playwright reporter for CI debugging with optional Sentinel cloud dashboards",
6
6
  "license": "MIT",