@schalkneethling/calavera-skill-frontend-testing 0.2.0-next.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.
@@ -0,0 +1,362 @@
1
+ # Accessibility Testing
2
+
3
+ Automated accessibility testing catches common issues early. Combine with manual testing and assistive technology validation for comprehensive coverage.
4
+
5
+ ## Limitations of Automated Testing
6
+
7
+ Automated tools detect approximately 30-40% of accessibility issues. They catch:
8
+
9
+ - Missing labels and alt text
10
+ - Color contrast violations
11
+ - Invalid ARIA attributes
12
+ - Duplicate IDs
13
+ - Missing landmark regions
14
+
15
+ They cannot catch:
16
+
17
+ - Logical reading order
18
+ - Meaningful link text in context
19
+ - Appropriate focus management
20
+ - Keyboard trap issues in complex flows
21
+ - Whether content is actually understandable
22
+
23
+ **Always combine automated testing with manual review and user testing.**
24
+
25
+ ## Playwright + axe-core Integration
26
+
27
+ ### Setup
28
+
29
+ ```bash
30
+ npm install @axe-core/playwright --save-dev
31
+ ```
32
+
33
+ ### Basic Page Scan
34
+
35
+ ```javascript
36
+ import { test, expect } from "@playwright/test";
37
+ import AxeBuilder from "@axe-core/playwright";
38
+
39
+ test.describe("Homepage", () => {
40
+ test("has no automatically detectable accessibility violations", async ({ page }) => {
41
+ await page.goto("/");
42
+
43
+ const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
44
+
45
+ expect(accessibilityScanResults.violations).toEqual([]);
46
+ });
47
+ });
48
+ ```
49
+
50
+ ### Wait for Page State
51
+
52
+ Always ensure the page is in the expected state before scanning:
53
+
54
+ ```javascript
55
+ test("navigation menu is accessible when open", async ({ page }) => {
56
+ await page.goto("/");
57
+
58
+ // Open the menu
59
+ await page.getByRole("button", { name: /menu/i }).click();
60
+
61
+ // Wait for menu to be visible before scanning
62
+ await page.getByRole("navigation", { name: /main/i }).waitFor();
63
+
64
+ const results = await new AxeBuilder({ page }).analyze();
65
+
66
+ expect(results.violations).toEqual([]);
67
+ });
68
+ ```
69
+
70
+ ### Scan Specific Regions
71
+
72
+ Focus on components you're testing:
73
+
74
+ ```javascript
75
+ test("checkout form is accessible", async ({ page }) => {
76
+ await page.goto("/checkout");
77
+
78
+ const results = await new AxeBuilder({ page })
79
+ .include("#checkout-form") // Only scan this region
80
+ .analyze();
81
+
82
+ expect(results.violations).toEqual([]);
83
+ });
84
+ ```
85
+
86
+ ### Exclude Known Issues
87
+
88
+ Temporarily exclude elements while fixing issues:
89
+
90
+ ```javascript
91
+ const results = await new AxeBuilder({ page })
92
+ .exclude("#third-party-widget") // Can't control this
93
+ .exclude("[data-ad-unit]") // Ads managed externally
94
+ .analyze();
95
+ ```
96
+
97
+ **Important**: Document why exclusions exist. Remove them when fixed.
98
+
99
+ ## WCAG Targeting
100
+
101
+ ### Target Specific WCAG Levels
102
+
103
+ ```javascript
104
+ // WCAG 2.1 Level AA (most common compliance target)
105
+ const results = await new AxeBuilder({ page })
106
+ .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
107
+ .analyze();
108
+
109
+ // WCAG 2.2 (latest standard)
110
+ const results = await new AxeBuilder({ page })
111
+ .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"])
112
+ .analyze();
113
+
114
+ // Best practices (not WCAG requirements but recommended)
115
+ const results = await new AxeBuilder({ page }).withTags(["best-practice"]).analyze();
116
+ ```
117
+
118
+ ### Common Tag Sets
119
+
120
+ | Target | Tags |
121
+ | -------------- | ---------------------------------------------- |
122
+ | WCAG 2.1 AA | `["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]` |
123
+ | WCAG 2.2 AA | Add `"wcag22aa"` |
124
+ | Section 508 | `["section508"]` |
125
+ | Best practices | `["best-practice"]` |
126
+
127
+ ## Creating Reusable Fixtures
128
+
129
+ ### Custom Test Fixture
130
+
131
+ ```javascript
132
+ // fixtures/axe.js
133
+ import { test as base } from "@playwright/test";
134
+ import AxeBuilder from "@axe-core/playwright";
135
+
136
+ export const test = base.extend({
137
+ makeAxeBuilder: async ({ page }, use) => {
138
+ const makeAxeBuilder = () =>
139
+ new AxeBuilder({ page })
140
+ .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
141
+ .exclude("#cookie-banner") // Known third-party issue
142
+ .exclude("[data-testid='ad-unit']");
143
+
144
+ await use(makeAxeBuilder);
145
+ },
146
+ });
147
+
148
+ export { expect } from "@playwright/test";
149
+ ```
150
+
151
+ ### Using the Fixture
152
+
153
+ ```javascript
154
+ import { test, expect } from "./fixtures/axe";
155
+
156
+ test("product page is accessible", async ({ page, makeAxeBuilder }) => {
157
+ await page.goto("/products/123");
158
+
159
+ const results = await makeAxeBuilder().analyze();
160
+
161
+ expect(results.violations).toEqual([]);
162
+ });
163
+ ```
164
+
165
+ ## Handling Violations
166
+
167
+ ### Understanding Results
168
+
169
+ ```javascript
170
+ const results = await new AxeBuilder({ page }).analyze();
171
+
172
+ // Structure of a violation
173
+ results.violations.forEach((violation) => {
174
+ console.log(`Rule: ${violation.id}`);
175
+ console.log(`Impact: ${violation.impact}`); // minor, moderate, serious, critical
176
+ console.log(`Description: ${violation.description}`);
177
+ console.log(`Help: ${violation.helpUrl}`);
178
+
179
+ violation.nodes.forEach((node) => {
180
+ console.log(` Element: ${node.html}`);
181
+ console.log(` Fix: ${node.failureSummary}`);
182
+ });
183
+ });
184
+ ```
185
+
186
+ ### Impact Levels
187
+
188
+ | Level | Description | Priority |
189
+ | -------- | ---------------------------- | --------------------- |
190
+ | Critical | Blocks access for some users | Fix immediately |
191
+ | Serious | Major barriers | Fix soon |
192
+ | Moderate | Inconsistencies | Plan to fix |
193
+ | Minor | Annoyances | Improve when possible |
194
+
195
+ ### Progressive Enforcement
196
+
197
+ Start permissive, tighten over time:
198
+
199
+ ```javascript
200
+ // Phase 1: Only critical issues fail
201
+ const criticalViolations = results.violations.filter((v) => v.impact === "critical");
202
+ expect(criticalViolations).toEqual([]);
203
+
204
+ // Phase 2: Critical and serious
205
+ const seriousViolations = results.violations.filter((v) =>
206
+ ["critical", "serious"].includes(v.impact),
207
+ );
208
+ expect(seriousViolations).toEqual([]);
209
+
210
+ // Phase 3: All violations (goal state)
211
+ expect(results.violations).toEqual([]);
212
+ ```
213
+
214
+ ### Disable Specific Rules
215
+
216
+ When you have a known issue being addressed:
217
+
218
+ ```javascript
219
+ const results = await new AxeBuilder({ page })
220
+ .disableRules(["color-contrast"]) // Tracked in JIRA-123
221
+ .analyze();
222
+ ```
223
+
224
+ **Document why rules are disabled. Remove when fixed.**
225
+
226
+ ## Integration Patterns
227
+
228
+ ### Per-Component Tests
229
+
230
+ Test components in isolation:
231
+
232
+ ```javascript
233
+ test.describe("Button component", () => {
234
+ test("default button is accessible", async ({ page }) => {
235
+ await page.goto("/storybook/button--default");
236
+ const results = await new AxeBuilder({ page }).include("#storybook-root").analyze();
237
+ expect(results.violations).toEqual([]);
238
+ });
239
+
240
+ test("disabled button is accessible", async ({ page }) => {
241
+ await page.goto("/storybook/button--disabled");
242
+ const results = await new AxeBuilder({ page }).include("#storybook-root").analyze();
243
+ expect(results.violations).toEqual([]);
244
+ });
245
+ });
246
+ ```
247
+
248
+ ### Critical User Flows
249
+
250
+ Scan at each step of important journeys:
251
+
252
+ ```javascript
253
+ test("checkout flow is accessible at each step", async ({ page }) => {
254
+ // Cart page
255
+ await page.goto("/cart");
256
+ let results = await new AxeBuilder({ page }).analyze();
257
+ expect(results.violations).toEqual([]);
258
+
259
+ // Shipping form
260
+ await page.getByRole("link", { name: /checkout/i }).click();
261
+ await page.getByRole("heading", { name: /shipping/i }).waitFor();
262
+ results = await new AxeBuilder({ page }).analyze();
263
+ expect(results.violations).toEqual([]);
264
+
265
+ // Payment form
266
+ await page.getByRole("button", { name: /continue/i }).click();
267
+ await page.getByRole("heading", { name: /payment/i }).waitFor();
268
+ results = await new AxeBuilder({ page }).analyze();
269
+ expect(results.violations).toEqual([]);
270
+ });
271
+ ```
272
+
273
+ ### After Dynamic Content
274
+
275
+ Always re-scan after content changes:
276
+
277
+ ```javascript
278
+ test("modal is accessible when opened", async ({ page }) => {
279
+ await page.goto("/");
280
+
281
+ // Initial page scan
282
+ let results = await new AxeBuilder({ page }).analyze();
283
+ expect(results.violations).toEqual([]);
284
+
285
+ // Open modal
286
+ await page.getByRole("button", { name: /settings/i }).click();
287
+ await page.getByRole("dialog").waitFor();
288
+
289
+ // Scan modal
290
+ results = await new AxeBuilder({ page }).include("[role='dialog']").analyze();
291
+ expect(results.violations).toEqual([]);
292
+ });
293
+ ```
294
+
295
+ ## Common Issues and Fixes
296
+
297
+ ### Missing Form Labels
298
+
299
+ ```html
300
+ <!-- BAD -->
301
+ <input type="email" placeholder="Email" />
302
+
303
+ <!-- GOOD -->
304
+ <label for="email">Email address</label>
305
+ <input id="email" type="email" />
306
+
307
+ <!-- ALSO GOOD (visually hidden label) -->
308
+ <label for="search" class="visually-hidden">Search products</label>
309
+ <input id="search" type="search" placeholder="Search..." />
310
+ ```
311
+
312
+ ### Color Contrast
313
+
314
+ ```css
315
+ /* BAD: 2.61:1 ratio */
316
+ .muted-text {
317
+ color: #a0a0a0;
318
+ background: #ffffff;
319
+ }
320
+
321
+ /* GOOD: 4.5:1+ ratio for normal text */
322
+ .muted-text {
323
+ color: #6b6b6b;
324
+ background: #ffffff;
325
+ }
326
+ ```
327
+
328
+ ### Missing Button Names
329
+
330
+ ```html
331
+ <!-- BAD -->
332
+ <button><svg>...</svg></button>
333
+
334
+ <!-- GOOD -->
335
+ <button aria-label="Close dialog"><svg>...</svg></button>
336
+
337
+ <!-- ALSO GOOD -->
338
+ <button>
339
+ <svg aria-hidden="true">...</svg>
340
+ <span class="visually-hidden">Close dialog</span>
341
+ </button>
342
+ ```
343
+
344
+ ### Duplicate IDs
345
+
346
+ ```html
347
+ <!-- BAD -->
348
+ <input id="email" />
349
+ <input id="email" />
350
+ <!-- Duplicate! -->
351
+
352
+ <!-- GOOD -->
353
+ <input id="billing-email" />
354
+ <input id="shipping-email" />
355
+ ```
356
+
357
+ ## References
358
+
359
+ - [axe-core Rules](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md)
360
+ - [Playwright Accessibility Testing](https://playwright.dev/docs/accessibility-testing)
361
+ - [WCAG Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
362
+ - [Deque University](https://dequeuniversity.com/) — Free accessibility training