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