@sentinelqa/playwright-reporter 0.1.53 → 0.1.56
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 +142 -165
- package/dist/localReport.js +812 -34
- package/dist/mode.d.ts +2 -0
- package/dist/mode.js +16 -0
- package/dist/quickDiagnosis.d.ts +42 -2
- package/dist/quickDiagnosis.js +1243 -98
- package/dist/reporter.d.ts +1 -0
- package/dist/reporter.js +102 -25
- package/dist/runHistory.d.ts +3 -0
- package/dist/runHistory.js +11 -1
- package/dist/terminalSummary.js +44 -12
- package/package.json +1 -1
package/dist/reporter.d.ts
CHANGED
package/dist/reporter.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
const node_1 = require("@sentinelqa/uploader/node");
|
|
6
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
7
|
+
const node_url_1 = require("node:url");
|
|
3
8
|
const env_1 = require("./env");
|
|
4
9
|
const quickDiagnosis_1 = require("./quickDiagnosis");
|
|
10
|
+
const localReport_1 = require("./localReport");
|
|
5
11
|
const terminalSummary_1 = require("./terminalSummary");
|
|
6
12
|
const runHistory_1 = require("./runHistory");
|
|
7
13
|
const telemetry_1 = require("./telemetry");
|
|
8
14
|
const { sentinelCaptureFailureContextFromReporter } = require("@sentinelqa/uploader/playwright");
|
|
15
|
+
const readSentinelMode = (env = process.env) => {
|
|
16
|
+
const raw = env.SENTINEL_MODE?.trim();
|
|
17
|
+
if (raw == null || raw === "")
|
|
18
|
+
return "hosted";
|
|
19
|
+
if (raw === "hosted" || raw === "offline")
|
|
20
|
+
return raw;
|
|
21
|
+
throw new Error([
|
|
22
|
+
"Invalid SENTINEL_MODE value: \"" + raw + "\".",
|
|
23
|
+
"Allowed values: hosted, offline.",
|
|
24
|
+
"Tip: remove SENTINEL_MODE to use the default hosted flow."
|
|
25
|
+
].join(" "));
|
|
26
|
+
};
|
|
9
27
|
const colorize = (value, code) => {
|
|
10
28
|
if (!process.stdout.isTTY)
|
|
11
29
|
return value;
|
|
@@ -15,9 +33,10 @@ const styleCritical = (value) => colorize(value, "1;31");
|
|
|
15
33
|
const styleWarning = (value) => colorize(value, "1;33");
|
|
16
34
|
const styleAction = (value) => colorize(value, "1;36");
|
|
17
35
|
const stylePrimary = (value) => colorize(value, "1;97");
|
|
36
|
+
const styleImportant = (value) => colorize(value, "97");
|
|
18
37
|
const styleSecondary = (value) => colorize(value, "2");
|
|
19
38
|
const divider = () => "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━";
|
|
20
|
-
const LABEL_WIDTH =
|
|
39
|
+
const LABEL_WIDTH = 19;
|
|
21
40
|
const renderRow = (label, value, valueStyle, indent = "") => {
|
|
22
41
|
const paddedLabel = `${label}:`.padEnd(LABEL_WIDTH);
|
|
23
42
|
const styledValue = valueStyle ? valueStyle(value) : value;
|
|
@@ -31,8 +50,8 @@ const highlightCounts = (value) => value
|
|
|
31
50
|
.replace(/\b(\d+\s+passing\s+runs?)\b/gi, (_, match) => styleSecondary(match))
|
|
32
51
|
.replace(/\b(\d+\s+newly\s+failing)\b/gi, (_, match) => styleWarning(match))
|
|
33
52
|
.replace(/\b(\d+\s+tests?\s+still\s+failing)\b/gi, (_, match) => styleWarning(match));
|
|
34
|
-
const styleIssueTitle = (value) => value.replace(/\((\d+\s+tests?)\)/i, (_, match) => `(${styleWarning(match)})`);
|
|
35
|
-
const styleWhereValue = (value) => value.replace(/\b([A-Za-z0-9_.-]+\.[cm]?[jt]sx?:\d+(?::\d+)?)\b/g, (_, match) =>
|
|
53
|
+
const styleIssueTitle = (value) => stylePrimary(value.replace(/\((\d+\s+tests?)\)/i, (_, match) => `(${styleWarning(match)})`));
|
|
54
|
+
const styleWhereValue = (value) => value.replace(/\b([A-Za-z0-9_.-]+\.[cm]?[jt]sx?:\d+(?::\d+)?)\b/g, (_, match) => styleImportant(match));
|
|
36
55
|
const formatCliLine = (line) => {
|
|
37
56
|
if (!line.trim())
|
|
38
57
|
return line;
|
|
@@ -45,17 +64,19 @@ const formatCliLine = (line) => {
|
|
|
45
64
|
}
|
|
46
65
|
if (/^All tests passed$/.test(line))
|
|
47
66
|
return stylePrimary(line);
|
|
67
|
+
if (/^❌\s+\d+\s+failures?/.test(line))
|
|
68
|
+
return styleCritical(line);
|
|
48
69
|
if (/^\d+\s+tests?\s+failed$/.test(line))
|
|
49
70
|
return styleCritical(line);
|
|
50
71
|
if (/^Collapsed into \d+\s+real issue/.test(line))
|
|
51
72
|
return highlightCounts(line);
|
|
52
73
|
if (/^\d+\s+tests?\s+in\s+/.test(line))
|
|
53
74
|
return line;
|
|
54
|
-
if (/^Issue \d
|
|
75
|
+
if (/^Issue(?:(?: \d+)?):/.test(line)) {
|
|
55
76
|
const [head, rest] = line.split(": ", 2);
|
|
56
77
|
return `${stylePrimary(head)}: ${styleIssueTitle(rest || "")}`;
|
|
57
78
|
}
|
|
58
|
-
const rowMatch = line.match(/^(\s*)(What broke|Why|Cause|Where|What changed|Next|Expected|Received|Confidence|Impact|At risk|Why this matters|Recommendation|Last failure|Status|Artifacts ready|Failing step|
|
|
79
|
+
const rowMatch = line.match(/^(\s*)(What broke|Why|Cause|Where|What changed|Changes since last run|Next|Likely fix|Expected|Received|Confidence|Impact|At risk|Why this matters|Recommendation|Last failure|Status|Artifacts ready|Failing step|Selector|Target state|Report|Blocker):\s*(.*)$/);
|
|
59
80
|
if (!rowMatch)
|
|
60
81
|
return line;
|
|
61
82
|
const [, indent, label, rawValue] = rowMatch;
|
|
@@ -66,17 +87,19 @@ const formatCliLine = (line) => {
|
|
|
66
87
|
case "Why":
|
|
67
88
|
return renderRow(label, value, undefined, indent);
|
|
68
89
|
case "Cause":
|
|
69
|
-
return renderRow(label,
|
|
90
|
+
return renderRow(label, rawValue, undefined, indent);
|
|
70
91
|
case "Where":
|
|
71
92
|
return renderRow(label, rawValue, styleWhereValue, indent);
|
|
72
93
|
case "What changed":
|
|
94
|
+
case "Changes since last run":
|
|
73
95
|
return renderRow(label, rawValue, styleWarning, indent);
|
|
74
96
|
case "Next":
|
|
97
|
+
case "Likely fix":
|
|
75
98
|
return renderRow(label, rawValue, styleAction, indent);
|
|
76
99
|
case "Report":
|
|
77
100
|
return renderRow(label, rawValue, styleAction, indent);
|
|
78
101
|
case "Expected":
|
|
79
|
-
return renderRow(label, rawValue,
|
|
102
|
+
return renderRow(label, rawValue, styleImportant, indent);
|
|
80
103
|
case "Received":
|
|
81
104
|
return renderRow(label, rawValue, styleCritical, indent);
|
|
82
105
|
case "Confidence":
|
|
@@ -96,15 +119,13 @@ const formatCliLine = (line) => {
|
|
|
96
119
|
case "Artifacts ready":
|
|
97
120
|
return renderRow(label, rawValue, styleSecondary, indent);
|
|
98
121
|
case "Failing step":
|
|
99
|
-
return renderRow(label, rawValue,
|
|
100
|
-
case "Failing code":
|
|
101
|
-
return renderRow(label, rawValue, stylePrimary, indent);
|
|
122
|
+
return renderRow(label, rawValue, styleImportant, indent);
|
|
102
123
|
case "Selector":
|
|
103
|
-
return renderRow(label, rawValue,
|
|
124
|
+
return renderRow(label, rawValue, styleImportant, indent);
|
|
125
|
+
case "Blocker":
|
|
126
|
+
return renderRow(label, rawValue, styleCritical, indent);
|
|
104
127
|
case "Target state":
|
|
105
128
|
return renderRow(label, rawValue, styleWarning, indent);
|
|
106
|
-
case "Clears":
|
|
107
|
-
return renderRow(label, highlightCounts(rawValue), styleWarning, indent);
|
|
108
129
|
default:
|
|
109
130
|
return line;
|
|
110
131
|
}
|
|
@@ -142,6 +163,7 @@ class SentinelReporter {
|
|
|
142
163
|
this.startedAt = Date.now();
|
|
143
164
|
(0, env_1.loadSentinelEnv)();
|
|
144
165
|
this.options = options;
|
|
166
|
+
this.sentinelMode = readSentinelMode(process.env);
|
|
145
167
|
}
|
|
146
168
|
onBegin(config, suite) {
|
|
147
169
|
this.startedAt = Date.now();
|
|
@@ -170,6 +192,37 @@ class SentinelReporter {
|
|
|
170
192
|
}
|
|
171
193
|
}
|
|
172
194
|
async onEnd() {
|
|
195
|
+
const offlineMode = this.sentinelMode === "offline";
|
|
196
|
+
const emitLocalReport = () => {
|
|
197
|
+
console.log(styleSecondary("Writing local debug report..."));
|
|
198
|
+
console.log("");
|
|
199
|
+
try {
|
|
200
|
+
const result = (0, localReport_1.generateLocalDebugReport)({
|
|
201
|
+
playwrightJsonPath: this.options.playwrightJsonPath,
|
|
202
|
+
playwrightReportDir: this.options.playwrightReportDir,
|
|
203
|
+
testResultsDir: this.options.testResultsDir,
|
|
204
|
+
artifactDirs: this.options.artifactDirs || [],
|
|
205
|
+
reportDir: process.env.SENTINEL_REPORT_DIR || undefined
|
|
206
|
+
});
|
|
207
|
+
const rel = node_path_1.default.relative(process.cwd(), result.htmlPath) || result.htmlPath;
|
|
208
|
+
const fileUrl = (0, node_url_1.pathToFileURL)(node_path_1.default.resolve(result.htmlPath)).toString();
|
|
209
|
+
console.log(styleAction("Local report ready"));
|
|
210
|
+
console.log(formatCliLine(`Next: ${rel}`));
|
|
211
|
+
console.log("");
|
|
212
|
+
console.log(styleAction("Shareable report:"));
|
|
213
|
+
console.log(` ${fileUrl}`);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
console.error(styleCritical("Sentinel: Local report generation failed."));
|
|
217
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
218
|
+
if (message && message !== "[object Object]") {
|
|
219
|
+
console.error(styleSecondary(message));
|
|
220
|
+
}
|
|
221
|
+
if (process.env.SENTINEL_DEBUG && err instanceof Error && err.stack) {
|
|
222
|
+
console.error(styleSecondary(err.stack));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
173
226
|
const hasWorkspaceToken = Boolean(process.env.SENTINEL_TOKEN);
|
|
174
227
|
const hasCiEnv = (0, node_1.hasSupportedCiEnv)(process.env);
|
|
175
228
|
const localUploadEnabled = (0, node_1.isLocalUploadEnabled)(process.env);
|
|
@@ -177,12 +230,15 @@ class SentinelReporter {
|
|
|
177
230
|
!hasCiEnv &&
|
|
178
231
|
!localUploadEnabled;
|
|
179
232
|
const quickDiagnosis = (0, quickDiagnosis_1.buildQuickDiagnosis)(this.options.playwrightJsonPath);
|
|
233
|
+
const isSetupFailureDiagnosis = Boolean(quickDiagnosis?.lines.some((line) => /^Issue(?:(?: \d+)?): Playwright setup error/.test(line)));
|
|
180
234
|
const finalFailedCount = readFinalFailedCount(this.options.playwrightJsonPath);
|
|
181
235
|
const effectiveFailedCount = typeof finalFailedCount === "number" ? finalFailedCount : this.failedCount;
|
|
182
236
|
const passingSummary = effectiveFailedCount === 0
|
|
183
237
|
? (0, terminalSummary_1.buildPassingRunSummary)(this.options.playwrightJsonPath, { observedRunDurationMs: Date.now() - this.startedAt })
|
|
184
238
|
: null;
|
|
185
|
-
const failedRunHistory = effectiveFailedCount > 0
|
|
239
|
+
const failedRunHistory = effectiveFailedCount > 0 && !isSetupFailureDiagnosis
|
|
240
|
+
? (0, runHistory_1.buildFailedRunHistorySummary)(this.options.playwrightJsonPath)
|
|
241
|
+
: null;
|
|
186
242
|
if (effectiveFailedCount > 0) {
|
|
187
243
|
(0, terminalSummary_1.recordReporterHistorySnapshot)(this.options.playwrightJsonPath);
|
|
188
244
|
}
|
|
@@ -208,35 +264,52 @@ class SentinelReporter {
|
|
|
208
264
|
if (hasWorkspaceToken && !hasCiEnv && !localUploadEnabled) {
|
|
209
265
|
console.log("Uploading debug report skipped");
|
|
210
266
|
console.log("Set SENTINEL_UPLOAD_LOCAL=1 for local workspace uploads.");
|
|
267
|
+
console.log("");
|
|
268
|
+
emitLocalReport();
|
|
211
269
|
return;
|
|
212
270
|
}
|
|
213
271
|
if (quickDiagnosis?.lines.length) {
|
|
214
272
|
console.log(stylePrimary("Sentinel diagnosis"));
|
|
215
273
|
console.log(styleSecondary(divider()));
|
|
216
274
|
console.log("");
|
|
217
|
-
if (failedRunHistory?.passStreakBeforeFailure && failedRunHistory.passStreakBeforeFailure > 0) {
|
|
275
|
+
if (!isSetupFailureDiagnosis && failedRunHistory?.passStreakBeforeFailure && failedRunHistory.passStreakBeforeFailure > 0) {
|
|
218
276
|
console.log(formatCliLine(`NEW FAILURE after ${failedRunHistory.passStreakBeforeFailure} passing runs`));
|
|
219
277
|
console.log("");
|
|
220
278
|
}
|
|
221
|
-
else if ((failedRunHistory?.recurringCount || 0) > 0) {
|
|
279
|
+
else if (!isSetupFailureDiagnosis && (failedRunHistory?.recurringCount || 0) > 0) {
|
|
222
280
|
console.log(formatCliLine(`RECURRING FAILURE (${failedRunHistory?.recurringCount} previous runs)`));
|
|
281
|
+
if (failedRunHistory?.isDominantRecurringIssue && failedRunHistory?.dominantRecurringIssueTitle) {
|
|
282
|
+
console.log(formatCliLine(`Impact: ${failedRunHistory.dominantRecurringIssueTitle} is the most common recent failure in local history`));
|
|
283
|
+
}
|
|
223
284
|
console.log("");
|
|
224
285
|
}
|
|
225
286
|
for (const line of quickDiagnosis.lines) {
|
|
226
287
|
console.log(formatCliLine(line));
|
|
227
288
|
}
|
|
228
289
|
console.log("");
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (failedRunHistory?.newFailures && failedRunHistory.newFailures > 0) {
|
|
232
|
-
console.log(formatCliLine(`Impact: ${failedRunHistory.newFailures} newly failing in this run`));
|
|
233
|
-
console.log("");
|
|
234
|
-
}
|
|
235
|
-
else if (failedRunHistory?.stillFailing && failedRunHistory.stillFailing > 0) {
|
|
236
|
-
console.log(formatCliLine(`Impact: ${failedRunHistory.stillFailing} tests still failing`));
|
|
290
|
+
if (!isSetupFailureDiagnosis) {
|
|
291
|
+
console.log(styleSecondary(divider()));
|
|
237
292
|
console.log("");
|
|
293
|
+
if (failedRunHistory?.newFailures && failedRunHistory.newFailures > 0) {
|
|
294
|
+
console.log(formatCliLine(`Impact: ${failedRunHistory.newFailures} newly failing in this run`));
|
|
295
|
+
console.log("");
|
|
296
|
+
}
|
|
297
|
+
else if (failedRunHistory?.stillFailing && failedRunHistory.stillFailing > 0) {
|
|
298
|
+
console.log(formatCliLine(`Impact: ${failedRunHistory.stillFailing} tests still failing`));
|
|
299
|
+
if (failedRunHistory?.isDominantRecurringIssue && failedRunHistory?.dominantRecurringIssueCount > 0) {
|
|
300
|
+
console.log(formatCliLine(`Impact: Seen in ${failedRunHistory.dominantRecurringIssueCount} recorded failed runs`));
|
|
301
|
+
}
|
|
302
|
+
console.log("");
|
|
303
|
+
}
|
|
238
304
|
}
|
|
239
305
|
}
|
|
306
|
+
if (isSetupFailureDiagnosis) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (offlineMode) {
|
|
310
|
+
emitLocalReport();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
240
313
|
console.log(styleSecondary("Uploading debug report..."));
|
|
241
314
|
console.log("");
|
|
242
315
|
const upload = (await (0, node_1.runSentinelUpload)({
|
|
@@ -252,7 +325,11 @@ class SentinelReporter {
|
|
|
252
325
|
}
|
|
253
326
|
}));
|
|
254
327
|
if (upload.exitCode !== 0) {
|
|
255
|
-
|
|
328
|
+
console.log(styleWarning("Hosted upload failed"));
|
|
329
|
+
console.log(styleSecondary("Falling back to a local report."));
|
|
330
|
+
console.log("");
|
|
331
|
+
emitLocalReport();
|
|
332
|
+
return;
|
|
256
333
|
}
|
|
257
334
|
if (!quickDiagnosis?.lines.length && upload.diagnosis?.lines.length) {
|
|
258
335
|
console.log(stylePrimary("Sentinel diagnosis"));
|
package/dist/runHistory.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export type FailedRunHistorySummary = {
|
|
|
12
12
|
stillFailing: number;
|
|
13
13
|
recurringCount: number;
|
|
14
14
|
recurringTitle: string | null;
|
|
15
|
+
dominantRecurringIssueTitle: string | null;
|
|
16
|
+
dominantRecurringIssueCount: number;
|
|
17
|
+
isDominantRecurringIssue: boolean;
|
|
15
18
|
};
|
|
16
19
|
export declare const buildRunDiffSummary: (playwrightJsonPath: string) => RunDiffSummary | null;
|
|
17
20
|
export declare const buildFailedRunHistorySummary: (playwrightJsonPath: string) => FailedRunHistorySummary | null;
|
package/dist/runHistory.js
CHANGED
|
@@ -204,6 +204,10 @@ const buildFailedRunHistorySummary = (playwrightJsonPath) => {
|
|
|
204
204
|
}))
|
|
205
205
|
.sort((a, b) => b.occurrences - a.occurrences);
|
|
206
206
|
const topRecurring = recurringFailures.find((item) => item.occurrences > 0) || null;
|
|
207
|
+
const topRecurringTotal = topRecurring ? topRecurring.occurrences + 1 : 0;
|
|
208
|
+
const isDominantRecurringIssue = Boolean(topRecurring &&
|
|
209
|
+
topRecurringTotal >= 3 &&
|
|
210
|
+
topRecurringTotal >= Math.ceil(Math.max(failingReporterRuns.length, 1) / 2));
|
|
207
211
|
writeSnapshot(snapshot);
|
|
208
212
|
const lines = [];
|
|
209
213
|
if (passStreakBeforeFailure > 0) {
|
|
@@ -220,6 +224,9 @@ const buildFailedRunHistorySummary = (playwrightJsonPath) => {
|
|
|
220
224
|
}
|
|
221
225
|
if (topRecurring) {
|
|
222
226
|
lines.push(`- Recurring across ${topRecurring.occurrences + 1} recorded failed runs in local history (${topRecurring.failure.title})`);
|
|
227
|
+
if (isDominantRecurringIssue) {
|
|
228
|
+
lines.push(`- This is the most common recent failure in local history`);
|
|
229
|
+
}
|
|
223
230
|
}
|
|
224
231
|
return lines.length
|
|
225
232
|
? {
|
|
@@ -230,7 +237,10 @@ const buildFailedRunHistorySummary = (playwrightJsonPath) => {
|
|
|
230
237
|
fixedTests,
|
|
231
238
|
stillFailing,
|
|
232
239
|
recurringCount: topRecurring ? topRecurring.occurrences : 0,
|
|
233
|
-
recurringTitle: topRecurring?.failure.title || null
|
|
240
|
+
recurringTitle: topRecurring?.failure.title || null,
|
|
241
|
+
dominantRecurringIssueTitle: topRecurring?.failure.title || null,
|
|
242
|
+
dominantRecurringIssueCount: topRecurringTotal,
|
|
243
|
+
isDominantRecurringIssue
|
|
234
244
|
}
|
|
235
245
|
: null;
|
|
236
246
|
};
|
package/dist/terminalSummary.js
CHANGED
|
@@ -104,6 +104,25 @@ const getPrimaryRiskKind = (candidate) => {
|
|
|
104
104
|
return "historical_failures";
|
|
105
105
|
return "generic";
|
|
106
106
|
};
|
|
107
|
+
const isStrongPassRisk = (candidate, lastFailureRunsAgo) => {
|
|
108
|
+
if (candidate.retries > 0)
|
|
109
|
+
return true;
|
|
110
|
+
if (candidate.ratio !== null && candidate.ratio >= 2.0)
|
|
111
|
+
return true;
|
|
112
|
+
if (candidate.repeatedSlowPasses >= 3)
|
|
113
|
+
return true;
|
|
114
|
+
if (candidate.timeoutUtilization !== null && candidate.timeoutUtilization >= 0.9)
|
|
115
|
+
return true;
|
|
116
|
+
if (candidate.historicalRetries >= 2)
|
|
117
|
+
return true;
|
|
118
|
+
if (candidate.historicalFailures >= 6 &&
|
|
119
|
+
lastFailureRunsAgo !== null &&
|
|
120
|
+
lastFailureRunsAgo <= 2 &&
|
|
121
|
+
(candidate.ratio !== null || candidate.historicalRetries > 0 || candidate.repeatedSlowPasses > 0)) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
107
126
|
const cleanTitlePath = (parts) => {
|
|
108
127
|
const normalized = parts.map((part) => part.trim()).filter(Boolean);
|
|
109
128
|
const withoutUnnamed = normalized.filter((part) => part !== "Unnamed test");
|
|
@@ -339,6 +358,22 @@ const buildPassingRunSummary = (playwrightJsonPath, options) => {
|
|
|
339
358
|
: recentFailurePressure
|
|
340
359
|
? `failed in ${historicalFailures} of the last 10 runs`
|
|
341
360
|
: `has weak instability signals`;
|
|
361
|
+
const riskKind = getPrimaryRiskKind({
|
|
362
|
+
title: test.title,
|
|
363
|
+
ratio,
|
|
364
|
+
recentTrendRatio,
|
|
365
|
+
retries: test.retries,
|
|
366
|
+
historicalRetries,
|
|
367
|
+
historicalFailures,
|
|
368
|
+
repeatedSlowPasses,
|
|
369
|
+
timeoutUtilization,
|
|
370
|
+
medianDurationMs: med,
|
|
371
|
+
currentDurationMs: test.durationMs,
|
|
372
|
+
score,
|
|
373
|
+
strongSignal,
|
|
374
|
+
currentRunSignal,
|
|
375
|
+
primaryReason
|
|
376
|
+
});
|
|
342
377
|
if (score > 0) {
|
|
343
378
|
nearFailureCandidates.push({
|
|
344
379
|
title: test.title,
|
|
@@ -354,7 +389,8 @@ const buildPassingRunSummary = (playwrightJsonPath, options) => {
|
|
|
354
389
|
score,
|
|
355
390
|
strongSignal,
|
|
356
391
|
currentRunSignal,
|
|
357
|
-
primaryReason
|
|
392
|
+
primaryReason,
|
|
393
|
+
riskKind
|
|
358
394
|
});
|
|
359
395
|
}
|
|
360
396
|
}
|
|
@@ -364,20 +400,16 @@ const buildPassingRunSummary = (playwrightJsonPath, options) => {
|
|
|
364
400
|
(right.timeoutUtilization || 0) - (left.timeoutUtilization || 0) ||
|
|
365
401
|
(right.ratio || 0) - (left.ratio || 0));
|
|
366
402
|
const strongNearFailures = nearFailureCandidates.filter((candidate) => candidate.strongSignal &&
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
candidate.score >= 3);
|
|
372
|
-
const topNearFailures = strongNearFailures.slice(0, 2);
|
|
373
|
-
const flakyLookingCount = strongNearFailures.length;
|
|
374
|
-
const hasActiveRisk = flakyLookingCount > 0;
|
|
403
|
+
candidate.score >= 3 &&
|
|
404
|
+
isStrongPassRisk(candidate, lastFailureRunsAgo));
|
|
405
|
+
const topRisk = strongNearFailures[0] || null;
|
|
406
|
+
const hasActiveRisk = Boolean(topRisk);
|
|
375
407
|
const totalDurationMs = snapshot.tests.reduce((sum, test) => sum + test.durationMs, 0);
|
|
376
408
|
const displayedRunDurationMs = options?.observedRunDurationMs || snapshot.wallDurationMs || totalDurationMs;
|
|
377
409
|
const lines = ["All tests passed", `${snapshot.passedCount} tests in ${formatDuration(displayedRunDurationMs)}`];
|
|
378
|
-
if (hasActiveRisk &&
|
|
379
|
-
const riskKind =
|
|
380
|
-
lines.push(`At risk: ${
|
|
410
|
+
if (hasActiveRisk && topRisk) {
|
|
411
|
+
const riskKind = topRisk.riskKind;
|
|
412
|
+
lines.push(`At risk: ${topRisk.title} ${topRisk.primaryReason}`);
|
|
381
413
|
if (riskKind === "retry" || riskKind === "historical_retries") {
|
|
382
414
|
lines.push("Why this matters: Similar retry patterns often turn into flaky failures");
|
|
383
415
|
}
|