@schalkneethling/toolkit 0.5.0 → 0.5.3

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.
Files changed (26) hide show
  1. package/dist/index.mjs.map +1 -1
  2. package/hooks/auto-approve-safe-commands/hook.mjs +5 -1
  3. package/hooks/auto-approve-safe-commands/hook.mts +7 -6
  4. package/hooks/block-dangerous-commands/hook.mjs +3 -3
  5. package/hooks/block-dangerous-commands/hook.mts +10 -22
  6. package/package.json +9 -9
  7. package/skills/css-tokens/SKILL.md +1 -1
  8. package/skills/css-tokens/references/tokens.css +6 -10
  9. package/skills/frontend-security/SKILL.md +3 -0
  10. package/skills/frontend-security/references/csp-configuration.md +68 -51
  11. package/skills/frontend-security/references/csrf-protection.md +74 -70
  12. package/skills/frontend-security/references/dom-security.md +36 -29
  13. package/skills/frontend-security/references/file-upload-security.md +101 -69
  14. package/skills/frontend-security/references/framework-patterns.md +42 -40
  15. package/skills/frontend-security/references/input-validation.md +36 -31
  16. package/skills/frontend-security/references/jwt-security.md +68 -84
  17. package/skills/frontend-security/references/nodejs-npm-security.md +63 -55
  18. package/skills/frontend-security/references/xss-prevention.md +38 -36
  19. package/skills/frontend-testing/SKILL.md +31 -38
  20. package/skills/frontend-testing/references/accessibility-testing.md +56 -62
  21. package/skills/frontend-testing/references/aria-snapshots.md +35 -34
  22. package/skills/frontend-testing/references/locator-strategies.md +37 -40
  23. package/skills/frontend-testing/references/visual-regression.md +29 -23
  24. package/skills/npm-publishing-best-practices/SKILL.md +316 -0
  25. package/skills/semantic-html/SKILL.md +5 -21
  26. package/skills/semantic-html/references/heading-patterns.md +1 -5
@@ -5,6 +5,7 @@ Automated accessibility testing catches common issues early. Combine with manual
5
5
  ## Limitations of Automated Testing
6
6
 
7
7
  Automated tools detect approximately 30-40% of accessibility issues. They catch:
8
+
8
9
  - Missing labels and alt text
9
10
  - Color contrast violations
10
11
  - Invalid ARIA attributes
@@ -12,6 +13,7 @@ Automated tools detect approximately 30-40% of accessibility issues. They catch:
12
13
  - Missing landmark regions
13
14
 
14
15
  They cannot catch:
16
+
15
17
  - Logical reading order
16
18
  - Meaningful link text in context
17
19
  - Appropriate focus management
@@ -37,9 +39,9 @@ import AxeBuilder from "@axe-core/playwright";
37
39
  test.describe("Homepage", () => {
38
40
  test("has no automatically detectable accessibility violations", async ({ page }) => {
39
41
  await page.goto("/");
40
-
42
+
41
43
  const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
42
-
44
+
43
45
  expect(accessibilityScanResults.violations).toEqual([]);
44
46
  });
45
47
  });
@@ -52,15 +54,15 @@ Always ensure the page is in the expected state before scanning:
52
54
  ```javascript
53
55
  test("navigation menu is accessible when open", async ({ page }) => {
54
56
  await page.goto("/");
55
-
57
+
56
58
  // Open the menu
57
59
  await page.getByRole("button", { name: /menu/i }).click();
58
-
60
+
59
61
  // Wait for menu to be visible before scanning
60
62
  await page.getByRole("navigation", { name: /main/i }).waitFor();
61
-
63
+
62
64
  const results = await new AxeBuilder({ page }).analyze();
63
-
65
+
64
66
  expect(results.violations).toEqual([]);
65
67
  });
66
68
  ```
@@ -72,11 +74,11 @@ Focus on components you're testing:
72
74
  ```javascript
73
75
  test("checkout form is accessible", async ({ page }) => {
74
76
  await page.goto("/checkout");
75
-
77
+
76
78
  const results = await new AxeBuilder({ page })
77
- .include("#checkout-form") // Only scan this region
79
+ .include("#checkout-form") // Only scan this region
78
80
  .analyze();
79
-
81
+
80
82
  expect(results.violations).toEqual([]);
81
83
  });
82
84
  ```
