@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,517 @@
|
|
|
1
|
+
# ARIA Snapshot Testing
|
|
2
|
+
|
|
3
|
+
ARIA snapshots capture the accessibility tree structure in YAML format. A single snapshot assertion can replace multiple individual assertions while validating semantic structure.
|
|
4
|
+
|
|
5
|
+
## Why ARIA Snapshots
|
|
6
|
+
|
|
7
|
+
### Consolidation
|
|
8
|
+
|
|
9
|
+
Instead of multiple assertions:
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
// Multiple individual assertions
|
|
13
|
+
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Welcome");
|
|
14
|
+
await expect(page.getByRole("textbox", { name: /email/i })).toBeVisible();
|
|
15
|
+
await expect(page.getByRole("textbox", { name: /password/i })).toBeVisible();
|
|
16
|
+
await expect(page.getByRole("button", { name: /sign in/i })).toBeEnabled();
|
|
17
|
+
await expect(page.getByRole("link", { name: /forgot/i })).toBeVisible();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
One snapshot covers all:
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
|
|
24
|
+
- heading "Welcome" [level=1]
|
|
25
|
+
- textbox "Email"
|
|
26
|
+
- textbox "Password"
|
|
27
|
+
- button "Sign in"
|
|
28
|
+
- link "Forgot password?"
|
|
29
|
+
`);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Benefits Over Visual Snapshots
|
|
33
|
+
|
|
34
|
+
| Aspect | ARIA Snapshot | Visual Screenshot |
|
|
35
|
+
|--------|---------------|-------------------|
|
|
36
|
+
| Styling changes | Unaffected | Fails |
|
|
37
|
+
| Font rendering | Unaffected | Varies by OS |
|
|
38
|
+
| Cross-browser | Consistent | Different per browser |
|
|
39
|
+
| What it validates | Semantic structure | Pixel appearance |
|
|
40
|
+
| Accessibility | Validates a11y tree | No a11y validation |
|
|
41
|
+
| File size | Tiny (YAML text) | Large (PNG images) |
|
|
42
|
+
|
|
43
|
+
### What It Catches
|
|
44
|
+
|
|
45
|
+
- Missing or changed accessible names
|
|
46
|
+
- Wrong heading levels
|
|
47
|
+
- Missing form labels
|
|
48
|
+
- Changed element roles
|
|
49
|
+
- Broken landmark structure
|
|
50
|
+
- State changes (checked, disabled, expanded)
|
|
51
|
+
|
|
52
|
+
## YAML Syntax
|
|
53
|
+
|
|
54
|
+
Each element is represented as:
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
- role "accessible name" [attribute=value]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Roles
|
|
61
|
+
|
|
62
|
+
Common roles from the accessibility tree:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- heading "Page Title" [level=1]
|
|
66
|
+
- navigation
|
|
67
|
+
- main
|
|
68
|
+
- button "Submit"
|
|
69
|
+
- link "Learn more"
|
|
70
|
+
- textbox "Email"
|
|
71
|
+
- checkbox "Remember me" [checked=true]
|
|
72
|
+
- combobox "Country"
|
|
73
|
+
- list
|
|
74
|
+
- listitem "Item one"
|
|
75
|
+
- table
|
|
76
|
+
- row
|
|
77
|
+
- cell "Data"
|
|
78
|
+
- dialog "Confirm deletion"
|
|
79
|
+
- alert
|
|
80
|
+
- img "Product photo"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Accessible Names
|
|
84
|
+
|
|
85
|
+
Quoted strings for exact match, regex for flexible matching:
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
# Exact match
|
|
89
|
+
- button "Submit Order"
|
|
90
|
+
|
|
91
|
+
# Regex match
|
|
92
|
+
- heading /Welcome.*/ [level=1]
|
|
93
|
+
- link /\d+ items in cart/
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Attributes
|
|
97
|
+
|
|
98
|
+
State and properties in square brackets:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
# Heading levels
|
|
102
|
+
- heading "Title" [level=1]
|
|
103
|
+
- heading "Subtitle" [level=2]
|
|
104
|
+
|
|
105
|
+
# Checkbox/radio state
|
|
106
|
+
- checkbox "Accept terms" [checked=true]
|
|
107
|
+
- checkbox "Newsletter" [checked=false]
|
|
108
|
+
|
|
109
|
+
# Disabled state
|
|
110
|
+
- button "Submit" [disabled=true]
|
|
111
|
+
|
|
112
|
+
# Expanded state (accordions, menus)
|
|
113
|
+
- button "Menu" [expanded=true]
|
|
114
|
+
- button "Details" [expanded=false]
|
|
115
|
+
|
|
116
|
+
# Selected state (tabs, options)
|
|
117
|
+
- tab "Overview" [selected=true]
|
|
118
|
+
- option "English" [selected=true]
|
|
119
|
+
|
|
120
|
+
# Pressed state (toggle buttons)
|
|
121
|
+
- button "Bold" [pressed=true]
|
|
122
|
+
|
|
123
|
+
# Link URLs (newer feature)
|
|
124
|
+
- link "Documentation":
|
|
125
|
+
- /url: "https://docs.example.com"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Nesting
|
|
129
|
+
|
|
130
|
+
Indentation shows hierarchy:
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
- navigation:
|
|
134
|
+
- list:
|
|
135
|
+
- listitem:
|
|
136
|
+
- link "Home"
|
|
137
|
+
- listitem:
|
|
138
|
+
- link "Products"
|
|
139
|
+
- listitem:
|
|
140
|
+
- link "About"
|
|
141
|
+
- main:
|
|
142
|
+
- heading "Products" [level=1]
|
|
143
|
+
- list:
|
|
144
|
+
- listitem "Product A"
|
|
145
|
+
- listitem "Product B"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Basic Usage
|
|
149
|
+
|
|
150
|
+
### Inline Snapshot
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
import { test, expect } from "@playwright/test";
|
|
154
|
+
|
|
155
|
+
test("login form structure", async ({ page }) => {
|
|
156
|
+
await page.goto("/login");
|
|
157
|
+
|
|
158
|
+
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
|
|
159
|
+
- heading "Sign In" [level=1]
|
|
160
|
+
- textbox "Email"
|
|
161
|
+
- textbox "Password"
|
|
162
|
+
- button "Sign In"
|
|
163
|
+
- link "Forgot password?"
|
|
164
|
+
- link "Create account"
|
|
165
|
+
`);
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### External Snapshot File
|
|
170
|
+
|
|
171
|
+
Store snapshots in separate YAML files:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
test("dashboard structure", async ({ page }) => {
|
|
175
|
+
await page.goto("/dashboard");
|
|
176
|
+
|
|
177
|
+
await expect(page.getByRole("main")).toMatchAriaSnapshot({
|
|
178
|
+
name: "dashboard-main.aria.yml",
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Scoped Snapshots
|
|
184
|
+
|
|
185
|
+
Target specific regions:
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
// Just the header
|
|
189
|
+
await expect(page.getByRole("banner")).toMatchAriaSnapshot(`
|
|
190
|
+
- link "Logo"
|
|
191
|
+
- navigation:
|
|
192
|
+
- link "Home"
|
|
193
|
+
- link "Products"
|
|
194
|
+
- link "About"
|
|
195
|
+
- button "Menu"
|
|
196
|
+
`);
|
|
197
|
+
|
|
198
|
+
// Just a form
|
|
199
|
+
await expect(page.getByRole("form", { name: /checkout/i })).toMatchAriaSnapshot(`
|
|
200
|
+
- textbox "Card number"
|
|
201
|
+
- textbox "Expiry"
|
|
202
|
+
- textbox "CVC"
|
|
203
|
+
- button "Pay now"
|
|
204
|
+
`);
|
|
205
|
+
|
|
206
|
+
// Just a specific component
|
|
207
|
+
await expect(page.getByTestId("product-card")).toMatchAriaSnapshot(`
|
|
208
|
+
- img "Product photo"
|
|
209
|
+
- heading "Product Name" [level=3]
|
|
210
|
+
- text "$29.99"
|
|
211
|
+
- button "Add to cart"
|
|
212
|
+
`);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Partial Matching
|
|
216
|
+
|
|
217
|
+
By default, snapshots perform subset matching. Omit elements you don't care about.
|
|
218
|
+
|
|
219
|
+
### Omit Names
|
|
220
|
+
|
|
221
|
+
Match role without requiring specific text:
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
// Matches any button, regardless of name
|
|
225
|
+
await expect(locator).toMatchAriaSnapshot(`
|
|
226
|
+
- button
|
|
227
|
+
`);
|
|
228
|
+
|
|
229
|
+
// Matches heading at level 1, any text
|
|
230
|
+
await expect(locator).toMatchAriaSnapshot(`
|
|
231
|
+
- heading [level=1]
|
|
232
|
+
`);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Omit Attributes
|
|
236
|
+
|
|
237
|
+
Match role and name without checking state:
|
|
238
|
+
|
|
239
|
+
```javascript
|
|
240
|
+
// Matches checkbox regardless of checked state
|
|
241
|
+
await expect(locator).toMatchAriaSnapshot(`
|
|
242
|
+
- checkbox "Remember me"
|
|
243
|
+
`);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Omit Children
|
|
247
|
+
|
|
248
|
+
Only verify some children exist:
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
// Only verify Home and About links exist
|
|
252
|
+
// Other links in the nav are ignored
|
|
253
|
+
await expect(page.getByRole("navigation")).toMatchAriaSnapshot(`
|
|
254
|
+
- link "Home"
|
|
255
|
+
- link "About"
|
|
256
|
+
`);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Strict Child Matching
|
|
260
|
+
|
|
261
|
+
When order and completeness matter:
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
// Require exactly these children, in this order
|
|
265
|
+
await expect(page.getByRole("list")).toMatchAriaSnapshot(`
|
|
266
|
+
- list:
|
|
267
|
+
- /children: equal
|
|
268
|
+
- listitem "Step 1"
|
|
269
|
+
- listitem "Step 2"
|
|
270
|
+
- listitem "Step 3"
|
|
271
|
+
`);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Generating Snapshots
|
|
275
|
+
|
|
276
|
+
### Using Codegen
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
npx playwright codegen example.com
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
In the recorder:
|
|
283
|
+
1. Click "Assert snapshot" action
|
|
284
|
+
2. Select the element
|
|
285
|
+
3. ARIA snapshot is generated
|
|
286
|
+
|
|
287
|
+
### Programmatically
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
test("generate snapshot for review", async ({ page }) => {
|
|
291
|
+
await page.goto("/products");
|
|
292
|
+
|
|
293
|
+
// Get the YAML representation
|
|
294
|
+
const snapshot = await page.getByRole("main").ariaSnapshot();
|
|
295
|
+
console.log(snapshot);
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Updating Snapshots
|
|
300
|
+
|
|
301
|
+
When structure intentionally changes:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
npx playwright test --update-snapshots
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Review changes before committing:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
git diff
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Testing Patterns
|
|
314
|
+
|
|
315
|
+
### Component States
|
|
316
|
+
|
|
317
|
+
Test different states of the same component:
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
test.describe("Accordion", () => {
|
|
321
|
+
test("collapsed state", async ({ page }) => {
|
|
322
|
+
await page.goto("/accordion");
|
|
323
|
+
|
|
324
|
+
await expect(page.getByRole("region", { name: /details/i })).toMatchAriaSnapshot(`
|
|
325
|
+
- button "Details" [expanded=false]
|
|
326
|
+
`);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("expanded state", async ({ page }) => {
|
|
330
|
+
await page.goto("/accordion");
|
|
331
|
+
await page.getByRole("button", { name: /details/i }).click();
|
|
332
|
+
|
|
333
|
+
await expect(page.getByRole("region", { name: /details/i })).toMatchAriaSnapshot(`
|
|
334
|
+
- button "Details" [expanded=true]
|
|
335
|
+
- text "Additional information here"
|
|
336
|
+
`);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Form Validation States
|
|
342
|
+
|
|
343
|
+
```javascript
|
|
344
|
+
test("shows validation errors", async ({ page }) => {
|
|
345
|
+
await page.goto("/signup");
|
|
346
|
+
await page.getByRole("button", { name: /submit/i }).click();
|
|
347
|
+
|
|
348
|
+
await expect(page.getByRole("form")).toMatchAriaSnapshot(`
|
|
349
|
+
- textbox "Email"
|
|
350
|
+
- text "Email is required"
|
|
351
|
+
- textbox "Password"
|
|
352
|
+
- text "Password is required"
|
|
353
|
+
- button "Submit"
|
|
354
|
+
`);
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Dynamic Content with Regex
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
test("cart shows item count", async ({ page }) => {
|
|
362
|
+
await page.goto("/cart");
|
|
363
|
+
|
|
364
|
+
await expect(page.getByRole("banner")).toMatchAriaSnapshot(`
|
|
365
|
+
- link "Home"
|
|
366
|
+
- link /\\d+ items?/
|
|
367
|
+
`);
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Before/After Interaction
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
test("dialog opens and closes", async ({ page }) => {
|
|
375
|
+
await page.goto("/");
|
|
376
|
+
|
|
377
|
+
// Before: no dialog
|
|
378
|
+
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
|
|
379
|
+
- button "Open Settings"
|
|
380
|
+
`);
|
|
381
|
+
|
|
382
|
+
// Open dialog
|
|
383
|
+
await page.getByRole("button", { name: /settings/i }).click();
|
|
384
|
+
|
|
385
|
+
// After: dialog visible
|
|
386
|
+
await expect(page.getByRole("dialog")).toMatchAriaSnapshot(`
|
|
387
|
+
- dialog "Settings":
|
|
388
|
+
- heading "Settings" [level=2]
|
|
389
|
+
- checkbox "Dark mode"
|
|
390
|
+
- checkbox "Notifications"
|
|
391
|
+
- button "Save"
|
|
392
|
+
- button "Cancel"
|
|
393
|
+
`);
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Combining with Other Assertions
|
|
398
|
+
|
|
399
|
+
ARIA snapshots validate structure. Combine with other assertions for complete coverage:
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
test("product page", async ({ page }) => {
|
|
403
|
+
await page.goto("/products/123");
|
|
404
|
+
|
|
405
|
+
// Structure validation
|
|
406
|
+
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
|
|
407
|
+
- img "Product photo"
|
|
408
|
+
- heading "Widget Pro" [level=1]
|
|
409
|
+
- text "$49.99"
|
|
410
|
+
- button "Add to cart"
|
|
411
|
+
`);
|
|
412
|
+
|
|
413
|
+
// Functional validation
|
|
414
|
+
await page.getByRole("button", { name: /add to cart/i }).click();
|
|
415
|
+
await expect(page.getByRole("alert")).toContainText("Added to cart");
|
|
416
|
+
|
|
417
|
+
// Accessibility validation (axe-core)
|
|
418
|
+
const results = await new AxeBuilder({ page }).analyze();
|
|
419
|
+
expect(results.violations).toEqual([]);
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Debugging
|
|
424
|
+
|
|
425
|
+
### View Current Structure
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
const snapshot = await page.getByRole("main").ariaSnapshot();
|
|
429
|
+
console.log(snapshot);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Chrome DevTools
|
|
433
|
+
|
|
434
|
+
1. Open DevTools
|
|
435
|
+
2. Go to Elements panel
|
|
436
|
+
3. Open Accessibility tab
|
|
437
|
+
4. Inspect the accessibility tree
|
|
438
|
+
|
|
439
|
+
### When Snapshots Fail
|
|
440
|
+
|
|
441
|
+
The error shows expected vs actual:
|
|
442
|
+
|
|
443
|
+
```
|
|
444
|
+
Expected:
|
|
445
|
+
- heading "Welcome" [level=1]
|
|
446
|
+
- button "Sign In"
|
|
447
|
+
|
|
448
|
+
Actual:
|
|
449
|
+
- heading "Welcome Back" [level=1]
|
|
450
|
+
- button "Log In"
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## Best Practices
|
|
454
|
+
|
|
455
|
+
### Scope Appropriately
|
|
456
|
+
|
|
457
|
+
```javascript
|
|
458
|
+
// BAD: Entire page - too broad, fragile
|
|
459
|
+
await expect(page.locator("body")).toMatchAriaSnapshot(`...`);
|
|
460
|
+
|
|
461
|
+
// GOOD: Specific region - focused, stable
|
|
462
|
+
await expect(page.getByRole("form", { name: /login/i })).toMatchAriaSnapshot(`...`);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Use for Structure, Not Content
|
|
466
|
+
|
|
467
|
+
```javascript
|
|
468
|
+
// BAD: Testing dynamic data
|
|
469
|
+
await expect(locator).toMatchAriaSnapshot(`
|
|
470
|
+
- text "Order #12345"
|
|
471
|
+
- text "Total: $127.50"
|
|
472
|
+
`);
|
|
473
|
+
|
|
474
|
+
// GOOD: Testing structure with flexible matching
|
|
475
|
+
await expect(locator).toMatchAriaSnapshot(`
|
|
476
|
+
- text /Order #\d+/
|
|
477
|
+
- text /Total: \$[\d.]+/
|
|
478
|
+
`);
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Keep Snapshots Readable
|
|
482
|
+
|
|
483
|
+
```javascript
|
|
484
|
+
// BAD: Too detailed, hard to review
|
|
485
|
+
await expect(locator).toMatchAriaSnapshot(`
|
|
486
|
+
- main:
|
|
487
|
+
- article:
|
|
488
|
+
- header:
|
|
489
|
+
- div:
|
|
490
|
+
- span:
|
|
491
|
+
- heading "Title" [level=1]
|
|
492
|
+
`);
|
|
493
|
+
|
|
494
|
+
// GOOD: Focus on meaningful structure
|
|
495
|
+
await expect(locator).toMatchAriaSnapshot(`
|
|
496
|
+
- heading "Title" [level=1]
|
|
497
|
+
- text "Description"
|
|
498
|
+
- button "Action"
|
|
499
|
+
`);
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Review Before Committing
|
|
503
|
+
|
|
504
|
+
Always review snapshot updates:
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
# See what changed
|
|
508
|
+
git diff **/*.aria.yml
|
|
509
|
+
|
|
510
|
+
# Verify changes are intentional before committing
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## References
|
|
514
|
+
|
|
515
|
+
- [Playwright ARIA Snapshots](https://playwright.dev/docs/aria-snapshots)
|
|
516
|
+
- [ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
|
|
517
|
+
- [Accessible Name Computation](https://www.w3.org/TR/accname-1.1/)
|