@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,523 @@
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
+
284
+ 1. Click "Assert snapshot" action
285
+ 2. Select the element
286
+ 3. ARIA snapshot is generated
287
+
288
+ ### Programmatically
289
+
290
+ ```javascript
291
+ test("generate snapshot for review", async ({ page }) => {
292
+ await page.goto("/products");
293
+
294
+ // Get the YAML representation
295
+ const snapshot = await page.getByRole("main").ariaSnapshot();
296
+ console.log(snapshot);
297
+ });
298
+ ```
299
+
300
+ ### Updating Snapshots
301
+
302
+ When structure intentionally changes:
303
+
304
+ ```bash
305
+ npx playwright test --update-snapshots
306
+ ```
307
+
308
+ Review changes before committing:
309
+
310
+ ```bash
311
+ git diff
312
+ ```
313
+
314
+ ## Testing Patterns
315
+
316
+ ### Component States
317
+
318
+ Test different states of the same component:
319
+
320
+ ```javascript
321
+ test.describe("Accordion", () => {
322
+ test("collapsed state", async ({ page }) => {
323
+ await page.goto("/accordion");
324
+
325
+ await expect(page.getByRole("region", { name: /details/i })).toMatchAriaSnapshot(`
326
+ - button "Details" [expanded=false]
327
+ `);
328
+ });
329
+
330
+ test("expanded state", async ({ page }) => {
331
+ await page.goto("/accordion");
332
+ await page.getByRole("button", { name: /details/i }).click();
333
+
334
+ await expect(page.getByRole("region", { name: /details/i })).toMatchAriaSnapshot(`
335
+ - button "Details" [expanded=true]
336
+ - text "Additional information here"
337
+ `);
338
+ });
339
+ });
340
+ ```
341
+
342
+ ### Form Validation States
343
+
344
+ ```javascript
345
+ test("shows validation errors", async ({ page }) => {
346
+ await page.goto("/signup");
347
+ await page.getByRole("button", { name: /submit/i }).click();
348
+
349
+ await expect(page.getByRole("form")).toMatchAriaSnapshot(`
350
+ - textbox "Email"
351
+ - text "Email is required"
352
+ - textbox "Password"
353
+ - text "Password is required"
354
+ - button "Submit"
355
+ `);
356
+ });
357
+ ```
358
+
359
+ ### Dynamic Content with Regex
360
+
361
+ ```javascript
362
+ test("cart shows item count", async ({ page }) => {
363
+ await page.goto("/cart");
364
+
365
+ await expect(page.getByRole("banner")).toMatchAriaSnapshot(`
366
+ - link "Home"
367
+ - link /\\d+ items?/
368
+ `);
369
+ });
370
+ ```
371
+
372
+ ### Before/After Interaction
373
+
374
+ ```javascript
375
+ test("dialog opens and closes", async ({ page }) => {
376
+ await page.goto("/");
377
+
378
+ // Before: no dialog
379
+ await expect(page.getByRole("main")).toMatchAriaSnapshot(`
380
+ - button "Open Settings"
381
+ `);
382
+
383
+ // Open dialog
384
+ await page.getByRole("button", { name: /settings/i }).click();
385
+
386
+ // After: dialog visible
387
+ await expect(page.getByRole("dialog")).toMatchAriaSnapshot(`
388
+ - dialog "Settings":
389
+ - heading "Settings" [level=2]
390
+ - checkbox "Dark mode"
391
+ - checkbox "Notifications"
392
+ - button "Save"
393
+ - button "Cancel"
394
+ `);
395
+
396
+ await page.getByRole("button", { name: "Cancel" }).click();
397
+ await expect(page.getByRole("dialog")).toBeHidden();
398
+ });
399
+ ```
400
+
401
+ ## Combining with Other Assertions
402
+
403
+ ARIA snapshots validate structure. Combine with other assertions for complete coverage:
404
+
405
+ ```javascript
406
+ import AxeBuilder from "@axe-core/playwright";
407
+
408
+ test("product page", async ({ page }) => {
409
+ await page.goto("/products/123");
410
+
411
+ // Structure validation
412
+ await expect(page.getByRole("main")).toMatchAriaSnapshot(`
413
+ - img "Product photo"
414
+ - heading "Widget Pro" [level=1]
415
+ - text "$49.99"
416
+ - button "Add to cart"
417
+ `);
418
+
419
+ // Functional validation
420
+ await page.getByRole("button", { name: /add to cart/i }).click();
421
+ await expect(page.getByRole("alert")).toContainText("Added to cart");
422
+
423
+ // Accessibility validation (axe-core)
424
+ const results = await new AxeBuilder({ page }).analyze();
425
+ expect(results.violations).toEqual([]);
426
+ });
427
+ ```
428
+
429
+ ## Debugging
430
+
431
+ ### View Current Structure
432
+
433
+ ```javascript
434
+ const snapshot = await page.getByRole("main").ariaSnapshot();
435
+ console.log(snapshot);
436
+ ```
437
+
438
+ ### Chrome DevTools
439
+
440
+ 1. Open DevTools
441
+ 2. Go to Elements panel
442
+ 3. Open Accessibility tab
443
+ 4. Inspect the accessibility tree
444
+
445
+ ### When Snapshots Fail
446
+
447
+ The error shows expected vs actual:
448
+
449
+ ```text
450
+ Expected:
451
+ - heading "Welcome" [level=1]
452
+ - button "Sign In"
453
+
454
+ Actual:
455
+ - heading "Welcome Back" [level=1]
456
+ - button "Log In"
457
+ ```
458
+
459
+ ## Best Practices
460
+
461
+ ### Scope Appropriately
462
+
463
+ ```javascript
464
+ // BAD: Entire page - too broad, fragile
465
+ await expect(page.locator("body")).toMatchAriaSnapshot(`...`);
466
+
467
+ // GOOD: Specific region - focused, stable
468
+ await expect(page.getByRole("form", { name: /login/i })).toMatchAriaSnapshot(`...`);
469
+ ```
470
+
471
+ ### Use for Structure, Not Content
472
+
473
+ ```javascript
474
+ // BAD: Testing dynamic data
475
+ await expect(locator).toMatchAriaSnapshot(`
476
+ - text "Order #12345"
477
+ - text "Total: $127.50"
478
+ `);
479
+
480
+ // GOOD: Testing structure with flexible matching
481
+ await expect(locator).toMatchAriaSnapshot(`
482
+ - text /Order #\d+/
483
+ - text /Total: \$[\d.]+/
484
+ `);
485
+ ```
486
+
487
+ ### Keep Snapshots Readable
488
+
489
+ ```javascript
490
+ // BAD: Too detailed, hard to review
491
+ await expect(locator).toMatchAriaSnapshot(`
492
+ - main:
493
+ - article:
494
+ - header:
495
+ - div:
496
+ - span:
497
+ - heading "Title" [level=1]
498
+ `);
499
+
500
+ // GOOD: Focus on meaningful structure
501
+ await expect(locator).toMatchAriaSnapshot(`
502
+ - heading "Title" [level=1]
503
+ - text "Description"
504
+ - button "Action"
505
+ `);
506
+ ```
507
+
508
+ ### Review Before Committing
509
+
510
+ Always review snapshot updates:
511
+
512
+ ```bash
513
+ # See what changed
514
+ git diff **/*.aria.yml
515
+
516
+ # Verify changes are intentional before committing
517
+ ```
518
+
519
+ ## References
520
+
521
+ - [Playwright ARIA Snapshots](https://playwright.dev/docs/aria-snapshots)
522
+ - [ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
523
+ - [Accessible Name Computation](https://www.w3.org/TR/accname-1.1/)