@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.
@@ -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/)