@sentinelqa/playwright-reporter 0.1.53 → 0.1.54

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/reporter.js CHANGED
@@ -1,5 +1,10 @@
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_fs_1 = __importDefault(require("node:fs"));
7
+ const node_path_1 = __importDefault(require("node:path"));
3
8
  const env_1 = require("./env");
4
9
  const quickDiagnosis_1 = require("./quickDiagnosis");
5
10
  const terminalSummary_1 = require("./terminalSummary");
@@ -15,9 +20,10 @@ const styleCritical = (value) => colorize(value, "1;31");
15
20
  const styleWarning = (value) => colorize(value, "1;33");
16
21
  const styleAction = (value) => colorize(value, "1;36");
17
22
  const stylePrimary = (value) => colorize(value, "1;97");
23
+ const styleImportant = (value) => colorize(value, "97");
18
24
  const styleSecondary = (value) => colorize(value, "2");
19
25
  const divider = () => "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━";
20
- const LABEL_WIDTH = 10;
26
+ const LABEL_WIDTH = 19;
21
27
  const renderRow = (label, value, valueStyle, indent = "") => {
22
28
  const paddedLabel = `${label}:`.padEnd(LABEL_WIDTH);
23
29
  const styledValue = valueStyle ? valueStyle(value) : value;
@@ -31,8 +37,8 @@ const highlightCounts = (value) => value
31
37
  .replace(/\b(\d+\s+passing\s+runs?)\b/gi, (_, match) => styleSecondary(match))
32
38
  .replace(/\b(\d+\s+newly\s+failing)\b/gi, (_, match) => styleWarning(match))
33
39
  .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) => stylePrimary(match));
40
+ const styleIssueTitle = (value) => stylePrimary(value.replace(/\((\d+\s+tests?)\)/i, (_, match) => `(${styleWarning(match)})`));
41
+ const styleWhereValue = (value) => value.replace(/\b([A-Za-z0-9_.-]+\.[cm]?[jt]sx?:\d+(?::\d+)?)\b/g, (_, match) => styleImportant(match));
36
42
  const formatCliLine = (line) => {
37
43
  if (!line.trim())
38
44
  return line;
@@ -45,17 +51,19 @@ const formatCliLine = (line) => {
45
51
  }
46
52
  if (/^All tests passed$/.test(line))
47
53
  return stylePrimary(line);
54
+ if (/^❌\s+\d+\s+failures?/.test(line))
55
+ return styleCritical(line);
48
56
  if (/^\d+\s+tests?\s+failed$/.test(line))
49
57
  return styleCritical(line);
50
58
  if (/^Collapsed into \d+\s+real issue/.test(line))
51
59
  return highlightCounts(line);
52
60
  if (/^\d+\s+tests?\s+in\s+/.test(line))
53
61
  return line;
54
- if (/^Issue \d+:/.test(line)) {
62
+ if (/^Issue(?:(?: \d+)?):/.test(line)) {
55
63
  const [head, rest] = line.split(": ", 2);
56
64
  return `${stylePrimary(head)}: ${styleIssueTitle(rest || "")}`;
57
65
  }
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|Failing code|Selector|Target state|Clears|Report):\s*(.*)$/);
66
+ 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
67
  if (!rowMatch)
60
68
  return line;
61
69
  const [, indent, label, rawValue] = rowMatch;