@@ -87,8 +89,8 @@ Temporarily exclude elements while fixing issues:
87
89
 
88
90
  ```javascript
89
91
  const results = await new AxeBuilder({ page })
90
- .exclude("#third-party-widget") // Can't control this
91
- .exclude("[data-ad-unit]") // Ads managed externally
92
+ .exclude("#third-party-widget") // Can't control this
93
+ .exclude("[data-ad-unit]") // Ads managed externally
92
94
  .analyze();
93
95
  ```
94
96
 
@@ -110,19 +112,17 @@ const results = await new AxeBuilder({ page })
110
112
  .analyze();
111
113
 
112
114
  // Best practices (not WCAG requirements but recommended)
113
- const results = await new AxeBuilder({ page })
114
- .withTags(["best-practice"])
115
- .analyze();
115
+ const results = await new AxeBuilder({ page }).withTags(["best-practice"]).analyze();
116
116
  ```
117
117
 
118
118
  ### Common Tag Sets
119
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"]` |
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
126
 
127
127
  ## Creating Reusable Fixtures
128
128
 
@@ -135,11 +135,12 @@ import AxeBuilder from "@axe-core/playwright";
135
135
 
136
136
  export const test = base.extend({
137
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
-
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
+
143
144
  await use(makeAxeBuilder);
144
145
  },
145
146
  });
@@ -154,9 +155,9 @@ import { test, expect } from "./fixtures/axe";
154
155
 
155
156
  test("product page is accessible", async ({ page, makeAxeBuilder }) => {
156
157
  await page.goto("/products/123");
157
-
158
+
158
159
  const results = await makeAxeBuilder().analyze();
159
-
160
+
160
161
  expect(results.violations).toEqual([]);
161
162
  });
162
163
  ```
@@ -169,13 +170,13 @@ test("product page is accessible", async ({ page, makeAxeBuilder }) => {
169
170
  const results = await new AxeBuilder({ page }).analyze();
170
171
 
171
172
  // Structure of a violation
172
- results.violations.forEach(violation => {
173
+ results.violations.forEach((violation) => {
173
174
  console.log(`Rule: ${violation.id}`);
174
- console.log(`Impact: ${violation.impact}`); // minor, moderate, serious, critical
175
+ console.log(`Impact: ${violation.impact}`); // minor, moderate, serious, critical
175
176
  console.log(`Description: ${violation.description}`);
176
177
  console.log(`Help: ${violation.helpUrl}`);
177
-
178
- violation.nodes.forEach(node => {
178
+
179
+ violation.nodes.forEach((node) => {
179
180
  console.log(` Element: ${node.html}`);
180
181
  console.log(` Fix: ${node.failureSummary}`);
181
182
  });
@@ -184,12 +185,12 @@ results.violations.forEach(violation => {
184
185
 
185
186
  ### Impact Levels
186
187
 
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 |
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 |
193
194
 
194
195
  ### Progressive Enforcement
195
196
 
@@ -197,14 +198,12 @@ Start permissive, tighten over time:
197
198
 
198
199
  ```javascript
199
200
  // Phase 1: Only critical issues fail
200
- const criticalViolations = results.violations.filter(
201
- v => v.impact === "critical"
202
- );
201
+ const criticalViolations = results.violations.filter((v) => v.impact === "critical");
203
202
  expect(criticalViolations).toEqual([]);
204
203
 
205
204
  // Phase 2: Critical and serious
