@sentinelqa/playwright-reporter 0.1.25 → 0.1.26
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 +45 -0
- package/dist/fixtures.d.ts +1 -0
- package/dist/fixtures.js +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -0
- package/dist/quickDiagnosis.d.ts +5 -0
- package/dist/quickDiagnosis.js +118 -0
- package/dist/reporter.d.ts +1 -1
- package/dist/reporter.js +14 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -36,6 +36,14 @@ so you can quickly understand what failed.
|
|
|
36
36
|
|
|
37
37
|
## Quick Start
|
|
38
38
|
|
|
39
|
+
`withSentinel()` is the default setup for everyone:
|
|
40
|
+
|
|
41
|
+
- best for free and local users
|
|
42
|
+
- zero-friction setup
|
|
43
|
+
- local HTML report works exactly as today
|
|
44
|
+
- cloud upload works when configured
|
|
45
|
+
- AI summaries use trace and reporter evidence, but are less precise than live page capture
|
|
46
|
+
|
|
39
47
|
Install:
|
|
40
48
|
|
|
41
49
|
```bash
|
|
@@ -122,6 +130,43 @@ npx playwright test
|
|
|
122
130
|
- screenshot: `only-on-failure`
|
|
123
131
|
- video: `retain-on-failure`
|
|
124
132
|
|
|
133
|
+
## Recommended Cloud Setup
|
|
134
|
+
|
|
135
|
+
If you use Sentinel Cloud and want the best AI summaries and fix suggestions, keep `withSentinel()` in your Playwright config and add the live capture fixture.
|
|
136
|
+
|
|
137
|
+
Why:
|
|
138
|
+
|
|
139
|
+
- `withSentinel()` alone works from reporter and trace data
|
|
140
|
+
- a Playwright reporter does not get the live `page` fixture
|
|
141
|
+
- the live capture fixture lets Sentinel collect richer DOM and code context at the exact failure moment
|
|
142
|
+
- this is required for the highest-quality DOM-aware patches
|
|
143
|
+
|
|
144
|
+
Create one shared test wrapper:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// tests/test.ts
|
|
148
|
+
import { test as base, expect } from "@playwright/test";
|
|
149
|
+
import { attachSentinelFailureCapture } from "@sentinelqa/playwright-reporter/fixtures";
|
|
150
|
+
|
|
151
|
+
export const test = attachSentinelFailureCapture(base);
|
|
152
|
+
export { expect };
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Then import from that file in your specs instead of `@playwright/test`:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { test, expect } from "./test";
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Use this cloud setup when you want:
|
|
162
|
+
|
|
163
|
+
- best AI summaries
|
|
164
|
+
- best fix suggestions
|
|
165
|
+
- richer DOM-aware diagnosis
|
|
166
|
+
- more reliable code patches grounded in real page state
|
|
167
|
+
|
|
168
|
+
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.
|
|
169
|
+
|
|
125
170
|
## Options
|
|
126
171
|
|
|
127
172
|
```ts
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function attachSentinelFailureCapture(baseTest: any): any;
|
package/dist/fixtures.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachSentinelFailureCapture = attachSentinelFailureCapture;
|
|
4
|
+
const { sentinelCaptureFailureContext } = require("@sentinelqa/uploader/playwright");
|
|
5
|
+
function attachSentinelFailureCapture(baseTest) {
|
|
6
|
+
return baseTest.extend({
|
|
7
|
+
_sentinelFailureCapture: [
|
|
8
|
+
async ({ page }, use, testInfo) => {
|
|
9
|
+
await use();
|
|
10
|
+
await sentinelCaptureFailureContext(page, testInfo).catch(() => null);
|
|
11
|
+
},
|
|
12
|
+
{ auto: true }
|
|
13
|
+
]
|
|
14
|
+
});
|
|
15
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -29,4 +29,4 @@ export type SentinelPlaywrightOptions = {
|
|
|
29
29
|
};
|
|
30
30
|
export declare function withSentinel(config: PlaywrightConfig, options?: SentinelPlaywrightOptions): PlaywrightConfig;
|
|
31
31
|
export declare function resolveSentinelPaths(config: PlaywrightConfig, options?: SentinelPlaywrightOptions): SentinelResolvedPaths;
|
|
32
|
-
export {};
|
|
32
|
+
export { attachSentinelFailureCapture } from "./fixtures";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ 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.attachSentinelFailureCapture = void 0;
|
|
6
7
|
exports.withSentinel = withSentinel;
|
|
7
8
|
exports.resolveSentinelPaths = resolveSentinelPaths;
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -142,3 +143,5 @@ function resolveSentinelPaths(config, options = {}) {
|
|
|
142
143
|
artifactDirs
|
|
143
144
|
};
|
|
144
145
|
}
|
|
146
|
+
var fixtures_1 = require("./fixtures");
|
|
147
|
+
Object.defineProperty(exports, "attachSentinelFailureCapture", { enumerable: true, get: function () { return fixtures_1.attachSentinelFailureCapture; } });
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildQuickDiagnosis = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
9
|
+
const toMessage = (result) => {
|
|
10
|
+
const direct = result.error?.message ||
|
|
11
|
+
result.error?.stack ||
|
|
12
|
+
result.error?.value ||
|
|
13
|
+
null;
|
|
14
|
+
if (direct)
|
|
15
|
+
return stripAnsi(String(direct));
|
|
16
|
+
const first = result.errors?.find(Boolean);
|
|
17
|
+
return first ? stripAnsi(String(first.message || first.stack || first.value || "")) : "";
|
|
18
|
+
};
|
|
19
|
+
const classifySignal = (message) => {
|
|
20
|
+
const lower = message.toLowerCase();
|
|
21
|
+
if (/timeout|timed out|waiting for/.test(lower))
|
|
22
|
+
return "timeout";
|
|
23
|
+
if (/expected substring|expected string|received string|tohavetext|tocontaintext/.test(lower)) {
|
|
24
|
+
return "assertion_mismatch";
|
|
25
|
+
}
|
|
26
|
+
if (/resolved to 0 elements|locator.*not found|never appeared|strict mode violation/.test(lower)) {
|
|
27
|
+
return "locator_not_found";
|
|
28
|
+
}
|
|
29
|
+
if (/not visible|not enabled|not stable|intercepts pointer events|not actionable/.test(lower)) {
|
|
30
|
+
return "actionability";
|
|
31
|
+
}
|
|
32
|
+
if (/status\s*[45]\d{2}|net::|failed to fetch|network|request failed/.test(lower)) {
|
|
33
|
+
return "network";
|
|
34
|
+
}
|
|
35
|
+
if (/typeerror|referenceerror|syntaxerror|unhandled/.test(lower))
|
|
36
|
+
return "runtime";
|
|
37
|
+
return "unknown";
|
|
38
|
+
};
|
|
39
|
+
const signalSummary = (signal) => {
|
|
40
|
+
switch (signal) {
|
|
41
|
+
case "timeout":
|
|
42
|
+
return "timeout while waiting for UI or network conditions";
|
|
43
|
+
case "assertion_mismatch":
|
|
44
|
+
return "assertion mismatch between expected and rendered UI state";
|
|
45
|
+
case "locator_not_found":
|
|
46
|
+
return "missing or changed locator";
|
|
47
|
+
case "actionability":
|
|
48
|
+
return "target element was not actionable";
|
|
49
|
+
case "network":
|
|
50
|
+
return "network or API failure";
|
|
51
|
+
case "runtime":
|
|
52
|
+
return "frontend runtime error";
|
|
53
|
+
default:
|
|
54
|
+
return "failure signal could not be classified cleanly";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const flattenFailedCases = (node, titlePath = []) => {
|
|
58
|
+
const currentTitlePath = node.title ? [...titlePath, node.title] : titlePath;
|
|
59
|
+
const failedCases = [];
|
|
60
|
+
for (const test of node.tests || []) {
|
|
61
|
+
const title = [...currentTitlePath, test.title || "Unnamed test"].join(" > ");
|
|
62
|
+
for (const result of test.results || []) {
|
|
63
|
+
if (!["failed", "timedOut", "interrupted"].includes(result.status || ""))
|
|
64
|
+
continue;
|
|
65
|
+
const message = toMessage(result);
|
|
66
|
+
failedCases.push({
|
|
67
|
+
title,
|
|
68
|
+
message,
|
|
69
|
+
signal: classifySignal(message)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const child of node.specs || []) {
|
|
74
|
+
failedCases.push(...flattenFailedCases(child, currentTitlePath));
|
|
75
|
+
}
|
|
76
|
+
for (const child of node.suites || []) {
|
|
77
|
+
failedCases.push(...flattenFailedCases(child, currentTitlePath));
|
|
78
|
+
}
|
|
79
|
+
return failedCases;
|
|
80
|
+
};
|
|
81
|
+
const shortenTitle = (value) => {
|
|
82
|
+
const parts = value.split(" > ").filter(Boolean);
|
|
83
|
+
return parts[parts.length - 1] || value;
|
|
84
|
+
};
|
|
85
|
+
const buildQuickDiagnosis = (playwrightJsonPath) => {
|
|
86
|
+
if (!node_fs_1.default.existsSync(playwrightJsonPath))
|
|
87
|
+
return null;
|
|
88
|
+
try {
|
|
89
|
+
const raw = node_fs_1.default.readFileSync(playwrightJsonPath, "utf8");
|
|
90
|
+
const parsed = JSON.parse(raw);
|
|
91
|
+
const failedCases = flattenFailedCases(parsed);
|
|
92
|
+
if (!failedCases.length)
|
|
93
|
+
return null;
|
|
94
|
+
if (failedCases.length === 1) {
|
|
95
|
+
const failed = failedCases[0];
|
|
96
|
+
return {
|
|
97
|
+
lines: [
|
|
98
|
+
`Test "${shortenTitle(failed.title)}" likely failed due to ${signalSummary(failed.signal)}.`
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const counts = new Map();
|
|
103
|
+
for (const failed of failedCases) {
|
|
104
|
+
counts.set(failed.signal, (counts.get(failed.signal) || 0) + 1);
|
|
105
|
+
}
|
|
106
|
+
const topSignal = Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] || "unknown";
|
|
107
|
+
return {
|
|
108
|
+
lines: [
|
|
109
|
+
`${failedCases.length} tests failed.`,
|
|
110
|
+
`Most common signal: ${signalSummary(topSignal)}.`
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
exports.buildQuickDiagnosis = buildQuickDiagnosis;
|
package/dist/reporter.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ declare class SentinelReporter {
|
|
|
15
15
|
private options;
|
|
16
16
|
constructor(options: ReporterOptions);
|
|
17
17
|
onBegin(config: any, suite: any): void;
|
|
18
|
-
onTestEnd(
|
|
18
|
+
onTestEnd(test: any, result: any): Promise<void>;
|
|
19
19
|
private printLocalReport;
|
|
20
20
|
onEnd(): Promise<void>;
|
|
21
21
|
}
|
package/dist/reporter.js
CHANGED
|
@@ -7,6 +7,8 @@ const url_1 = require("url");
|
|
|
7
7
|
const node_1 = require("@sentinelqa/uploader/node");
|
|
8
8
|
const env_1 = require("./env");
|
|
9
9
|
const localReport_1 = require("./localReport");
|
|
10
|
+
const quickDiagnosis_1 = require("./quickDiagnosis");
|
|
11
|
+
const { sentinelCaptureFailureContextFromReporter } = require("@sentinelqa/uploader/playwright");
|
|
10
12
|
const pluralize = (count, singular, plural) => {
|
|
11
13
|
return count === 1 ? singular : plural;
|
|
12
14
|
};
|
|
@@ -49,10 +51,11 @@ class SentinelReporter {
|
|
|
49
51
|
console.log("");
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
|
-
onTestEnd(
|
|
54
|
+
async onTestEnd(test, result) {
|
|
53
55
|
if (!result)
|
|
54
56
|
return;
|
|
55
57
|
if (["failed", "timedOut", "interrupted"].includes(result.status)) {
|
|
58
|
+
await sentinelCaptureFailureContextFromReporter(test, result).catch(() => null);
|
|
56
59
|
this.failedCount += 1;
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -74,11 +77,17 @@ class SentinelReporter {
|
|
|
74
77
|
console.log(bold("Open"));
|
|
75
78
|
console.log(` ${cyan(openCommand)}`);
|
|
76
79
|
console.log("");
|
|
80
|
+
const quickDiagnosis = (0, quickDiagnosis_1.buildQuickDiagnosis)(this.options.playwrightJsonPath);
|
|
81
|
+
if (quickDiagnosis?.lines.length) {
|
|
82
|
+
console.log(yellow("Quick diagnosis"));
|
|
83
|
+
for (const line of quickDiagnosis.lines) {
|
|
84
|
+
console.log(` ${dim(line)}`);
|
|
85
|
+
}
|
|
86
|
+
console.log("");
|
|
87
|
+
}
|
|
77
88
|
console.log(yellow("Tip"));
|
|
78
|
-
console.log(` ${dim("
|
|
79
|
-
console.log(` ${dim("
|
|
80
|
-
console.log("");
|
|
81
|
-
console.log(` ${cyan(formatTerminalLink("https://sentinelqa.com", "https://sentinelqa.com"))}`);
|
|
89
|
+
console.log(` ${dim("Want full AI analysis, shareable run links, and CI history?")}`);
|
|
90
|
+
console.log(` ${dim("Try Sentinel Cloud Beta free:")} ${cyan(formatTerminalLink("https://sentinelqa.com", "https://sentinelqa.com"))}`);
|
|
82
91
|
console.log("");
|
|
83
92
|
console.log(` ${magenta("★ If this reporter helped you debug faster,")}`);
|
|
84
93
|
console.log(` ${dim("consider starring the project:")}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentinelqa/playwright-reporter",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Playwright reporter for CI debugging with optional Sentinel cloud dashboards",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"exports": {
|
|
34
34
|
".": "./dist/index.js",
|
|
35
|
-
"./reporter": "./dist/reporter.js"
|
|
35
|
+
"./reporter": "./dist/reporter.js",
|
|
36
|
+
"./fixtures": "./dist/fixtures.js"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
39
|
"@playwright/test": ">=1.40.0"
|