@trailmark/playwright 0.1.0

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.
Files changed (94) hide show
  1. package/.trailmark/current-run.json +136 -0
  2. package/dist/artifacts/artifactManager.d.ts +31 -0
  3. package/dist/artifacts/artifactManager.d.ts.map +1 -0
  4. package/dist/artifacts/artifactManager.js +140 -0
  5. package/dist/artifacts/artifactManager.js.map +1 -0
  6. package/dist/artifacts/capture.d.ts +4 -0
  7. package/dist/artifacts/capture.d.ts.map +1 -0
  8. package/dist/artifacts/capture.js +16 -0
  9. package/dist/artifacts/capture.js.map +1 -0
  10. package/dist/artifacts/options.d.ts +11 -0
  11. package/dist/artifacts/options.d.ts.map +1 -0
  12. package/dist/artifacts/options.js +55 -0
  13. package/dist/artifacts/options.js.map +1 -0
  14. package/dist/artifacts/registry.d.ts +24 -0
  15. package/dist/artifacts/registry.d.ts.map +1 -0
  16. package/dist/artifacts/registry.js +99 -0
  17. package/dist/artifacts/registry.js.map +1 -0
  18. package/dist/constants.d.ts +2 -0
  19. package/dist/constants.d.ts.map +1 -0
  20. package/dist/constants.js +5 -0
  21. package/dist/constants.js.map +1 -0
  22. package/dist/fixture.d.ts +7 -0
  23. package/dist/fixture.d.ts.map +1 -0
  24. package/dist/fixture.js +193 -0
  25. package/dist/fixture.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +7 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/mode.d.ts +3 -0
  31. package/dist/mode.d.ts.map +1 -0
  32. package/dist/mode.js +9 -0
  33. package/dist/mode.js.map +1 -0
  34. package/dist/reporter/attachments.d.ts +4 -0
  35. package/dist/reporter/attachments.d.ts.map +1 -0
  36. package/dist/reporter/attachments.js +112 -0
  37. package/dist/reporter/attachments.js.map +1 -0
  38. package/dist/reporter/errors.d.ts +4 -0
  39. package/dist/reporter/errors.d.ts.map +1 -0
  40. package/dist/reporter/errors.js +11 -0
  41. package/dist/reporter/errors.js.map +1 -0
  42. package/dist/reporter/index.d.ts +33 -0
  43. package/dist/reporter/index.d.ts.map +1 -0
  44. package/dist/reporter/index.js +407 -0
  45. package/dist/reporter/index.js.map +1 -0
  46. package/dist/reporter/io.d.ts +15 -0
  47. package/dist/reporter/io.d.ts.map +1 -0
  48. package/dist/reporter/io.js +34 -0
  49. package/dist/reporter/io.js.map +1 -0
  50. package/dist/reporter/normalization.d.ts +13 -0
  51. package/dist/reporter/normalization.d.ts.map +1 -0
  52. package/dist/reporter/normalization.js +91 -0
  53. package/dist/reporter/normalization.js.map +1 -0
  54. package/dist/reporter/output.d.ts +5 -0
  55. package/dist/reporter/output.d.ts.map +1 -0
  56. package/dist/reporter/output.js +90 -0
  57. package/dist/reporter/output.js.map +1 -0
  58. package/dist/reporter/run.d.ts +4 -0
  59. package/dist/reporter/run.d.ts.map +1 -0
  60. package/dist/reporter/run.js +18 -0
  61. package/dist/reporter/run.js.map +1 -0
  62. package/dist/types.d.ts +48 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +3 -0
  65. package/dist/types.js.map +1 -0
  66. package/integration/app/base.html +16 -0
  67. package/integration/app/extra.html +17 -0
  68. package/integration/playwright.config.ts +44 -0
  69. package/integration/specs/sample.spec.ts +15 -0
  70. package/integration/tmp/baseline.json +46 -0
  71. package/integration/tmp/report.json +97 -0
  72. package/package.json +35 -0
  73. package/src/artifacts/artifactManager.ts +178 -0
  74. package/src/artifacts/capture.ts +18 -0
  75. package/src/artifacts/options.ts +79 -0
  76. package/src/artifacts/registry.ts +143 -0
  77. package/src/constants.ts +1 -0
  78. package/src/fixture.ts +239 -0
  79. package/src/index.ts +3 -0
  80. package/src/mode.ts +7 -0
  81. package/src/reporter/attachments.ts +139 -0
  82. package/src/reporter/errors.ts +6 -0
  83. package/src/reporter/index.ts +572 -0
  84. package/src/reporter/io.ts +33 -0
  85. package/src/reporter/normalization.ts +122 -0
  86. package/src/reporter/output.ts +103 -0
  87. package/src/reporter/run.ts +17 -0
  88. package/src/types.ts +53 -0
  89. package/test/artifactManager.unit.test.ts +141 -0
  90. package/test/reporter.integration.test.ts +153 -0
  91. package/test/reporter.unit.test.ts +337 -0
  92. package/test-results/.last-run.json +4 -0
  93. package/trailmark-playwright-0.1.0.tgz +0 -0
  94. package/tsconfig.json +8 -0