206
- const seriousViolations = results.violations.filter(
207
- v => ["critical", "serious"].includes(v.impact)
205
+ const seriousViolations = results.violations.filter((v) =>
206
+ ["critical", "serious"].includes(v.impact),
208
207
  );
209
208
  expect(seriousViolations).toEqual([]);
210
209
 
@@ -218,7 +217,7 @@ When you have a known issue being addressed:
218
217
 
219
218
  ```javascript
220
219
  const results = await new AxeBuilder({ page })
221
- .disableRules(["color-contrast"]) // Tracked in JIRA-123
220
+ .disableRules(["color-contrast"]) // Tracked in JIRA-123
222
221
  .analyze();
223
222
  ```
224
223
 
@@ -234,17 +233,13 @@ Test components in isolation:
234
233
  test.describe("Button component", () => {
235
234
  test("default button is accessible", async ({ page }) => {
236
235
  await page.goto("/storybook/button--default");
237
- const results = await new AxeBuilder({ page })
238
- .include("#storybook-root")
239
- .analyze();
236
+ const results = await new AxeBuilder({ page }).include("#storybook-root").analyze();
240
237
  expect(results.violations).toEqual([]);
241
238
  });
242
-
239
+
243
240
  test("disabled button is accessible", async ({ page }) => {
244
241
  await page.goto("/storybook/button--disabled");
245
- const results = await new AxeBuilder({ page })
246
- .include("#storybook-root")
247
- .analyze();
242
+ const results = await new AxeBuilder({ page }).include("#storybook-root").analyze();
248
243
  expect(results.violations).toEqual([]);
249
244
  });
250
245
  });
@@ -260,13 +255,13 @@ test("checkout flow is accessible at each step", async ({ page }) => {
260
255
  await page.goto("/cart");
261
256
  let results = await new AxeBuilder({ page }).analyze();
262
257
  expect(results.violations).toEqual([]);
263
-
258
+
264
259
  // Shipping form
265
260
  await page.getByRole("link", { name: /checkout/i }).click();
266
261
  await page.getByRole("heading", { name: /shipping/i }).waitFor();
267
262
  results = await new AxeBuilder({ page }).analyze();
268
263
  expect(results.violations).toEqual([]);
269
-
264
+
270
265
  // Payment form
271
266
  await page.getByRole("button", { name: /continue/i }).click();
272
267
  await page.getByRole("heading", { name: /payment/i }).waitFor();
@@ -282,19 +277,17 @@ Always re-scan after content changes:
282
277
  ```javascript
283
278
  test("modal is accessible when opened", async ({ page }) => {
284
279
  await page.goto("/");
285
-
280
+
286
281
  // Initial page scan
287
282
  let results = await new AxeBuilder({ page }).analyze();
288
283
  expect(results.violations).toEqual([]);
289
-
284
+
290
285
  // Open modal
291
286
  await page.getByRole("button", { name: /settings/i }).click();
292
287
  await page.getByRole("dialog").waitFor();
293
-
288
+
294
289
  // Scan modal
295
- results = await new AxeBuilder({ page })
296
- .include("[role='dialog']")
297
- .analyze();
290
+ results = await new AxeBuilder({ page }).include("[role='dialog']").analyze();
298
291
  expect(results.violations).toEqual([]);
299
292
  });
300
293
  ```
@@ -305,15 +298,15 @@ test("modal is accessible when opened", async ({ page }) => {
305
298
 
306
299
  ```html
307
300
  <!-- BAD -->
308
- <input type="email" placeholder="Email">
301
+ <input type="email" placeholder="Email" />
309
302
 
310
303
  <!-- GOOD -->
311
304
  <label for="email">Email address</label>
312
- <input id="email" type="email">
305
+ <input id="email" type="email" />
313
306
 
314
307
  <!-- ALSO GOOD (visually hidden label) -->
315
308
  <label for="search" class="visually-hidden">Search products</label>
316
- <input id="search" type="search" placeholder="Search...">
309
+ <input id="search" type="search" placeholder="Search..." />
317
310
  ```
318
311
 
319
312
  ### Color Contrast
@@ -353,7 +346,8 @@ test("modal is accessible when opened", async ({ page }) => {
353
346
  ```html
354
347
  <!-- BAD -->
355
348
  <input id="email" />
356
- <input id="email" /> <!-- Duplicate! -->
349
+ <input id="email" />
350
+ <!-- Duplicate! -->
357
351
 
358
352
  <!-- GOOD -->
359
353
  <input id="billing-email" />
@@ -31,14 +31,14 @@ await expect(page.getByRole("main")).toMatchAriaSnapshot(`
31
31
 
32
32
  ### Benefits Over Visual Snapshots
33
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) |
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
42
 
43
43
  ### What It Catches
44
44
 
@@ -122,7 +122,7 @@ State and properties in square brackets:
122
122
 
123
123
  # Link URLs (newer feature)
124
124
  - link "Documentation":
125
- - /url: "https://docs.example.com"
125
+ - /url: "https://docs.example.com"
126
126
  ```
127
127
 
128
128
  ### Nesting
@@ -131,18 +131,18 @@ Indentation shows hierarchy:
131
131
 
132
132
  ```yaml
133
133
  - navigation:
134
- - list:
135
- - listitem:
136
- - link "Home"
137
- - listitem:
138
- - link "Products"
139
- - listitem:
140
- - link "About"
134
+ - list:
135
+ - listitem:
136
+ - link "Home"
137
+ - listitem:
138
+ - link "Products"
139
+ - listitem:
140
+ - link "About"
141
141
  - main:
142
- - heading "Products" [level=1]
143
- - list:
144
- - listitem "Product A"
145
- - listitem "Product B"
142
+ - heading "Products" [level=1]
143
+ - list:
144
+ - listitem "Product A"
145
+ - listitem "Product B"
146
146
  ```
147
147
 
148
148
  ## Basic Usage
@@ -154,7 +154,7 @@ import { test, expect } from "@playwright/test";
154
154
 
155
155
  test("login form structure", async ({ page }) => {
156
156
  await page.goto("/login");
157
-
157
+
158
158
  await expect(page.getByRole("main")).toMatchAriaSnapshot(`
159
159
  - heading "Sign In" [level=1]
160
160
  - textbox "Email"
@@ -173,7 +173,7 @@ Store snapshots in separate YAML files:
173
173
  ```javascript
174
174
  test("dashboard structure", async ({ page }) => {
175
175
  await page.goto("/dashboard");
176
-
176
+
177
177
  await expect(page.getByRole("main")).toMatchAriaSnapshot({
178
178
  name: "dashboard-main.aria.yml",
179
179
  });
@@ -280,6 +280,7 @@ npx playwright codegen example.com
280
280
  ```
281
281
 
282
282
  In the recorder:
283
+
283
284
  1. Click "Assert snapshot" action
284
285
  2. Select the element
285
286
  3. ARIA snapshot is generated
@@ -289,7 +290,7 @@ In the recorder:
289
290
  ```javascript
290
291
  test("generate snapshot for review", async ({ page }) => {
291
292
  await page.goto("/products");
292
-
293
+
293
294
  // Get the YAML representation
294
295
  const snapshot = await page.getByRole("main").ariaSnapshot();
295
296
  console.log(snapshot);
@@ -320,16 +321,16 @@ Test different states of the same component:
320
321
  test.describe("Accordion", () => {
321
322
  test("collapsed state", async ({ page }) => {
322
323
  await page.goto("/accordion");
323
-
324
+
324
325
  await expect(page.getByRole("region", { name: /details/i })).toMatchAriaSnapshot(`
325
326
  - button "Details" [expanded=false]
326
327
  `);
327
328
  });
328
-
329
+
329
330
  test("expanded state", async ({ page }) => {
330
331
  await page.goto("/accordion");
331
332
  await page.getByRole("button", { name: /details/i }).click();
332
-
333
+
333
334
  await expect(page.getByRole("region", { name: /details/i })).toMatchAriaSnapshot(`
334
335
  - button "Details" [expanded=true]
335
336
  - text "Additional information here"
@@ -344,7 +345,7 @@ test.describe("Accordion", () => {
344
345
  test("shows validation errors", async ({ page }) => {
345
346
  await page.goto("/signup");
346
347
  await page.getByRole("button", { name: /submit/i }).click();
347
-
348
+
348
349
  await expect(page.getByRole("form")).toMatchAriaSnapshot(`
349
350
  - textbox "Email"
350
351
  - text "Email is required"
@@ -360,7 +361,7 @@ test("shows validation errors", async ({ page }) => {
360
361
  ```javascript
361
362
  test("cart shows item count", async ({ page }) => {
362
363
  await page.goto("/cart");
363
-
364
+
364
365
  await expect(page.getByRole("banner")).toMatchAriaSnapshot(`
365
366
  - link "Home"
366
367
  - link /\\d+ items?/
@@ -373,15 +374,15 @@ test("cart shows item count", async ({ page }) => {
373
374
  ```javascript
374
375
  test("dialog opens and closes", async ({ page }) => {
375
376
  await page.goto("/");
376
-
377
+
377
378
  // Before: no dialog
378
379
  await expect(page.getByRole("main")).toMatchAriaSnapshot(`
379
380
  - button "Open Settings"
380
381
  `);
381
-
382
+
382
383
  // Open dialog
383
384
  await page.getByRole("button", { name: /settings/i }).click();
384
-
385
+
385
386
  // After: dialog visible
386
387
  await expect(page.getByRole("dialog")).toMatchAriaSnapshot(`
387
388
  - dialog "Settings":
@@ -401,7 +402,7 @@ ARIA snapshots validate structure. Combine with other assertions for complete co
401
402
  ```javascript
402
403
  test("product page", async ({ page }) => {
403
404
  await page.goto("/products/123");
404
-
405
+
405
406
  // Structure validation
406
407
  await expect(page.getByRole("main")).toMatchAriaSnapshot(`
407
408
  - img "Product photo"
@@ -409,11 +410,11 @@ test("product page", async ({ page }) => {
409
410
  - text "$49.99"
410
411
  - button "Add to cart"
411
412
  `);
412
-
413
+
413
414
  // Functional validation
414
415
  await page.getByRole("button", { name: /add to cart/i }).click();
415
416
  await expect(page.getByRole("alert")).toContainText("Added to cart");
416
-
417
+
417
418
  // Accessibility validation (axe-core)
418
419
  const results = await new AxeBuilder({ page }).analyze();
419
420
  expect(results.violations).toEqual([]);
@@ -12,15 +12,15 @@ Query the accessibility tree. If this fails, the UI likely has accessibility iss
12
12
 
13
13
  ```javascript
14
14
  // Playwright
15
- page.getByRole("button", { name: /submit/i })
16
- page.getByRole("textbox", { name: /email/i })
17
- page.getByRole("heading", { level: 1 })
18
- page.getByRole("navigation")
19
- page.getByRole("listitem")
15
+ page.getByRole("button", { name: /submit/i });
16
+ page.getByRole("textbox", { name: /email/i });
17
+ page.getByRole("heading", { level: 1 });
18
+ page.getByRole("navigation");
19
+ page.getByRole("listitem");
20
20
 
21
21
  // Testing Library
22
- screen.getByRole("button", { name: /submit/i })
23
- screen.getByRole("checkbox", { checked: true })
22
+ screen.getByRole("button", { name: /submit/i });
23
+ screen.getByRole("checkbox", { checked: true });
24
24
  ```
25
25
 
26
26
  **Why first**: Users and assistive technologies perceive the page through roles. Testing with roles validates both functionality and accessibility.
@@ -29,12 +29,13 @@ screen.getByRole("checkbox", { checked: true })
29
29
 
30
30
  ```javascript
31
31
  // Multiple buttons? Filter by name
32
- page.getByRole("button", { name: /save/i }) // Save button
33
- page.getByRole("button", { name: /cancel/i }) // Cancel button
34
- page.getByRole("button", { name: /delete/i }) // Delete button
32
+ page.getByRole("button", { name: /save/i }); // Save button
33
+ page.getByRole("button", { name: /cancel/i }); // Cancel button
34
+ page.getByRole("button", { name: /delete/i }); // Delete button
35
35
  ```
36
36
 
37
37
  **Common roles**:
38
+
38
39
  - `button`, `link`, `textbox`, `checkbox`, `radio`
39
40
  - `combobox` (select), `listbox`, `option`
40
41
  - `heading`, `navigation`, `main`, `article`
@@ -48,12 +49,12 @@ How users find form fields. Validates label-input association.
48
49
 
49
50
  ```javascript
50
51
  // Playwright
51
- page.getByLabel("Email address")
52
- page.getByLabel(/password/i)
52
+ page.getByLabel("Email address");
53
+ page.getByLabel(/password/i);
53
54
 
54
55
  // Testing Library
55
- screen.getByLabelText("Email address")
56
- screen.getByLabelText(/confirm password/i)
56
+ screen.getByLabelText("Email address");
57
+ screen.getByLabelText(/confirm password/i);
57
58
  ```
58
59
 
59
60
  **Why second**: Form users navigate by labels. Tests using labels fail if labels are missing or improperly associated—catching real accessibility bugs.
@@ -63,8 +64,8 @@ screen.getByLabelText(/confirm password/i)
63
64
  Use only when labels aren't available. Placeholder is not a label substitute.
64
65
 
65
66
  ```javascript
66
- page.getByPlaceholder("Search products...")
67
- screen.getByPlaceholderText("Enter your query")
67
+ page.getByPlaceholder("Search products...");
68
+ screen.getByPlaceholderText("Enter your query");
68
69
  ```
69
70
 
70
71
  **Why third**: Better than test IDs, but UI should have proper labels.
@@ -75,12 +76,12 @@ For elements identified by their content.
75
76
 
76
77
  ```javascript
77
78
  // Playwright
78
- page.getByText("Welcome back, Sarah")
79
- page.getByText(/order confirmed/i)
79
+ page.getByText("Welcome back, Sarah");
80
+ page.getByText(/order confirmed/i);
80
81
 
81
82
  // Testing Library
82
- screen.getByText("No results found")
83
- screen.getByText(/loading/i)
83
+ screen.getByText("No results found");
84
+ screen.getByText(/loading/i);
84
85
  ```
85
86
 
86
87
  **Why fourth**: Useful for assertions on content, but doesn't verify semantic structure.
@@ -90,8 +91,8 @@ screen.getByText(/loading/i)
90
91
  For images with meaningful alt text.
91
92
 
92
93
  ```javascript
93
- page.getByAltText("Company logo")
94
- screen.getByAltText(/product photo/i)
94
+ page.getByAltText("Company logo");
95
+ screen.getByAltText(/product photo/i);
95
96
  ```
96
97
 
97
98
  ### 6. Title Attribute
@@ -99,8 +100,8 @@ screen.getByAltText(/product photo/i)
99
100
  Rarely used. Most elements shouldn't rely on title for identification.
100
101
 
101
102
  ```javascript
102
- page.getByTitle("Close dialog")
103
- screen.getByTitle(/help/i)
103
+ page.getByTitle("Close dialog");
104
+ screen.getByTitle(/help/i);
104
105
  ```
105
106
 
106
107
  ### 7. Test IDs (Last Resort)
@@ -109,18 +110,20 @@ Escape hatch when semantic queries fail. Provides no accessibility verification.
109
110
 
110
111
  ```javascript
111
112
  // Playwright
112
- page.getByTestId("pricing-calculator")
113
+ page.getByTestId("pricing-calculator");
113
114
 
114
115
  // Testing Library
115
- screen.getByTestId("data-grid")
116
+ screen.getByTestId("data-grid");
116
117
  ```
117
118
 
118
119
  **When appropriate**:
120
+
119
121
  - Complex dynamic components (data grids, charts)
120
122
  - Elements with no stable accessible name
121
123
  - Third-party components you can't modify
122
124
 
123
125
  **When to avoid**:
126
+
124
127
  - Any element that has text, label, or semantic role
125
128
  - As a default choice to "make tests easier"
126
129
 
@@ -163,12 +166,8 @@ When strict matching fails:
163
166
  ```javascript
164
167
  // Get all, then filter
165
168
  const buttons = await page.getByRole("button").all();
166
- const buttonTexts = await Promise.all(
167
- buttons.map(button => button.textContent())
168
- );
169
- const deleteButtons = buttons.filter((button, index) =>
170
- buttonTexts[index]?.includes("Delete")
171
- );
169
+ const buttonTexts = await Promise.all(buttons.map((button) => button.textContent()));
170
+ const deleteButtons = buttons.filter((button, index) => buttonTexts[index]?.includes("Delete"));
172
171
 
173
172
  // Or be more specific with the query
174
173
  page.getByRole("button", { name: "Delete", exact: true });
@@ -178,11 +177,11 @@ page.getByRole("button", { name: "Delete", exact: true });
178
177
 
179
178
  ### get vs query vs find
180
179
 
181
- | Variant | No match | Multiple matches | Async |
182
- |---------|----------|------------------|-------|
183
- | `getBy` | Throws | Throws | No |
184
- | `queryBy` | Returns null | Throws | No |
185
- | `findBy` | Throws | Throws | Yes (waits) |
180
+ | Variant | No match | Multiple matches | Async |
181
+ | --------- | ------------ | ---------------- | ----------- |
182
+ | `getBy` | Throws | Throws | No |
183
+ | `queryBy` | Returns null | Throws | No |
184
+ | `findBy` | Throws | Throws | Yes (waits) |
186
185
 
187
186
  ```javascript
188
187
  // Element should exist now
@@ -262,9 +261,7 @@ page.getByRole("alert");
262
261
 
263
262
  ```javascript
264
263
  // Log all accessible elements
265
- await page.getByRole("button").evaluateAll(buttons =>
266
- buttons.map(b => b.textContent)
267
- );
264
+ await page.getByRole("button").evaluateAll((buttons) => buttons.map((b) => b.textContent));
268
265
 
269
266
  // Use Playwright Inspector
270
267
  // npx playwright test --debug