@lullabot/playwright-testing 0.1.0 → 1.0.1
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/bin/github-a11y-summary +2 -2
- package/bin/github-failure-summary +4 -4
- package/lib/accessibility-baseline-file.d.ts +6 -6
- package/lib/accessibility-baseline-file.js +29 -23
- package/lib/accessibility-baseline.js +5 -3
- package/lib/accessible-screenshot.d.ts +4 -4
- package/lib/accessible-screenshot.js +88 -76
- package/lib/focus.js +4 -1
- package/lib/frames.js +4 -4
- package/lib/github/a11y-summary.js +74 -79
- package/lib/github/attachments.d.ts +1 -1
- package/lib/github/attachments.js +36 -34
- package/lib/github/failure-summary.d.ts +4 -4
- package/lib/github/failure-summary.js +139 -107
- package/lib/github/index.d.ts +6 -6
- package/lib/github/report-paths.js +17 -9
- package/lib/hover.js +17 -17
- package/lib/images.js +15 -12
- package/lib/index.d.ts +13 -13
- package/lib/interaction-states.d.ts +2 -2
- package/lib/interaction-states.js +7 -7
- package/lib/mock/index.d.ts +1 -1
- package/lib/mock/youtube.d.ts +1 -1
- package/lib/mock/youtube.js +1 -1
- package/lib/pseudo-state.d.ts +2 -2
- package/lib/pseudo-state.js +15 -7
- package/lib/videos.js +21 -10
- package/lib/visualdiff.d.ts +4 -4
- package/lib/visualdiff.js +35 -27
- package/package.json +3 -3
- package/src/accessibility-baseline-file.test.ts +190 -154
- package/src/accessibility-baseline-file.ts +107 -83
- package/src/accessibility-baseline.test.ts +633 -433
- package/src/accessibility-baseline.ts +41 -21
- package/src/accessible-screenshot.test.ts +551 -423
- package/src/accessible-screenshot.ts +334 -245
- package/src/focus.test.ts +23 -23
- package/src/focus.ts +7 -2
- package/src/fonts.test.ts +13 -13
- package/src/fonts.ts +1 -1
- package/src/frames.test.ts +10 -8
- package/src/frames.ts +5 -5
- package/src/github/a11y-summary.test.ts +436 -332
- package/src/github/a11y-summary.ts +189 -179
- package/src/github/attachments.test.ts +228 -167
- package/src/github/attachments.ts +144 -121
- package/src/github/failure-summary.test.ts +659 -445
- package/src/github/failure-summary.ts +376 -274
- package/src/github/index.test.ts +14 -14
- package/src/github/index.ts +6 -6
- package/src/github/report-paths.test.ts +211 -156
- package/src/github/report-paths.ts +75 -61
- package/src/hover.test.ts +50 -46
- package/src/hover.ts +18 -18
- package/src/images.test.ts +251 -206
- package/src/images.ts +52 -43
- package/src/index.ts +13 -13
- package/src/interaction-states.test.ts +72 -42
- package/src/interaction-states.ts +43 -29
- package/src/mock/index.ts +1 -1
- package/src/mock/youtube.test.ts +26 -26
- package/src/mock/youtube.ts +4 -6
- package/src/pseudo-state.test.ts +58 -51
- package/src/pseudo-state.ts +32 -27
- package/src/videos.test.ts +559 -399
- package/src/videos.ts +68 -44
- package/src/visualdiff.test.ts +416 -315
- package/src/visualdiff.ts +140 -99
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as path from
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
3
|
|
|
4
4
|
/** Annotation types that identify accessibility-relevant test data. */
|
|
5
5
|
const A11Y_ANNOTATION_TYPES = new Set([
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
])
|
|
6
|
+
"@a11y",
|
|
7
|
+
"Accessibility",
|
|
8
|
+
"Baselined a11y violation",
|
|
9
|
+
"Stale a11y baseline entry",
|
|
10
|
+
]);
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Escape a string for use inside a GitHub Actions workflow-command message
|
|
@@ -17,9 +17,9 @@ const A11Y_ANNOTATION_TYPES = new Set([
|
|
|
17
17
|
*/
|
|
18
18
|
function escapeWorkflowCommand(value: string): string {
|
|
19
19
|
return String(value)
|
|
20
|
-
.replace(/%/g,
|
|
21
|
-
.replace(/\r/g,
|
|
22
|
-
.replace(/\n/g,
|
|
20
|
+
.replace(/%/g, "%25")
|
|
21
|
+
.replace(/\r/g, "%0D")
|
|
22
|
+
.replace(/\n/g, "%0A");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -28,9 +28,7 @@ function escapeWorkflowCommand(value: string): string {
|
|
|
28
28
|
* escaping of `:` and `,` on top of the message-body escapes.
|
|
29
29
|
*/
|
|
30
30
|
function escapeWorkflowCommandProperty(value: string): string {
|
|
31
|
-
return escapeWorkflowCommand(value)
|
|
32
|
-
.replace(/:/g, '%3A')
|
|
33
|
-
.replace(/,/g, '%2C')
|
|
31
|
+
return escapeWorkflowCommand(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
/**
|
|
@@ -40,9 +38,9 @@ function escapeWorkflowCommandProperty(value: string): string {
|
|
|
40
38
|
*/
|
|
41
39
|
function escapeMarkdownTableCell(value: string): string {
|
|
42
40
|
return String(value)
|
|
43
|
-
.replace(/\\/g,
|
|
44
|
-
.replace(/\|/g,
|
|
45
|
-
.replace(/\r?\n/g,
|
|
41
|
+
.replace(/\\/g, "\\\\")
|
|
42
|
+
.replace(/\|/g, "\\|")
|
|
43
|
+
.replace(/\r?\n/g, " ");
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
/**
|
|
@@ -52,39 +50,39 @@ function escapeMarkdownTableCell(value: string): string {
|
|
|
52
50
|
* with spaces, which is the standard CommonMark escape for code spans.
|
|
53
51
|
*/
|
|
54
52
|
function escapeMarkdownCodeSpan(value: string): string {
|
|
55
|
-
const s = String(value).replace(/\r?\n/g,
|
|
56
|
-
if (!s.includes(
|
|
57
|
-
return `\`\` ${s.replace(/``/g,
|
|
53
|
+
const s = String(value).replace(/\r?\n/g, " ");
|
|
54
|
+
if (!s.includes("`")) return `\`${s}\``;
|
|
55
|
+
return `\`\` ${s.replace(/``/g, "` `")} \`\``;
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
/** A test that has accessibility-related annotations and/or attachments. */
|
|
61
59
|
export interface A11yTestResult {
|
|
62
60
|
/** Test title path, e.g. "a11y > homepage has accessibility violations" */
|
|
63
|
-
title: string
|
|
61
|
+
title: string;
|
|
64
62
|
/** Source file, e.g. "tests/a11y.spec.ts" */
|
|
65
|
-
file: string
|
|
63
|
+
file: string;
|
|
66
64
|
/** Line number in the source file. */
|
|
67
|
-
line: number
|
|
65
|
+
line: number;
|
|
68
66
|
/** Accessibility annotations from testInfo.annotations. */
|
|
69
|
-
annotations: Array<{ type: string; description?: string }
|
|
67
|
+
annotations: Array<{ type: string; description?: string }>;
|
|
70
68
|
/** Parsed WCAG scan violations (from a11y-wcag-scan-results attachment). */
|
|
71
|
-
violations: NormalizedViolation[]
|
|
69
|
+
violations: NormalizedViolation[];
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
interface NormalizedViolation {
|
|
75
|
-
rule: string
|
|
76
|
-
impact: string
|
|
77
|
-
description: string
|
|
78
|
-
helpUrl: string
|
|
79
|
-
targets: string[]
|
|
73
|
+
rule: string;
|
|
74
|
+
impact: string;
|
|
75
|
+
description: string;
|
|
76
|
+
helpUrl: string;
|
|
77
|
+
targets: string[];
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
/** Parsed report with only accessibility-relevant data. */
|
|
83
81
|
export interface A11yReport {
|
|
84
|
-
tests: A11yTestResult[]
|
|
85
|
-
totalViolations: number
|
|
86
|
-
totalBaselined: number
|
|
87
|
-
totalStale: number
|
|
82
|
+
tests: A11yTestResult[];
|
|
83
|
+
totalViolations: number;
|
|
84
|
+
totalBaselined: number;
|
|
85
|
+
totalStale: number;
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
/**
|
|
@@ -94,27 +92,27 @@ export function parseA11yResults(reportPath: string): A11yReport {
|
|
|
94
92
|
if (!fs.existsSync(reportPath)) {
|
|
95
93
|
throw new Error(
|
|
96
94
|
`Playwright JSON report not found at ${reportPath}. ` +
|
|
97
|
-
|
|
98
|
-
)
|
|
95
|
+
"Ensure the Playwright JSON reporter is enabled.",
|
|
96
|
+
);
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
const report = JSON.parse(fs.readFileSync(reportPath,
|
|
102
|
-
const tests: A11yTestResult[] = []
|
|
103
|
-
let totalViolations = 0
|
|
104
|
-
let totalBaselined = 0
|
|
105
|
-
let totalStale = 0
|
|
99
|
+
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
100
|
+
const tests: A11yTestResult[] = [];
|
|
101
|
+
let totalViolations = 0;
|
|
102
|
+
let totalBaselined = 0;
|
|
103
|
+
let totalStale = 0;
|
|
106
104
|
|
|
107
|
-
walkSuites(report.suites ?? [],
|
|
105
|
+
walkSuites(report.suites ?? [], "", tests);
|
|
108
106
|
|
|
109
107
|
for (const test of tests) {
|
|
110
108
|
for (const ann of test.annotations) {
|
|
111
|
-
if (ann.type ===
|
|
112
|
-
if (ann.type ===
|
|
109
|
+
if (ann.type === "Baselined a11y violation") totalBaselined++;
|
|
110
|
+
if (ann.type === "Stale a11y baseline entry") totalStale++;
|
|
113
111
|
}
|
|
114
|
-
totalViolations += test.violations.length
|
|
112
|
+
totalViolations += test.violations.length;
|
|
115
113
|
}
|
|
116
114
|
|
|
117
|
-
return { tests, totalViolations, totalBaselined, totalStale }
|
|
115
|
+
return { tests, totalViolations, totalBaselined, totalStale };
|
|
118
116
|
}
|
|
119
117
|
|
|
120
118
|
/**
|
|
@@ -124,52 +122,55 @@ export function parseA11yResults(reportPath: string): A11yReport {
|
|
|
124
122
|
* on `result.annotations`. checkAccessibility() uses the latter.
|
|
125
123
|
*/
|
|
126
124
|
function collectAnnotations(test: any, lastResult: any): any[] {
|
|
127
|
-
return [
|
|
128
|
-
...(test.annotations ?? []),
|
|
129
|
-
...(lastResult?.annotations ?? []),
|
|
130
|
-
]
|
|
125
|
+
return [...(test.annotations ?? []), ...(lastResult?.annotations ?? [])];
|
|
131
126
|
}
|
|
132
127
|
|
|
133
128
|
/** Read and parse the WCAG scan attachment, if present. */
|
|
134
129
|
function parseWcagViolations(attachment: any): NormalizedViolation[] {
|
|
135
|
-
if (!attachment) return []
|
|
130
|
+
if (!attachment) return [];
|
|
136
131
|
try {
|
|
137
132
|
const raw = attachment.body
|
|
138
|
-
? Buffer.from(attachment.body,
|
|
133
|
+
? Buffer.from(attachment.body, "base64").toString("utf8")
|
|
139
134
|
: attachment.path
|
|
140
|
-
? fs.readFileSync(attachment.path,
|
|
141
|
-
: null
|
|
142
|
-
if (!raw) return []
|
|
143
|
-
const json = JSON.parse(raw)
|
|
144
|
-
if (!json?.violations) return []
|
|
135
|
+
? fs.readFileSync(attachment.path, "utf8")
|
|
136
|
+
: null;
|
|
137
|
+
if (!raw) return [];
|
|
138
|
+
const json = JSON.parse(raw);
|
|
139
|
+
if (!json?.violations) return [];
|
|
145
140
|
return json.violations.map((v: any) => ({
|
|
146
141
|
rule: v.id,
|
|
147
|
-
impact: v.impact ??
|
|
142
|
+
impact: v.impact ?? "unknown",
|
|
148
143
|
description: v.description,
|
|
149
144
|
helpUrl: v.helpUrl,
|
|
150
|
-
targets:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
targets:
|
|
146
|
+
v.nodes
|
|
147
|
+
?.flatMap((n: any) => n.target)
|
|
148
|
+
.filter((t: any) => typeof t === "string") ?? [],
|
|
149
|
+
}));
|
|
154
150
|
} catch {
|
|
155
151
|
// Parse errors are non-fatal — we'll just report without violation details.
|
|
156
|
-
return []
|
|
152
|
+
return [];
|
|
157
153
|
}
|
|
158
154
|
}
|
|
159
155
|
|
|
160
156
|
/** Extract an A11yTestResult from a Playwright test, or null if not a11y-related. */
|
|
161
|
-
function extractTestResult(
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
function extractTestResult(
|
|
158
|
+
spec: any,
|
|
159
|
+
test: any,
|
|
160
|
+
file: string,
|
|
161
|
+
): A11yTestResult | null {
|
|
162
|
+
const lastResult = test.results?.[test.results.length - 1];
|
|
163
|
+
const attachments = lastResult?.attachments ?? [];
|
|
164
164
|
|
|
165
|
-
const a11yAnnotations = collectAnnotations(test, lastResult)
|
|
166
|
-
|
|
165
|
+
const a11yAnnotations = collectAnnotations(test, lastResult).filter(
|
|
166
|
+
(a: any) => A11Y_ANNOTATION_TYPES.has(a.type),
|
|
167
|
+
);
|
|
167
168
|
|
|
168
|
-
if (a11yAnnotations.length === 0) return null
|
|
169
|
+
if (a11yAnnotations.length === 0) return null;
|
|
169
170
|
|
|
170
171
|
const violations = parseWcagViolations(
|
|
171
|
-
attachments.find((a: any) => a.name ===
|
|
172
|
-
)
|
|
172
|
+
attachments.find((a: any) => a.name === "a11y-wcag-scan-results"),
|
|
173
|
+
);
|
|
173
174
|
|
|
174
175
|
return {
|
|
175
176
|
title: spec.title,
|
|
@@ -177,66 +178,66 @@ function extractTestResult(spec: any, test: any, file: string): A11yTestResult |
|
|
|
177
178
|
line: spec.line ?? 1,
|
|
178
179
|
annotations: a11yAnnotations,
|
|
179
180
|
violations,
|
|
180
|
-
}
|
|
181
|
+
};
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
function walkSuites(
|
|
184
|
-
suites: any[],
|
|
185
|
-
parentFile: string,
|
|
186
|
-
out: A11yTestResult[],
|
|
187
|
-
) {
|
|
184
|
+
function walkSuites(suites: any[], parentFile: string, out: A11yTestResult[]) {
|
|
188
185
|
for (const suite of suites) {
|
|
189
|
-
const file = suite.file || parentFile
|
|
186
|
+
const file = suite.file || parentFile;
|
|
190
187
|
|
|
191
188
|
for (const spec of suite.specs ?? []) {
|
|
192
189
|
for (const test of spec.tests ?? []) {
|
|
193
|
-
const result = extractTestResult(spec, test, file)
|
|
194
|
-
if (result) out.push(result)
|
|
190
|
+
const result = extractTestResult(spec, test, file);
|
|
191
|
+
if (result) out.push(result);
|
|
195
192
|
}
|
|
196
193
|
}
|
|
197
194
|
|
|
198
195
|
if (suite.suites) {
|
|
199
|
-
walkSuites(suite.suites, file, out)
|
|
196
|
+
walkSuites(suite.suites, file, out);
|
|
200
197
|
}
|
|
201
198
|
}
|
|
202
199
|
}
|
|
203
200
|
|
|
204
201
|
/** Headline with pass/fail counts. */
|
|
205
202
|
function renderHeadline(report: A11yReport): string {
|
|
206
|
-
const hasViolations = report.totalViolations > 0
|
|
207
|
-
const hasBaselined = report.totalBaselined > 0
|
|
208
|
-
const hasStale = report.totalStale > 0
|
|
203
|
+
const hasViolations = report.totalViolations > 0;
|
|
204
|
+
const hasBaselined = report.totalBaselined > 0;
|
|
205
|
+
const hasStale = report.totalStale > 0;
|
|
209
206
|
|
|
210
207
|
if (!hasViolations && !hasBaselined && !hasStale) {
|
|
211
|
-
return
|
|
208
|
+
return ":white_check_mark: All accessibility checks passed.\n";
|
|
212
209
|
}
|
|
213
210
|
|
|
214
|
-
const parts: string[] = []
|
|
215
|
-
if (hasViolations) parts.push(`**${report.totalViolations}** violation(s)`)
|
|
216
|
-
if (hasBaselined) parts.push(`**${report.totalBaselined}** baselined`)
|
|
217
|
-
if (hasStale) parts.push(`**${report.totalStale}** stale baseline entries`)
|
|
218
|
-
return parts.join(
|
|
211
|
+
const parts: string[] = [];
|
|
212
|
+
if (hasViolations) parts.push(`**${report.totalViolations}** violation(s)`);
|
|
213
|
+
if (hasBaselined) parts.push(`**${report.totalBaselined}** baselined`);
|
|
214
|
+
if (hasStale) parts.push(`**${report.totalStale}** stale baseline entries`);
|
|
215
|
+
return parts.join(" · ") + "\n";
|
|
219
216
|
}
|
|
220
217
|
|
|
221
218
|
/** Icon for an `Accessibility` annotation based on its violation count. */
|
|
222
219
|
function iconForAccessibilityAnnotation(description: string): string {
|
|
223
220
|
// Descriptions look like "WCAG scan: 0 violations ..." or "WCAG scan: 3 violations ...".
|
|
224
|
-
const colonIdx = description.lastIndexOf(
|
|
225
|
-
const afterColon =
|
|
226
|
-
|
|
227
|
-
|
|
221
|
+
const colonIdx = description.lastIndexOf(": ");
|
|
222
|
+
const afterColon =
|
|
223
|
+
colonIdx >= 0 ? description.substring(colonIdx + 2) : description;
|
|
224
|
+
const violationCount = parseInt(afterColon, 10);
|
|
225
|
+
return violationCount > 0 ? ":x:" : ":white_check_mark:";
|
|
228
226
|
}
|
|
229
227
|
|
|
230
|
-
function iconForAnnotation(ann: {
|
|
228
|
+
function iconForAnnotation(ann: {
|
|
229
|
+
type: string;
|
|
230
|
+
description?: string;
|
|
231
|
+
}): string {
|
|
231
232
|
switch (ann.type) {
|
|
232
|
-
case
|
|
233
|
-
return
|
|
234
|
-
case
|
|
235
|
-
return
|
|
236
|
-
case
|
|
237
|
-
return iconForAccessibilityAnnotation(ann.description ??
|
|
233
|
+
case "Baselined a11y violation":
|
|
234
|
+
return ":white_check_mark:";
|
|
235
|
+
case "Stale a11y baseline entry":
|
|
236
|
+
return ":warning:";
|
|
237
|
+
case "Accessibility":
|
|
238
|
+
return iconForAccessibilityAnnotation(ann.description ?? "");
|
|
238
239
|
default:
|
|
239
|
-
return
|
|
240
|
+
return ":information_source:";
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
|
|
@@ -244,36 +245,39 @@ function iconForAnnotation(ann: { type: string; description?: string }): string
|
|
|
244
245
|
function renderAnnotations(
|
|
245
246
|
annotations: Array<{ type: string; description?: string }>,
|
|
246
247
|
): string[] {
|
|
247
|
-
const lines: string[] = []
|
|
248
|
-
const seen = new Set<string>()
|
|
248
|
+
const lines: string[] = [];
|
|
249
|
+
const seen = new Set<string>();
|
|
249
250
|
for (const ann of annotations) {
|
|
250
|
-
const key = `${ann.type}:${ann.description ??
|
|
251
|
-
if (seen.has(key)) continue
|
|
252
|
-
seen.add(key)
|
|
253
|
-
lines.push(
|
|
251
|
+
const key = `${ann.type}:${ann.description ?? ""}`;
|
|
252
|
+
if (seen.has(key)) continue;
|
|
253
|
+
seen.add(key);
|
|
254
|
+
lines.push(
|
|
255
|
+
`${iconForAnnotation(ann)} **${ann.type}**: ${ann.description ?? ""}\n`,
|
|
256
|
+
);
|
|
254
257
|
}
|
|
255
|
-
return lines
|
|
258
|
+
return lines;
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
/** Render the violation table for a single test. */
|
|
259
262
|
function renderViolationTable(violations: NormalizedViolation[]): string[] {
|
|
260
|
-
if (violations.length === 0) return []
|
|
263
|
+
if (violations.length === 0) return [];
|
|
261
264
|
const lines: string[] = [
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
]
|
|
265
|
+
"| Rule | Impact | Description | Targets |",
|
|
266
|
+
"|------|--------|-------------|---------|",
|
|
267
|
+
];
|
|
265
268
|
for (const v of violations) {
|
|
266
|
-
const shown = v.targets.slice(0, 3).map(escapeMarkdownCodeSpan).join(
|
|
267
|
-
const targets =
|
|
268
|
-
? `${shown}, +${v.targets.length - 3} more`
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
269
|
+
const shown = v.targets.slice(0, 3).map(escapeMarkdownCodeSpan).join(", ");
|
|
270
|
+
const targets =
|
|
271
|
+
v.targets.length > 3 ? `${shown}, +${v.targets.length - 3} more` : shown;
|
|
272
|
+
const rule = escapeMarkdownTableCell(v.rule);
|
|
273
|
+
const impact = escapeMarkdownTableCell(v.impact);
|
|
274
|
+
const description = escapeMarkdownTableCell(v.description);
|
|
275
|
+
lines.push(
|
|
276
|
+
`| [${rule}](${v.helpUrl}) | ${impact} | ${description} | ${targets} |`,
|
|
277
|
+
);
|
|
274
278
|
}
|
|
275
|
-
lines.push(
|
|
276
|
-
return lines
|
|
279
|
+
lines.push("");
|
|
280
|
+
return lines;
|
|
277
281
|
}
|
|
278
282
|
|
|
279
283
|
/**
|
|
@@ -281,29 +285,32 @@ function renderViolationTable(violations: NormalizedViolation[]): string[] {
|
|
|
281
285
|
*/
|
|
282
286
|
export function generateSummary(report: A11yReport): string {
|
|
283
287
|
if (report.tests.length === 0) {
|
|
284
|
-
return
|
|
288
|
+
return "## Accessibility Results\n\nNo accessibility tests were found in the report.\n";
|
|
285
289
|
}
|
|
286
290
|
|
|
287
|
-
const lines: string[] = [
|
|
288
|
-
|
|
291
|
+
const lines: string[] = [
|
|
292
|
+
"## Accessibility Results\n",
|
|
293
|
+
renderHeadline(report),
|
|
294
|
+
];
|
|
295
|
+
const seenTitles = new Set<string>();
|
|
289
296
|
|
|
290
297
|
for (const test of report.tests) {
|
|
291
|
-
const testAnnotations = test.annotations.filter(a => a.type !==
|
|
292
|
-
const hasTestViolations = test.violations.length > 0
|
|
293
|
-
if (testAnnotations.length === 0 && !hasTestViolations) continue
|
|
298
|
+
const testAnnotations = test.annotations.filter((a) => a.type !== "@a11y");
|
|
299
|
+
const hasTestViolations = test.violations.length > 0;
|
|
300
|
+
if (testAnnotations.length === 0 && !hasTestViolations) continue;
|
|
294
301
|
|
|
295
302
|
// Skip duplicate test titles (different browsers report the same spec).
|
|
296
|
-
if (seenTitles.has(test.title)) continue
|
|
297
|
-
seenTitles.add(test.title)
|
|
303
|
+
if (seenTitles.has(test.title)) continue;
|
|
304
|
+
seenTitles.add(test.title);
|
|
298
305
|
|
|
299
|
-
const testLines: string[] = [`### ${test.title}\n`]
|
|
300
|
-
testLines.push(...renderAnnotations(testAnnotations))
|
|
301
|
-
testLines.push(...renderViolationTable(test.violations))
|
|
306
|
+
const testLines: string[] = [`### ${test.title}\n`];
|
|
307
|
+
testLines.push(...renderAnnotations(testAnnotations));
|
|
308
|
+
testLines.push(...renderViolationTable(test.violations));
|
|
302
309
|
|
|
303
|
-
lines.push(...testLines)
|
|
310
|
+
lines.push(...testLines);
|
|
304
311
|
}
|
|
305
312
|
|
|
306
|
-
return lines.join(
|
|
313
|
+
return lines.join("\n");
|
|
307
314
|
}
|
|
308
315
|
|
|
309
316
|
/**
|
|
@@ -314,33 +321,35 @@ export function generateSummary(report: A11yReport): string {
|
|
|
314
321
|
* that would otherwise let a crafted page forge additional workflow commands.
|
|
315
322
|
*/
|
|
316
323
|
export function generateAnnotations(report: A11yReport): string {
|
|
317
|
-
const lines: string[] = []
|
|
324
|
+
const lines: string[] = [];
|
|
318
325
|
|
|
319
326
|
for (const test of report.tests) {
|
|
320
|
-
const file = escapeWorkflowCommandProperty(test.file)
|
|
321
|
-
const line = escapeWorkflowCommandProperty(String(test.line))
|
|
327
|
+
const file = escapeWorkflowCommandProperty(test.file);
|
|
328
|
+
const line = escapeWorkflowCommandProperty(String(test.line));
|
|
322
329
|
|
|
323
330
|
// Emit ::error for each violation.
|
|
324
331
|
for (const v of test.violations) {
|
|
325
|
-
const targets = v.targets.slice(0, 3).join(
|
|
326
|
-
const title = escapeWorkflowCommandProperty(`${v.rule} (${v.impact})`)
|
|
332
|
+
const targets = v.targets.slice(0, 3).join(", ");
|
|
333
|
+
const title = escapeWorkflowCommandProperty(`${v.rule} (${v.impact})`);
|
|
327
334
|
const msg = escapeWorkflowCommand(
|
|
328
335
|
`a11y: ${v.rule} (${v.impact}) — ${v.description}. Targets: ${targets}`,
|
|
329
|
-
)
|
|
330
|
-
lines.push(`::error file=${file},line=${line},title=${title}::${msg}`)
|
|
336
|
+
);
|
|
337
|
+
lines.push(`::error file=${file},line=${line},title=${title}::${msg}`);
|
|
331
338
|
}
|
|
332
339
|
|
|
333
340
|
// Emit ::warning for stale baseline entries.
|
|
334
341
|
for (const ann of test.annotations) {
|
|
335
|
-
if (ann.type ===
|
|
336
|
-
const title = escapeWorkflowCommandProperty(
|
|
337
|
-
const msg = escapeWorkflowCommand(ann.description ??
|
|
338
|
-
lines.push(
|
|
342
|
+
if (ann.type === "Stale a11y baseline entry") {
|
|
343
|
+
const title = escapeWorkflowCommandProperty("Stale a11y baseline");
|
|
344
|
+
const msg = escapeWorkflowCommand(ann.description ?? "");
|
|
345
|
+
lines.push(
|
|
346
|
+
`::warning file=${file},line=${line},title=${title}::${msg}`,
|
|
347
|
+
);
|
|
339
348
|
}
|
|
340
349
|
}
|
|
341
350
|
}
|
|
342
351
|
|
|
343
|
-
return lines.join(
|
|
352
|
+
return lines.join("\n");
|
|
344
353
|
}
|
|
345
354
|
|
|
346
355
|
/**
|
|
@@ -352,34 +361,35 @@ export function main(
|
|
|
352
361
|
args: string[] = process.argv.slice(2),
|
|
353
362
|
adapter: { commandName?: string } = {},
|
|
354
363
|
): void {
|
|
355
|
-
if (args.includes(
|
|
364
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
356
365
|
process.stdout.write(
|
|
357
|
-
`Usage: ${adapter.commandName ??
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)
|
|
364
|
-
return
|
|
366
|
+
`Usage: ${adapter.commandName ?? "playwright-testing-a11y-summary"} [options]\n\n` +
|
|
367
|
+
"Options:\n" +
|
|
368
|
+
" --report-path=PATH Playwright JSON report (default: test-results/results.json)\n" +
|
|
369
|
+
" --mode=summary Write a Markdown accessibility summary (default)\n" +
|
|
370
|
+
" --mode=annotations Write GitHub Actions annotations\n" +
|
|
371
|
+
" -h, --help Show this help\n",
|
|
372
|
+
);
|
|
373
|
+
return;
|
|
365
374
|
}
|
|
366
375
|
|
|
367
|
-
let mode =
|
|
368
|
-
let reportPath =
|
|
376
|
+
let mode = "summary";
|
|
377
|
+
let reportPath = "test-results/results.json";
|
|
369
378
|
|
|
370
379
|
for (const arg of args) {
|
|
371
|
-
if (arg.startsWith(
|
|
372
|
-
if (arg.startsWith(
|
|
380
|
+
if (arg.startsWith("--mode=")) mode = arg.slice("--mode=".length);
|
|
381
|
+
if (arg.startsWith("--report-path="))
|
|
382
|
+
reportPath = arg.slice("--report-path=".length);
|
|
373
383
|
}
|
|
374
384
|
|
|
375
|
-
const validModes = new Set([
|
|
385
|
+
const validModes = new Set(["summary", "annotations"]);
|
|
376
386
|
if (!validModes.has(mode)) {
|
|
377
|
-
console.error(`Unknown mode: ${mode}. Use summary or annotations.`)
|
|
378
|
-
process.exit(2)
|
|
387
|
+
console.error(`Unknown mode: ${mode}. Use summary or annotations.`);
|
|
388
|
+
process.exit(2);
|
|
379
389
|
}
|
|
380
390
|
|
|
381
391
|
// Resolve relative to cwd.
|
|
382
|
-
const resolvedPath = path.resolve(reportPath)
|
|
392
|
+
const resolvedPath = path.resolve(reportPath);
|
|
383
393
|
|
|
384
394
|
// Missing reports are intentionally non-fatal — the JSON report may be
|
|
385
395
|
// absent if no tests ran. Any other failure (parse error, permissions, etc.)
|
|
@@ -388,34 +398,34 @@ export function main(
|
|
|
388
398
|
if (!fs.existsSync(resolvedPath)) {
|
|
389
399
|
console.error(
|
|
390
400
|
`Playwright JSON report not found at ${resolvedPath} — skipping a11y summary.`,
|
|
391
|
-
)
|
|
392
|
-
return
|
|
401
|
+
);
|
|
402
|
+
return;
|
|
393
403
|
}
|
|
394
404
|
|
|
395
|
-
const report = parseA11yResults(resolvedPath)
|
|
405
|
+
const report = parseA11yResults(resolvedPath);
|
|
396
406
|
|
|
397
407
|
switch (mode) {
|
|
398
|
-
case
|
|
399
|
-
const md = generateSummary(report)
|
|
400
|
-
const summaryFile = process.env.GITHUB_STEP_SUMMARY
|
|
408
|
+
case "summary": {
|
|
409
|
+
const md = generateSummary(report);
|
|
410
|
+
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
401
411
|
if (summaryFile) {
|
|
402
|
-
fs.appendFileSync(summaryFile, md)
|
|
403
|
-
console.log(
|
|
412
|
+
fs.appendFileSync(summaryFile, md);
|
|
413
|
+
console.log("Accessibility summary written to $GITHUB_STEP_SUMMARY");
|
|
404
414
|
} else {
|
|
405
415
|
// Not in GitHub Actions — just print.
|
|
406
|
-
process.stdout.write(md)
|
|
416
|
+
process.stdout.write(md);
|
|
407
417
|
}
|
|
408
|
-
break
|
|
418
|
+
break;
|
|
409
419
|
}
|
|
410
|
-
case
|
|
411
|
-
const output = generateAnnotations(report)
|
|
412
|
-
if (output) process.stdout.write(output +
|
|
413
|
-
break
|
|
420
|
+
case "annotations": {
|
|
421
|
+
const output = generateAnnotations(report);
|
|
422
|
+
if (output) process.stdout.write(output + "\n");
|
|
423
|
+
break;
|
|
414
424
|
}
|
|
415
425
|
}
|
|
416
426
|
}
|
|
417
427
|
|
|
418
428
|
// Auto-invoke when run directly (node lib/github/a11y-summary.js).
|
|
419
429
|
if (require.main === module) {
|
|
420
|
-
main()
|
|
430
|
+
main();
|
|
421
431
|
}
|