@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,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.summarizeRunEnvelope = summarizeRunEnvelope;
4
+ exports.summarizeScanErrors = summarizeScanErrors;
5
+ exports.summarizeNewFindings = summarizeNewFindings;
6
+ function impactWeight(impact) {
7
+ switch ((impact ?? "unknown").toLowerCase()) {
8
+ case "critical":
9
+ return 0;
10
+ case "serious":
11
+ return 1;
12
+ case "moderate":
13
+ return 2;
14
+ case "minor":
15
+ return 3;
16
+ default:
17
+ return 4;
18
+ }
19
+ }
20
+ function summarizeRunEnvelope(sourceRuns, findingsCount) {
21
+ if (sourceRuns === 0) {
22
+ console.log("trailmark: 0 scans / no findings.");
23
+ return;
24
+ }
25
+ if (findingsCount === 0) {
26
+ console.log(`trailmark: ${sourceRuns} scan run(s), 0 findings.`);
27
+ }
28
+ }
29
+ function summarizeScanErrors(scanErrors) {
30
+ if (scanErrors.length === 0) {
31
+ return;
32
+ }
33
+ console.error(`trailmark: ${scanErrors.length} scan error(s) captured during run.`);
34
+ for (const scanError of scanErrors.slice(0, 10)) {
35
+ const location = scanError.pageKey ?? scanError.pageUrl ?? "<unknown-page>";
36
+ console.error(`- ${location} :: ${scanError.message}`);
37
+ }
38
+ }
39
+ function summarizeNewFindings(diff) {
40
+ if (diff.newFindings.length === 0) {
41
+ console.log("trailmark: no new accessibility violations compared to baseline.");
42
+ return;
43
+ }
44
+ const byImpact = Object.entries(diff.stats.newByImpact)
45
+ .sort((left, right) => impactWeight(left[0]) - impactWeight(right[0]))
46
+ .map(([impact, count]) => `${impact}:${count}`)
47
+ .join(", ");
48
+ const ruleCounts = new Map();
49
+ for (const finding of diff.newFindings) {
50
+ ruleCounts.set(finding.ruleId, (ruleCounts.get(finding.ruleId) ?? 0) + 1);
51
+ }
52
+ const topRules = Array.from(ruleCounts.entries())
53
+ .sort((left, right) => {
54
+ const countDiff = right[1] - left[1];
55
+ if (countDiff !== 0) {
56
+ return countDiff;
57
+ }
58
+ return left[0].localeCompare(right[0]);
59
+ })
60
+ .slice(0, 5)
61
+ .map(([ruleId, count]) => `${ruleId}:${count}`)
62
+ .join(", ");
63
+ console.log(`trailmark: new=${diff.stats.newCount} existing=${diff.stats.existingCount} resolved=${diff.stats.resolvedCount}.`);
64
+ console.log(`trailmark: new by impact -> ${byImpact || "none"}`);
65
+ console.log(`trailmark: top new rules -> ${topRules || "none"}`);
66
+ const sortedFindings = [...diff.newFindings].sort((left, right) => {
67
+ const impactDiff = impactWeight(left.impact) -
68
+ impactWeight(right.impact);
69
+ if (impactDiff !== 0) {
70
+ return impactDiff;
71
+ }
72
+ const ruleDiff = left.ruleId.localeCompare(right.ruleId);
73
+ if (ruleDiff !== 0) {
74
+ return ruleDiff;
75
+ }
76
+ const pageDiff = left.pageKey.localeCompare(right.pageKey);
77
+ if (pageDiff !== 0) {
78
+ return pageDiff;
79
+ }
80
+ const leftSelector = left.target[0] ?? left.selectorHint ?? "";
81
+ const rightSelector = right.target[0] ?? right.selectorHint ?? "";
82
+ return leftSelector.localeCompare(rightSelector);
83
+ });
84
+ for (const finding of sortedFindings) {
85
+ const impact = (finding.impact ?? "unknown").toLowerCase();
86
+ const selector = finding.target[0] ?? finding.selectorHint ?? "<unknown-target>";
87
+ console.log(`- ${impact} | ${finding.ruleId} | ${finding.pageKey} | ${selector}`);
88
+ }
89
+ }
90
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/reporter/output.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { TrailmarkFindingMeta, TrailmarkRun } from "@trailmark/core";
2
+ export declare function withRunShape(run: TrailmarkRun): TrailmarkRun;
3
+ export declare function scopeKeyFromMetaLocal(meta: TrailmarkFindingMeta | undefined): string;
4
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/reporter/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAEpE,wBAAgB,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,YAAY,CAS5D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB,GAAG,SAAS,GAAG,MAAM,CAGpF"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withRunShape = withRunShape;
4
+ exports.scopeKeyFromMetaLocal = scopeKeyFromMetaLocal;
5
+ function withRunShape(run) {
6
+ if (Array.isArray(run.scanErrors)) {
7
+ return run;
8
+ }
9
+ return {
10
+ ...run,
11
+ scanErrors: [],
12
+ };
13
+ }
14
+ function scopeKeyFromMetaLocal(meta) {
15
+ const normalized = meta?.projectName?.trim().toLowerCase();
16
+ return normalized || "default";
17
+ }
18
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/reporter/run.ts"],"names":[],"mappings":";;AAEA,oCASC;AAED,sDAGC;AAdD,SAAgB,YAAY,CAAC,GAAiB;IAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,OAAO;QACL,GAAG,GAAG;QACN,UAAU,EAAE,EAAE;KACf,CAAA;AACH,CAAC;AAED,SAAgB,qBAAqB,CAAC,IAAsC;IAC1E,MAAM,UAAU,GAAG,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAC1D,OAAO,UAAU,IAAI,SAAS,CAAA;AAChC,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { Page } from "@playwright/test";
2
+ import { TrailmarkFindingMeta, TrailmarkRun, UrlNormalizationOptions } from "@trailmark/core";
3
+ export interface TrailmarkAxeOptions {
4
+ include?: string[];
5
+ exclude?: string[];
6
+ tags?: string[];
7
+ rules?: Record<string, {
8
+ enabled: boolean;
9
+ }>;
10
+ disableRules?: string[];
11
+ }
12
+ export type TrailmarkArtifactCaptureMode = "new-only" | "all";
13
+ export interface TrailmarkArtifactOptions {
14
+ enabled?: boolean;
15
+ outputDir?: string;
16
+ limitPerRun?: number;
17
+ capture?: TrailmarkArtifactCaptureMode;
18
+ includeViewport?: boolean;
19
+ includeElement?: boolean;
20
+ }
21
+ export interface ScanOptions {
22
+ enabled?: boolean;
23
+ continueOnError?: boolean;
24
+ axe?: TrailmarkAxeOptions;
25
+ url?: UrlNormalizationOptions;
26
+ artifacts?: TrailmarkArtifactOptions;
27
+ meta?: Partial<TrailmarkFindingMeta>;
28
+ scanLabel?: string;
29
+ }
30
+ export interface TrailmarkFixture {
31
+ scan(page: Page, opts?: ScanOptions): Promise<void>;
32
+ getRun(): TrailmarkRun;
33
+ }
34
+ export type TrailmarkMode = "baseline" | "check";
35
+ export type TrailmarkModeOptions = {
36
+ mode?: TrailmarkMode;
37
+ };
38
+ export type TrailmarkReporterOptions = {
39
+ baselinePath?: string;
40
+ reportPath?: string;
41
+ failOnImpact?: string[];
42
+ failOnScanError?: boolean;
43
+ onConfigMismatch?: "warn" | "error" | "ignore";
44
+ verbose?: boolean;
45
+ url?: UrlNormalizationOptions;
46
+ artifacts?: TrailmarkArtifactOptions;
47
+ } & TrailmarkModeOptions;
48
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AACvC,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AAE7F,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAC5C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AAED,MAAM,MAAM,4BAA4B,GAAG,UAAU,GAAG,KAAK,CAAA;AAE7D,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,4BAA4B,CAAA;IACtC,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,GAAG,CAAC,EAAE,mBAAmB,CAAA;IACzB,GAAG,CAAC,EAAE,uBAAuB,CAAA;IAC7B,SAAS,CAAC,EAAE,wBAAwB,CAAA;IACpC,IAAI,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAA;IACpC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,IAAI,YAAY,CAAA;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,OAAO,CAAA;AAEhD,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,CAAC,EAAE,aAAa,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,GAAG,CAAC,EAAE,uBAAuB,CAAA;IAC7B,SAAS,CAAC,EAAE,wBAAwB,CAAA;CACrC,GAAG,oBAAoB,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Trailmark Sample Base</title>
7
+ </head>
8
+ <body>
9
+ <main>
10
+ <h1>Checkout</h1>
11
+ <p>Sample page used for Trailmark integration tests.</p>
12
+ <img data-testid="hero-image" src="hero.jpg" />
13
+ <button data-testid="continue-btn" aria-label="Continue to payment">Continue</button>
14
+ </main>
15
+ </body>
16
+ </html>
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Trailmark Sample Extra</title>
7
+ </head>
8
+ <body>
9
+ <main>
10
+ <h1>Checkout</h1>
11
+ <p>Sample page used for Trailmark integration tests.</p>
12
+ <img data-testid="hero-image" src="hero.jpg" />
13
+ <button data-testid="continue-btn" aria-label="Continue to payment">Continue</button>
14
+ <button data-testid="unnamed-action"></button>
15
+ </main>
16
+ </body>
17
+ </html>
@@ -0,0 +1,44 @@
1
+ import path from "node:path"
2
+ import { defineConfig } from "@playwright/test"
3
+
4
+ const mode = (process.env.TRAILMARK_IT_MODE as "baseline" | "check" | undefined) ?? "check"
5
+
6
+ export default defineConfig({
7
+ testDir: path.resolve(__dirname, "specs"),
8
+ fullyParallel: false,
9
+ workers: 1,
10
+ timeout: 30_000,
11
+ reporter: [
12
+ ["line"],
13
+ [
14
+ path.resolve(__dirname, "../dist/reporter/index.js"),
15
+ {
16
+ baselinePath: path.resolve(__dirname, "tmp", "baseline.json"),
17
+ reportPath: path.resolve(__dirname, "tmp", "report.json"),
18
+ mode,
19
+ failOnImpact: ["minor", "moderate", "serious", "critical", "unknown"],
20
+ url: {
21
+ stripHash: true,
22
+ stripQuery: true,
23
+ routePatterns: [
24
+ {
25
+ match: "^.*\\/integration\\/app\\/(base|extra)\\.html$",
26
+ replace: "sample-app/page.html",
27
+ },
28
+ ],
29
+ },
30
+ },
31
+ ],
32
+ ],
33
+ use: {
34
+ headless: true,
35
+ },
36
+ projects: [
37
+ {
38
+ name: "chromium",
39
+ use: {
40
+ browserName: "chromium",
41
+ },
42
+ },
43
+ ],
44
+ })
@@ -0,0 +1,15 @@
1
+ import path from "node:path"
2
+ import { pathToFileURL } from "node:url"
3
+ import { test, expect } from "../../src"
4
+
5
+ const targetPage = process.env.TRAILMARK_TARGET ?? "base"
6
+
7
+ test("trailmark sample app scan", async ({ page, trailmark }) => {
8
+ const appPath = path.resolve(__dirname, "..", "app", `${targetPage}.html`)
9
+ await page.goto(pathToFileURL(appPath).href)
10
+
11
+ await trailmark.scan(page)
12
+
13
+ const run = trailmark.getRun()
14
+ expect(run.findings.length).toBeGreaterThan(0)
15
+ })
@@ -0,0 +1,46 @@
1
+ {
2
+ "version": 1,
3
+ "createdAt": "2026-02-08T19:53:46.892Z",
4
+ "tool": {
5
+ "name": "trailmark",
6
+ "version": "0.1.0",
7
+ "scanner": "@axe-core/playwright"
8
+ },
9
+ "configHash": "bf23043ed4b93b2119d6c9048bc54afdcfa1b9b9",
10
+ "findings": {
11
+ "chromium::afbdf52e9915d0b0e73041501018ec09de569a5b": {
12
+ "issueId": "afbdf52e9915d0b0e73041501018ec09de569a5b",
13
+ "scopeKey": "chromium",
14
+ "projectName": "chromium",
15
+ "relaxedIssueId": "85362a09993122b59cdcb30d0d1682654e2bf705",
16
+ "ruleId": "image-alt",
17
+ "impact": "critical",
18
+ "pageKey": "sample-app/page.html",
19
+ "pageUrl": "file:///Users/dmitrijsminajevs/dev/axe/packages/trailmark-playwright/integration/app/base.html",
20
+ "targetHash": "affe482b172ca2ef1a2ef338d49db47e91b1f05e",
21
+ "selectorHint": "img",
22
+ "sampleTarget": "img",
23
+ "help": "Images must have alternative text",
24
+ "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/image-alt?application=playwright",
25
+ "tags": [
26
+ "ACT",
27
+ "cat.text-alternatives",
28
+ "EN-301-549",
29
+ "EN-9.1.1.1",
30
+ "RGAA-1.1.1",
31
+ "RGAAv4",
32
+ "section508",
33
+ "section508.22.a",
34
+ "TT7.a",
35
+ "TT7.b",
36
+ "TTv5",
37
+ "wcag111",
38
+ "wcag2a"
39
+ ],
40
+ "failureHint": "fix any of the following: element does not have an alt attribute aria-label attribute does not exist or is empty aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty element has no title attribute elements default semantics were not overridden with role=none or role=presentation",
41
+ "occurrences": 1,
42
+ "firstSeenAt": "2026-02-08T19:53:46.892Z",
43
+ "lastSeenAt": "2026-02-08T19:53:46.892Z"
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,97 @@
1
+ {
2
+ "run": {
3
+ "reportVersion": 2,
4
+ "mode": "check",
5
+ "createdAt": "2026-02-08T20:11:55.645Z",
6
+ "startedAt": "2026-02-08T20:11:53.970Z",
7
+ "finishedAt": "2026-02-08T20:11:55.643Z",
8
+ "configHash": "bf23043ed4b93b2119d6c9048bc54afdcfa1b9b9",
9
+ "tool": {
10
+ "name": "trailmark",
11
+ "version": "0.1.0",
12
+ "scanner": "@axe-core/playwright"
13
+ },
14
+ "engine": {
15
+ "scanner": "@axe-core/playwright",
16
+ "runtime": "playwright"
17
+ },
18
+ "metadata": {
19
+ "sourceRuns": 1,
20
+ "artifactRunIds": [
21
+ "20260208T201155296Z"
22
+ ],
23
+ "artifactWarnings": [],
24
+ "artifactIndex": {
25
+ "byOccurrenceId": {},
26
+ "byIssueId": {}
27
+ },
28
+ "scanCount": 1,
29
+ "skippedScanCount": 0
30
+ }
31
+ },
32
+ "summary": {
33
+ "scannedPages": 1,
34
+ "totalIssues": 1,
35
+ "totalOccurrences": 1,
36
+ "scanErrors": 0,
37
+ "newIssues": 0,
38
+ "existingIssues": 1,
39
+ "resolvedIssues": 0,
40
+ "byImpact": {
41
+ "critical": 1
42
+ },
43
+ "byRuleId": {
44
+ "image-alt": 1
45
+ },
46
+ "byPageKey": {
47
+ "sample-app/page.html": 1
48
+ }
49
+ },
50
+ "issues": [
51
+ {
52
+ "issueId": "afbdf52e9915d0b0e73041501018ec09de569a5b",
53
+ "ruleId": "image-alt",
54
+ "impact": "critical",
55
+ "tags": [
56
+ "ACT",
57
+ "cat.text-alternatives",
58
+ "EN-301-549",
59
+ "EN-9.1.1.1",
60
+ "RGAA-1.1.1",
61
+ "RGAAv4",
62
+ "section508",
63
+ "section508.22.a",
64
+ "TT7.a",
65
+ "TT7.b",
66
+ "TTv5",
67
+ "wcag111",
68
+ "wcag2a"
69
+ ],
70
+ "pageKey": "sample-app/page.html",
71
+ "url": "file:///Users/dmitrijsminajevs/dev/axe/packages/trailmark-playwright/integration/app/base.html",
72
+ "target": [
73
+ "img"
74
+ ],
75
+ "selectorHint": "img",
76
+ "failureSummary": "Fix any of the following:\n Element does not have an alt attribute\n aria-label attribute does not exist or is empty\n aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n Element has no title attribute\n Element's default semantics were not overridden with role=\"none\" or role=\"presentation\"",
77
+ "occurrences": [
78
+ {
79
+ "occurrenceId": "3473dff9eb8fb607ca9dcb0f558fb82305e1a033",
80
+ "testId": "b92766bcf73bd5c029a8-4f0088c96308a9a97a42",
81
+ "projectName": "chromium",
82
+ "url": "file:///Users/dmitrijsminajevs/dev/axe/packages/trailmark-playwright/integration/app/base.html",
83
+ "timestamp": "2026-02-08T20:11:55.607Z",
84
+ "attachments": []
85
+ }
86
+ ]
87
+ }
88
+ ],
89
+ "diff": {
90
+ "newIssueIds": [],
91
+ "existingIssueIds": [
92
+ "afbdf52e9915d0b0e73041501018ec09de569a5b"
93
+ ],
94
+ "resolvedIssueIds": [],
95
+ "newIssues": []
96
+ }
97
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@trailmark/playwright",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./reporter": {
12
+ "types": "./dist/reporter/index.d.ts",
13
+ "default": "./dist/reporter/index.js"
14
+ }
15
+ },
16
+ "dependencies": {
17
+ "@axe-core/playwright": "^4.10.2",
18
+ "@trailmark/core": "0.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "@playwright/test": "^1.49.1",
22
+ "@types/node": "^22.10.5"
23
+ },
24
+ "peerDependencies": {
25
+ "@playwright/test": ">=1.40.0"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "test": "pnpm -r build && vitest run --passWithNoTests --exclude test/reporter.integration.test.ts --exclude integration/specs/**",
30
+ "test:integration": "pnpm -r build && vitest run test/reporter.integration.test.ts",
31
+ "typecheck": "tsc -p tsconfig.json --noEmit",
32
+ "sample:baseline": "pnpm -r build && TRAILMARK_MODE=baseline playwright test -c integration/playwright.config.ts integration/specs/sample.spec.ts",
33
+ "sample:check": "pnpm -r build && playwright test -c integration/playwright.config.ts integration/specs/sample.spec.ts"
34
+ }
35
+ }
@@ -0,0 +1,178 @@
1
+ import { mkdirSync } from "node:fs"
2
+ import path from "node:path"
3
+ import { Page } from "@playwright/test"
4
+ import { TrailmarkAttachment, TrailmarkFinding } from "@trailmark/core"
5
+ import { captureElementScreenshot, captureViewportScreenshot } from "./capture"
6
+ import { ResolvedTrailmarkArtifactOptions } from "./options"
7
+
8
+ function impactKey(impact: TrailmarkFinding["impact"]): string {
9
+ if (!impact) {
10
+ return "unknown"
11
+ }
12
+
13
+ return String(impact).toLowerCase()
14
+ }
15
+
16
+ export function sanitizeArtifactSegment(input: string, maxLength = 60): string {
17
+ const cleaned = input
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9._-]+/g, "_")
20
+ .replace(/_+/g, "_")
21
+ .replace(/^_+|_+$/g, "")
22
+
23
+ if (!cleaned) {
24
+ return "unknown"
25
+ }
26
+
27
+ return cleaned.slice(0, maxLength)
28
+ }
29
+
30
+ function toPosixPath(filePath: string): string {
31
+ return filePath.split(path.sep).join(path.posix.sep)
32
+ }
33
+
34
+ function toRelativeArtifactPath(absolutePath: string): string {
35
+ const relative = path.relative(process.cwd(), absolutePath)
36
+ return toPosixPath(relative)
37
+ }
38
+
39
+ export function createArtifactRunId(date: Date = new Date()): string {
40
+ const iso = date.toISOString()
41
+ return iso.replace(/[-:.]/g, "").replace(".000", "")
42
+ }
43
+
44
+ export interface ArtifactManagerOptions extends ResolvedTrailmarkArtifactOptions {
45
+ runId: string
46
+ }
47
+
48
+ export interface ArtifactCaptureContext {
49
+ page: Page
50
+ finding: TrailmarkFinding
51
+ }
52
+
53
+ export interface ArtifactCaptureResult {
54
+ attachments: TrailmarkAttachment[]
55
+ skipped: boolean
56
+ }
57
+
58
+ export class ArtifactManager {
59
+ private readonly runDir: string
60
+
61
+ private readonly stemCounters = new Map<string, number>()
62
+
63
+ private capturedIssues = 0
64
+
65
+ private limitWarningEmitted = false
66
+
67
+ private readonly warnings: string[] = []
68
+
69
+ constructor(private readonly options: ArtifactManagerOptions) {
70
+ this.runDir = path.resolve(options.outputDir, options.runId)
71
+ if (options.enabled && (options.includeViewport || options.includeElement)) {
72
+ mkdirSync(this.runDir, { recursive: true })
73
+ }
74
+ }
75
+
76
+ getRunId(): string {
77
+ return this.options.runId
78
+ }
79
+
80
+ getWarnings(): string[] {
81
+ return [...this.warnings]
82
+ }
83
+
84
+ private nextStem(finding: TrailmarkFinding): string {
85
+ const baseStem = [
86
+ sanitizeArtifactSegment(impactKey(finding.impact)),
87
+ sanitizeArtifactSegment(finding.ruleId),
88
+ sanitizeArtifactSegment(finding.pageKey),
89
+ sanitizeArtifactSegment(finding.issueId.slice(0, 12)),
90
+ ].join("__")
91
+
92
+ const currentCount = (this.stemCounters.get(baseStem) ?? 0) + 1
93
+ this.stemCounters.set(baseStem, currentCount)
94
+
95
+ if (currentCount === 1) {
96
+ return baseStem
97
+ }
98
+
99
+ return `${baseStem}__${currentCount}`
100
+ }
101
+
102
+ private canCaptureIssue(): boolean {
103
+ if (!this.options.enabled) {
104
+ return false
105
+ }
106
+
107
+ if (this.capturedIssues < this.options.limitPerRun) {
108
+ return true
109
+ }
110
+
111
+ if (!this.limitWarningEmitted) {
112
+ this.limitWarningEmitted = true
113
+ this.warnings.push(
114
+ `trailmark: artifact capture limit reached (${this.options.limitPerRun}); skipping remaining captures.`,
115
+ )
116
+ }
117
+
118
+ return false
119
+ }
120
+
121
+ async captureForIssue(context: ArtifactCaptureContext): Promise<ArtifactCaptureResult> {
122
+ if (!this.canCaptureIssue()) {
123
+ return { attachments: [], skipped: true }
124
+ }
125
+
126
+ this.capturedIssues += 1
127
+
128
+ const attachments: TrailmarkAttachment[] = []
129
+ const stem = this.nextStem(context.finding)
130
+
131
+ if (this.options.includeViewport) {
132
+ const outputPath = path.join(this.runDir, `${stem}__viewport.png`)
133
+
134
+ try {
135
+ await captureViewportScreenshot(context.page, outputPath)
136
+ attachments.push({
137
+ kind: "viewport",
138
+ path: toRelativeArtifactPath(outputPath),
139
+ })
140
+ } catch (error) {
141
+ attachments.push({
142
+ kind: "viewport",
143
+ error: error instanceof Error ? error.message : String(error),
144
+ })
145
+ }
146
+ }
147
+
148
+ if (this.options.includeElement) {
149
+ const selector = context.finding.target[0] ?? context.finding.selectorHint
150
+ if (!selector) {
151
+ attachments.push({
152
+ kind: "element",
153
+ error: "No selector available for element screenshot capture.",
154
+ })
155
+ } else {
156
+ const outputPath = path.join(this.runDir, `${stem}__element.png`)
157
+
158
+ try {
159
+ await captureElementScreenshot(context.page, selector, outputPath)
160
+ attachments.push({
161
+ kind: "element",
162
+ path: toRelativeArtifactPath(outputPath),
163
+ })
164
+ } catch (error) {
165
+ attachments.push({
166
+ kind: "element",
167
+ error: error instanceof Error ? error.message : String(error),
168
+ })
169
+ }
170
+ }
171
+ }
172
+
173
+ return {
174
+ attachments,
175
+ skipped: false,
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,18 @@
1
+ import { Page } from "@playwright/test"
2
+
3
+ export async function captureViewportScreenshot(page: Page, outputPath: string): Promise<void> {
4
+ await page.screenshot({
5
+ path: outputPath,
6
+ fullPage: true,
7
+ })
8
+ }
9
+
10
+ export async function captureElementScreenshot(
11
+ page: Page,
12
+ selector: string,
13
+ outputPath: string,
14
+ ): Promise<void> {
15
+ await page.locator(selector).first().screenshot({
16
+ path: outputPath,
17
+ })
18
+ }