@@ -66,17 +74,19 @@ const formatCliLine = (line) => {
66
74
  case "Why":
67
75
  return renderRow(label, value, undefined, indent);
68
76
  case "Cause":
69
- return renderRow(label, value, undefined, indent);
77
+ return renderRow(label, rawValue, undefined, indent);
70
78
  case "Where":
71
79
  return renderRow(label, rawValue, styleWhereValue, indent);
72
80
  case "What changed":
81
+ case "Changes since last run":
73
82
  return renderRow(label, rawValue, styleWarning, indent);
74
83
  case "Next":
84
+ case "Likely fix":
75
85
  return renderRow(label, rawValue, styleAction, indent);
76
86
  case "Report":
77
87
  return renderRow(label, rawValue, styleAction, indent);
78
88
  case "Expected":
79
- return renderRow(label, rawValue, stylePrimary, indent);
89
+ return renderRow(label, rawValue, styleImportant, indent);
80
90
  case "Received":
81
91
  return renderRow(label, rawValue, styleCritical, indent);
82
92
  case "Confidence":
@@ -96,15 +106,13 @@ const formatCliLine = (line) => {
96
106
  case "Artifacts ready":
97
107
  return renderRow(label, rawValue, styleSecondary, indent);
98
108
  case "Failing step":
99
- return renderRow(label, rawValue, stylePrimary, indent);
100
- case "Failing code":
101
- return renderRow(label, rawValue, stylePrimary, indent);
109
+ return renderRow(label, rawValue, styleImportant, indent);
102
110
  case "Selector":
103
- return renderRow(label, rawValue, stylePrimary, indent);
111
+ return renderRow(label, rawValue, styleImportant, indent);
112
+ case "Blocker":
113
+ return renderRow(label, rawValue, styleCritical, indent);
104
114
  case "Target state":
105
115
  return renderRow(label, rawValue, styleWarning, indent);
106
- case "Clears":
107
- return renderRow(label, highlightCounts(rawValue), styleWarning, indent);
108
116
  default:
109
117
  return line;
110
118
  }
@@ -177,12 +185,16 @@ class SentinelReporter {
177
185
  !hasCiEnv &&
178
186
  !localUploadEnabled;
179
187
  const quickDiagnosis = (0, quickDiagnosis_1.buildQuickDiagnosis)(this.options.playwrightJsonPath);
188
+ const quickDiagnosisStructured = (0, quickDiagnosis_1.buildQuickDiagnosisStructured)(this.options.playwrightJsonPath);
189
+ const isSetupFailureDiagnosis = Boolean(quickDiagnosis?.lines.some((line) => /^Issue(?:(?: \d+)?): Playwright setup error/.test(line)));
180
190
  const finalFailedCount = readFinalFailedCount(this.options.playwrightJsonPath);
181
191
  const effectiveFailedCount = typeof finalFailedCount === "number" ? finalFailedCount : this.failedCount;
182
192
  const passingSummary = effectiveFailedCount === 0
183
193
  ? (0, terminalSummary_1.buildPassingRunSummary)(this.options.playwrightJsonPath, { observedRunDurationMs: Date.now() - this.startedAt })
184
194
  : null;
185
- const failedRunHistory = effectiveFailedCount > 0 ? (0, runHistory_1.buildFailedRunHistorySummary)(this.options.playwrightJsonPath) : null;
195
+ const failedRunHistory = effectiveFailedCount > 0 && !isSetupFailureDiagnosis
196
+ ? (0, runHistory_1.buildFailedRunHistorySummary)(this.options.playwrightJsonPath)
197
+ : null;
186
198
  if (effectiveFailedCount > 0) {
187
199
  (0, terminalSummary_1.recordReporterHistorySnapshot)(this.options.playwrightJsonPath);
188
200
  }
@@ -214,31 +226,53 @@ class SentinelReporter {
214
226
  console.log(stylePrimary("Sentinel diagnosis"));
215
227
  console.log(styleSecondary(divider()));
216
228
  console.log("");
217
- if (failedRunHistory?.passStreakBeforeFailure && failedRunHistory.passStreakBeforeFailure > 0) {
229
+ if (!isSetupFailureDiagnosis && failedRunHistory?.passStreakBeforeFailure && failedRunHistory.passStreakBeforeFailure > 0) {
218
230
  console.log(formatCliLine(`NEW FAILURE after ${failedRunHistory.passStreakBeforeFailure} passing runs`));
219
231
  console.log("");
220
232
  }
221
- else if ((failedRunHistory?.recurringCount || 0) > 0) {
233
+ else if (!isSetupFailureDiagnosis && (failedRunHistory?.recurringCount || 0) > 0) {
222
234
  console.log(formatCliLine(`RECURRING FAILURE (${failedRunHistory?.recurringCount} previous runs)`));
235
+ if (failedRunHistory?.isDominantRecurringIssue && failedRunHistory?.dominantRecurringIssueTitle) {
236
+ console.log(formatCliLine(`Impact: ${failedRunHistory.dominantRecurringIssueTitle} is the most common recent failure in local history`));
237
+ }
223
238
  console.log("");
224
239
  }
225
240
  for (const line of quickDiagnosis.lines) {
226
241
  console.log(formatCliLine(line));
227
242
  }
228
243
  console.log("");
229
- console.log(styleSecondary(divider()));
230
- console.log("");
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`));
244
+ if (!isSetupFailureDiagnosis) {
245
+ console.log(styleSecondary(divider()));
237
246
  console.log("");
247
+ if (failedRunHistory?.newFailures && failedRunHistory.newFailures > 0) {
248
+ console.log(formatCliLine(`Impact: ${failedRunHistory.newFailures} newly failing in this run`));
249
+ console.log("");
250
+ }
251
+ else if (failedRunHistory?.stillFailing && failedRunHistory.stillFailing > 0) {
252
+ console.log(formatCliLine(`Impact: ${failedRunHistory.stillFailing} tests still failing`));
253
+ if (failedRunHistory?.isDominantRecurringIssue && failedRunHistory?.dominantRecurringIssueCount > 0) {
254
+ console.log(formatCliLine(`Impact: Seen in ${failedRunHistory.dominantRecurringIssueCount} recorded failed runs`));
255
+ }
256
+ console.log("");
257
+ }
238
258
  }
239
259
  }
260
+ if (isSetupFailureDiagnosis) {
261
+ return;
262
+ }
240
263
  console.log(styleSecondary("Uploading debug report..."));
241
264
  console.log("");
265
+ let structuredDiagnosisPath = null;
266
+ if (quickDiagnosisStructured) {
267
+ try {
268
+ structuredDiagnosisPath = node_path_1.default.join(this.options.testResultsDir, "sentinel-structured-diagnosis.json");
269
+ node_fs_1.default.mkdirSync(this.options.testResultsDir, { recursive: true });
270
+ node_fs_1.default.writeFileSync(structuredDiagnosisPath, JSON.stringify(quickDiagnosisStructured, null, 2));
271
+ }
272
+ catch {
273
+ structuredDiagnosisPath = null;
274
+ }
275
+ }
242
276
  const upload = (await (0, node_1.runSentinelUpload)({
243
277
  playwrightJsonPath: this.options.playwrightJsonPath,
244
278
  playwrightReportDir: this.options.playwrightReportDir,
@@ -248,11 +282,12 @@ class SentinelReporter {
248
282
  env: {
249
283
  SENTINEL_REPORTER_PROJECT: this.options.project || undefined,
250
284
  SENTINEL_REPORTER_SILENT: "1",
285
+ SENTINEL_STRUCTURED_DIAGNOSIS_PATH: structuredDiagnosisPath || undefined,
251
286
  SENTINEL_UPLOAD_LOCAL: usingImplicitLocalPublicMode ? "1" : process.env.SENTINEL_UPLOAD_LOCAL
252
287
  }
253
288
  }));
254
289
  if (upload.exitCode !== 0) {
255
- throw new Error(`Sentinel upload failed with exit code ${upload.exitCode}`);
290
+ return;
256
291
  }
257
292
  if (!quickDiagnosis?.lines.length && upload.diagnosis?.lines.length) {
258
293
  console.log(stylePrimary("Sentinel diagnosis"));
@@ -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;
@@ -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
  };
@@ -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
- (candidate.currentRunSignal ||
368
- candidate.historicalRetries >= 2 ||
369
- candidate.repeatedSlowPasses >= 3 ||
370
- (candidate.recentTrendRatio !== null && candidate.recentTrendRatio >= 1.5)) &&
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 && topNearFailures[0]) {
379
- const riskKind = getPrimaryRiskKind(topNearFailures[0]);
380
- lines.push(`At risk: ${topNearFailures[0].title} ${topNearFailures[0].primaryReason}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentinelqa/playwright-reporter",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "private": false,
5
5
  "description": "Playwright reporter for CI debugging with optional Sentinel cloud dashboards",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "@playwright/test": ">=1.40.0"
40
40
  },
41
41
  "dependencies": {
42
- "@sentinelqa/uploader": "^0.1.35"
42
+ "@sentinelqa/uploader": "^0.1.38"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^20.19.32",