@sentinelqa/playwright-reporter 0.1.47 → 0.1.51

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.
@@ -95,21 +95,25 @@ const safeSlug = (value) => {
95
95
  .replace(/^-+|-+$/g, "")
96
96
  .slice(0, 64) || "artifact");
97
97
  };
98
+ const cleanTitleParts = (parts) => {
99
+ const normalized = parts.map((part) => String(part || "").trim()).filter(Boolean);
100
+ const withoutUnnamed = normalized.filter((part) => part !== "Unnamed test");
101
+ return withoutUnnamed.length ? withoutUnnamed : normalized;
102
+ };
98
103
  const buildTitlePath = (baseTitles, test) => {
99
104
  const title = typeof test?.title === "string" ? test.title : null;
100
- if (!title)
101
- return baseTitles.filter(Boolean);
102
- if (baseTitles[baseTitles.length - 1] === title)
103
- return baseTitles.filter(Boolean);
104
- return [...baseTitles, title].filter(Boolean);
105
+ const next = !title || baseTitles[baseTitles.length - 1] === title
106
+ ? baseTitles.filter(Boolean)
107
+ : [...baseTitles, title].filter(Boolean);
108
+ return cleanTitleParts(next);
105
109
  };
106
110
  const buildTestIdentity = (test, titlePath) => {
107
111
  const file = test?.location?.file || "unknown";
108
112
  const project = test?.projectName || "default";
109
- const joined = titlePath.join(" > ");
113
+ const joined = cleanTitleParts(titlePath).join(" > ");
110
114
  return {
111
115
  id: [file, project, joined].join("::"),
112
- matchKey: [file, joined].join("::")
116
+ matchKey: [file, project, joined].join("::")
113
117
  };
114
118
  };
115
119
  const formatDuration = (durationMs) => {
@@ -223,6 +227,18 @@ const resolveExistingFile = (candidate, baseDirs) => {
223
227
  }
224
228
  return null;
225
229
  };