@@ -0,0 +1,337 @@
1
+ import { Buffer } from "node:buffer"
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
6
+ import {
7
+ BaselineFile,
8
+ buildBaselineFromRun,
9
+ createEmptyRun,
10
+ normalizeAxeResults,
11
+ } from "@trailmark/core"
12
+ import { TRAILMARK_ATTACHMENT_NAME } from "../src/constants"
13
+ import TrailmarkReporter from "../src/reporter"
14
+
15
+ describe("trailmark reporter attachment handling", () => {
16
+ let originalCwd: string
17
+ let tempDir: string
18
+
19
+ beforeEach(() => {
20
+ originalCwd = process.cwd()
21
+ tempDir = mkdtempSync(path.join(tmpdir(), "trailmark-reporter-"))
22
+ process.chdir(tempDir)
23
+ })
24
+
25
+ afterEach(() => {
26
+ rmSync(path.join(tempDir, "baseline.json"), { force: true })
27
+ rmSync(path.join(tempDir, ".trailmark"), { recursive: true, force: true })
28
+ process.chdir(originalCwd)
29
+ rmSync(tempDir, { recursive: true, force: true })
30
+ })
31
+
32
+ it("builds a baseline from attachments nested in test steps", async () => {
33
+ const reporter = new TrailmarkReporter({
34
+ mode: "baseline",
35
+ baselinePath: path.join(tempDir, "baseline.json"),
36
+ reportPath: path.join(tempDir, ".trailmark", "report.json"),
37
+ failOnImpact: ["minor", "moderate", "serious", "critical", "unknown"],
38
+ })
39
+
40
+ reporter.onBegin({ projects: [{ name: "chromium" }] } as never)
41
+
42
+ const run = createEmptyRun()
43
+ run.findings.push(
44
+ ...normalizeAxeResults({
45
+ pageUrl: "file:///tmp/base.html",
46
+ results: {
47
+ violations: [
48
+ {
49
+ id: "image-alt",
50
+ impact: "critical",
51
+ tags: ["wcag2a"],
52
+ nodes: [
53
+ {
54
+ target: ["img.hero"],
55
+ html: '<img class="hero" src="hero.jpg">',
56
+ failureSummary: "Fix any of the following: Element does not have alt text",
57
+ },
58
+ ],
59
+ },
60
+ ],
61
+ },
62
+ }),
63
+ )
64
+
65
+ reporter.onTestEnd(
66
+ {} as never,
67
+ {
68
+ attachments: [],
69
+ steps: [
70
+ {
71
+ attachments: [
72
+ {
73
+ name: TRAILMARK_ATTACHMENT_NAME,
74
+ contentType: "application/json",
75
+ body: Buffer.from(JSON.stringify(run), "utf8"),
76
+ },
77
+ ],
78
+ steps: [],
79
+ },
80
+ ],
81
+ } as never,
82
+ )
83
+
84
+ const endResult = await reporter.onEnd({} as never)
85
+ expect(endResult).toBeUndefined()
86
+
87
+ const baseline = JSON.parse(
88
+ readFileSync(path.join(tempDir, "baseline.json"), "utf8"),
89
+ ) as BaselineFile
90
+ expect(Object.keys(baseline.findings).length).toBeGreaterThan(0)
91
+
92
+ const report = JSON.parse(
93
+ readFileSync(path.join(tempDir, ".trailmark", "report.json"), "utf8"),
94
+ ) as {
95
+ run: { mode: string }
96
+ summary: { totalIssues: number }
97
+ issues: unknown[]
98
+ diff?: unknown
99
+ }
100
+
101
+ expect(report.run.mode).toBe("baseline")
102
+ expect(report.summary.totalIssues).toBeGreaterThan(0)
103
+ expect(report.issues.length).toBeGreaterThan(0)
104
+ expect(report.diff).toBeUndefined()
105
+ })
106
+
107
+ it("aggregates findings across attachments and computes diff in check mode", async () => {
108
+ const baselinePath = path.join(tempDir, "baseline.json")
109
+ const reporter = new TrailmarkReporter({
110
+ mode: "check",
111
+ baselinePath,
112
+ reportPath: path.join(tempDir, ".trailmark", "report.json"),
113
+ failOnImpact: ["serious", "critical"],
114
+ })
115
+
116
+ reporter.onBegin({ projects: [{ name: "chromium" }, { name: "firefox" }] } as never)
117
+
118
+ const baseRun = createEmptyRun()
119
+ baseRun.findings.push(
120
+ ...normalizeAxeResults({
121
+ pageUrl: "https://example.com/base",
122
+ meta: { projectName: "chromium", testId: "t-1" },
123
+ results: {
124
+ violations: [
125
+ {
126
+ id: "image-alt",
127
+ impact: "serious",
128
+ tags: ["wcag2a"],
129
+ nodes: [
130
+ {
131
+ target: ["img.hero"],
132
+ html: '<img data-testid="hero-image" src="hero.jpg">',
133
+ failureSummary: "Element does not have alt text",
134
+ },
135
+ ],
136
+ },
137
+ ],
138
+ },
139
+ }),
140
+ )
141
+
142
+ const baseline = buildBaselineFromRun(baseRun, {
143
+ configHash: "cfg",
144
+ createdAt: "2026-01-01T00:00:00.000Z",
145
+ })
146
+ writeFileSync(baselinePath, JSON.stringify(baseline, null, 2), "utf8")
147
+
148
+ const followupRun = createEmptyRun()
149
+ followupRun.findings.push(...baseRun.findings)
150
+ const newFindings = normalizeAxeResults({
151
+ pageUrl: "https://example.com/extra",
152
+ meta: { projectName: "firefox", testId: "t-2" },
153
+ results: {
154
+ violations: [
155
+ {
156
+ id: "color-contrast",
157
+ impact: "critical",
158
+ tags: ["wcag2aa"],
159
+ nodes: [
160
+ {
161
+ target: ["button.checkout"],
162
+ html: '<button data-testid="checkout">Checkout</button>',
163
+ failureSummary: "Element has insufficient color contrast",
164
+ },
165
+ ],
166
+ },
167
+ ],
168
+ },
169
+ })
170
+ newFindings[0].occurrenceId = "occurrence-firefox-extra"
171
+ newFindings[0].occurredAt = "2026-01-02T00:00:00.000Z"
172
+ newFindings[0].attachments = [
173
+ {
174
+ kind: "viewport",
175
+ path: ".trailmark/artifacts/run-123/critical__color-contrast__example.com_extra__new__viewport.png",
176
+ },
177
+ ]
178
+ followupRun.findings.push(...newFindings)
179
+
180
+ reporter.onTestEnd(
181
+ {} as never,
182
+ {
183
+ attachments: [
184
+ {
185
+ name: TRAILMARK_ATTACHMENT_NAME,
186
+ contentType: "application/json",
187
+ body: Buffer.from(JSON.stringify(baseRun), "utf8"),
188
+ },
189
+ ],
190
+ steps: [],
191
+ } as never,
192
+ )
193
+
194
+ reporter.onTestEnd(
195
+ {} as never,
196
+ {
197
+ attachments: [
198
+ {
199
+ name: TRAILMARK_ATTACHMENT_NAME,
200
+ contentType: "application/json",
201
+ body: Buffer.from(JSON.stringify(followupRun), "utf8"),
202
+ },
203
+ ],
204
+ steps: [],
205
+ } as never,
206
+ )
207
+
208
+ const endResult = await reporter.onEnd({} as never)
209
+ expect(endResult?.status).toBe("failed")
210
+
211
+ const currentRunPath = path.join(tempDir, ".trailmark", "current-run.json")
212
+ const payload = JSON.parse(readFileSync(currentRunPath, "utf8")) as {
213
+ run: { findings: { meta?: { projectName?: string } }[] }
214
+ diff: { existing: unknown[]; newFindings: unknown[]; blockingNew: unknown[] }
215
+ }
216
+
217
+ expect(payload.run.findings).toHaveLength(3)
218
+ const projectNames = new Set(
219
+ payload.run.findings.map((finding) => finding.meta?.projectName).filter(Boolean),
220
+ )
221
+ expect(projectNames).toEqual(new Set(["chromium", "firefox"]))
222
+ expect(payload.diff.existing).toHaveLength(1)
223
+ expect(payload.diff.newFindings).toHaveLength(1)
224
+ expect(payload.diff.blockingNew).toHaveLength(1)
225
+
226
+ const report = JSON.parse(
227
+ readFileSync(path.join(tempDir, ".trailmark", "report.json"), "utf8"),
228
+ ) as {
229
+ summary: { newIssues: number; totalIssues: number }
230
+ diff?: { newIssueIds: string[] }
231
+ issues: Array<{
232
+ issueId: string
233
+ occurrences: Array<{ attachments: Array<{ path?: string }> }>
234
+ }>
235
+ }
236
+
237
+ expect(report.summary.totalIssues).toBe(2)
238
+ expect(report.summary.newIssues).toBe(1)
239
+ expect(report.diff?.newIssueIds).toHaveLength(1)
240
+
241
+ const newIssue = report.issues.find((issue) => issue.issueId === newFindings[0].issueId)
242
+ expect(newIssue).toBeDefined()
243
+ expect(newIssue?.occurrences[0].attachments[0].path).toContain(".trailmark/artifacts/run-123")
244
+ })
245
+
246
+ it("logs baseline delta details when updating an existing baseline", async () => {
247
+ const baselinePath = path.join(tempDir, "baseline.json")
248
+ const reporter = new TrailmarkReporter({
249
+ mode: "baseline",
250
+ baselinePath,
251
+ reportPath: path.join(tempDir, ".trailmark", "report.json"),
252
+ failOnImpact: ["serious", "critical"],
253
+ })
254
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined)
255
+
256
+ reporter.onBegin({ projects: [{ name: "chromium" }] } as never)
257
+
258
+ const previousRun = createEmptyRun()
259
+ previousRun.findings.push(
260
+ ...normalizeAxeResults({
261
+ pageUrl: "https://example.com/base",
262
+ meta: { projectName: "chromium", testId: "t-1" },
263
+ results: {
264
+ violations: [
265
+ {
266
+ id: "image-alt",
267
+ impact: "serious",
268
+ tags: ["wcag2a"],
269
+ nodes: [
270
+ {
271
+ target: ["img.hero"],
272
+ html: '<img data-testid="hero-image" src="hero.jpg">',
273
+ failureSummary: "Element does not have alt text",
274
+ },
275
+ ],
276
+ },
277
+ ],
278
+ },
279
+ }),
280
+ )
281
+ const previousBaseline = buildBaselineFromRun(previousRun, {
282
+ configHash: "cfg",
283
+ createdAt: "2026-01-01T00:00:00.000Z",
284
+ })
285
+ writeFileSync(baselinePath, JSON.stringify(previousBaseline, null, 2), "utf8")
286
+
287
+ const currentRun = createEmptyRun()
288
+ currentRun.findings.push(
289
+ ...normalizeAxeResults({
290
+ pageUrl: "https://example.com/extra",
291
+ meta: { projectName: "chromium", testId: "t-2" },
292
+ results: {
293
+ violations: [
294
+ {
295
+ id: "color-contrast",
296
+ impact: "critical",
297
+ tags: ["wcag2aa"],
298
+ nodes: [
299
+ {
300
+ target: ["button.checkout"],
301
+ html: '<button data-testid="checkout">Checkout</button>',
302
+ failureSummary: "Element has insufficient color contrast",
303
+ },
304
+ ],
305
+ },
306
+ ],
307
+ },
308
+ }),
309
+ )
310
+
311
+ reporter.onTestEnd(
312
+ {} as never,
313
+ {
314
+ attachments: [
315
+ {
316
+ name: TRAILMARK_ATTACHMENT_NAME,
317
+ contentType: "application/json",
318
+ body: Buffer.from(JSON.stringify(currentRun), "utf8"),
319
+ },
320
+ ],
321
+ steps: [],
322
+ } as never,
323
+ )
324
+
325
+ const endResult = await reporter.onEnd({} as never)
326
+ expect(endResult).toBeUndefined()
327
+
328
+ const logged = logSpy.mock.calls.map((call) => call.join(" ")).join("\n")
329
+ expect(logged).toContain("trailmark: baseline delta vs previous")
330
+ expect(logged).toContain("new=1")
331
+ expect(logged).toContain("resolved=1")
332
+ expect(logged).toContain("existing=0")
333
+ expect(logged).toContain("trailmark: baseline delta impacts (new): critical=1")
334
+
335
+ logSpy.mockRestore()
336
+ })
337
+ })
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }
Binary file
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*.ts"]
8
+ }