230
+ const readAttachmentJson = (attachments, name, baseDirs) => {
231
+ const attachment = attachments.find((item) => item?.name === name && item?.path);
232
+ const resolved = resolveExistingFile(attachment?.path, baseDirs);
233
+ if (!resolved)
234
+ return null;
235
+ try {
236
+ return JSON.parse(fs_1.default.readFileSync(resolved, "utf8"));
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ };
226
242
  const copyArtifact = (sourcePath, kind, reportDir, usedRelativePaths, testId) => {
227
243
  const hash = crypto_1.default
228
244
  .createHash("sha1")
@@ -253,6 +269,8 @@ const copyArtifact = (sourcePath, kind, reportDir, usedRelativePaths, testId) =>
253
269
  const createReportTest = (test, titlePath) => {
254
270
  const results = Array.isArray(test?.results) ? test.results : [];
255
271
  const lastResult = results.length > 0 ? results[results.length - 1] : null;
272
+ const attachments = Array.isArray(lastResult?.attachments) ? lastResult.attachments : [];
273
+ const baseDirs = [process.cwd(), path_1.default.resolve(process.cwd(), "test-results")];
256
274
  const errors = results.flatMap((result) => Array.isArray(result?.errors)
257
275
  ? result.errors
258
276
  .map((error) => error?.message || error?.stack || String(error || ""))
@@ -265,15 +283,20 @@ const createReportTest = (test, titlePath) => {
265
283
  return {
266
284
  id: identity.id,
267
285
  matchKey: identity.matchKey,
268
- title: test?.title || titlePath[titlePath.length - 1] || "Untitled test",
269
- titlePath,
286
+ title: cleanTitleParts(titlePath).slice(-1)[0] || test?.title || titlePath[titlePath.length - 1] || "Untitled test",
287
+ titlePath: cleanTitleParts(titlePath),
270
288
  file: test?.location?.file || null,
271
289
  projectName: test?.projectName || null,
272
290
  status,
273
291
  duration,
274
292
  errors,
275
293
  diagnosis: ["failed", "timedOut", "interrupted"].includes(status) && primaryError
276
- ? (0, quickDiagnosis_1.parseFailureFacts)(test?.title || titlePath[titlePath.length - 1] || "Untitled test", titlePath, primaryError, status)
294
+ ? (0, quickDiagnosis_1.parseFailureFacts)(cleanTitleParts(titlePath).slice(-1)[0] || test?.title || titlePath[titlePath.length - 1] || "Untitled test", cleanTitleParts(titlePath), primaryError, status, test?.location?.file || null, {
295
+ projectName: test?.projectName || null,
296
+ timeoutBudgetMs: typeof test?.timeout === "number" ? test.timeout : null,
297
+ codeContext: readAttachmentJson(attachments, "sentinel-code-context", baseDirs),
298
+ domCapture: readAttachmentJson(attachments, "sentinel-dom-capture", baseDirs)
299
+ })
277
300
  : null,
278
301
  artifacts: []
279
302
  };
@@ -384,7 +407,7 @@ const buildRunSnapshot = (tests, summary) => ({
384
407
  tests: tests.map((test) => ({
385
408
  id: test.id,
386
409
  matchKey: test.matchKey,
387
- title: test.titlePath.join(" > ") || test.title,
410
+ title: cleanTitleParts(test.titlePath).join(" > ") || test.title,
388
411
  status: test.status,
389
412
  signal: test.diagnosis?.signal || null,
390
413
  locator: test.diagnosis?.locator || null,
@@ -1,24 +1,88 @@
1
- type DiagnosisSignal = "timeout" | "assertion_mismatch" | "locator_not_found" | "actionability" | "network" | "runtime" | "unknown";
2
- type QuickDiagnosis = {
1
+ type DiagnosisSignal = "timeout" | "assertion_mismatch" | "locator_not_found" | "actionability" | "network" | "runtime" | "infra" | "unknown";
2
+ export type QuickDiagnosis = {
3
3
  lines: string[];
4
+ footer?: string[];
5
+ };
6
+ type CodeContextCapture = {
7
+ file?: string | null;
8
+ line?: number | null;
9
+ column?: number | null;
10
+ action?: string | null;
11
+ locator?: string | null;
12
+ expectedText?: string | null;
13
+ timeoutMs?: number | null;
14
+ apiCall?: string | null;
15
+ assertion?: string | null;
16
+ methodName?: string | null;
17
+ focusLine?: string | null;
18
+ previousActionLine?: string | null;
19
+ found?: boolean;
20
+ };
21
+ type DomCapture = {
22
+ locator?: string | null;
23
+ expectedText?: string | null;
24
+ observedText?: string | null;
25
+ captureSource?: "live_page" | "error_fallback";
26
+ matchedCount?: number | null;
27
+ targetFound?: boolean | null;
28
+ visible?: boolean | null;
29
+ attached?: boolean | null;
30
+ enabled?: boolean | null;
31
+ testId?: string | null;
32
+ role?: string | null;
33
+ accessibleName?: string | null;
34
+ textContent?: string | null;
35
+ tagName?: string | null;
36
+ inputType?: string | null;
37
+ placeholder?: string | null;
38
+ ariaLabel?: string | null;
39
+ textAlternatives?: string[] | null;
40
+ matchedElements?: Array<{
41
+ index: number;
42
+ role: string | null;
43
+ accessibleName: string | null;
44
+ visible: boolean | null;
45
+ enabled: boolean | null;
46
+ text: string | null;
47
+ }> | null;
4
48
  };
5
49
  export type FailureFacts = {
6
50
  title: string;
7
51
  titlePath: string[];
52
+ projectName: string | null;
8
53
  message: string;
54
+ firstErrorLine: string | null;
9
55
  signal: DiagnosisSignal;
10
56
  locator: string | null;
11
57
  expected: string | null;
12
58
  received: string | null;
13
59
  timeoutMs: number | null;
60
+ timeoutBudgetMs: number | null;
14
61
  lastUrl: string | null;
15
62
  status: string;
63
+ file: string | null;
64
+ likelyFile: string | null;
65
+ likelyModule: string | null;
66
+ apiHint: string | null;
67
+ codeContext: CodeContextCapture | null;
68
+ domCapture: DomCapture | null;
16
69
  };
17
- export declare const collectFailureFacts: (playwrightJsonPath: string) => FailureFacts[];
18
- export declare const parseFailureFacts: (title: string, titlePath: string[], message: string, status: string) => FailureFacts;
19
70
  export declare const describeFailure: (failure: FailureFacts) => string;
71
+ export declare const parseFailureFacts: (title: string, titlePath: string[], message: string, status: string, file?: string | null, options?: {
72
+ projectName?: string | null;
73
+ timeoutBudgetMs?: number | null;
74
+ codeContext?: CodeContextCapture | null;
75
+ domCapture?: DomCapture | null;
76
+ errorLocation?: {
77
+ file?: string | null;
78
+ line?: number | null;
79
+ column?: number | null;
80
+ } | null;
81
+ errorSnippet?: string | null;
82
+ }) => FailureFacts;
83
+ export declare const collectFailureFacts: (playwrightJsonPath: string) => FailureFacts[];
20
84
  export declare const buildDebugSummary: (failure: FailureFacts) => string;
21
85
  export declare const buildSimilarityKey: (failure: FailureFacts) => string;
22
- export declare const summarizeSignal: (signal: DiagnosisSignal) => "timeout while waiting for UI or network conditions" | "assertion mismatch between expected and rendered UI state" | "missing or changed locator" | "target element was not actionable" | "network or API failure" | "frontend runtime error" | "failure signal could not be classified cleanly";
86
+ export declare const summarizeSignal: (signal: DiagnosisSignal) => "timeout while waiting for UI or network conditions" | "assertion mismatch between expected and rendered UI state" | "missing or changed locator" | "target element was not actionable" | "network or API failure" | "runtime error thrown before the flow completed" | "browser or CI infrastructure failure" | "failure signal could not be classified cleanly";
23
87
  export declare const buildQuickDiagnosis: (playwrightJsonPath: string) => QuickDiagnosis | null;
24
88
  export {};