@assertrx/dino 1.0.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.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. package/themes/dino.json +66 -0
@@ -0,0 +1,588 @@
1
+ # Choosing a Locator Strategy
2
+
3
+ > **When to use**: When deciding which Playwright locator method to use for an element
4
+
5
+ ## Quick Answer
6
+
7
+ Use `getByRole()` for everything that has a semantic HTML role (buttons, links, headings, form fields, dialogs). Fall back to `getByLabel()` for form fields, `getByText()` for plain content, and `getByTestId()` only as a last resort for custom components with no accessible role.
8
+
9
+ ## Decision Flowchart
10
+
11
+ ```
12
+ Start: You need to locate an element
13
+ |
14
+ v
15
+ Does the element have a semantic role?
16
+ (button, link, heading, textbox, checkbox, combobox, dialog, img, row, cell, navigation...)
17
+ |
18
+ +-- YES --> Use getByRole('role', { name: 'accessible name' })
19
+ | |
20
+ | +-- Need to narrow scope? Chain from a parent role locator:
21
+ | getByRole('navigation').getByRole('link', { name: '...' })
22
+ |
23
+ +-- NO
24
+ |
25
+ v
26
+ Is it a form field with a visible <label>?
27
+ |
28
+ +-- YES --> Use getByLabel('label text')
29
+ |
30
+ +-- NO
31
+ |
32
+ v
33
+ Is it static text content (paragraph, span, div with text)?
34
+ |
35
+ +-- YES --> Use getByText('text content')
36
+ | Prefer { exact: true } when text is short or common
37
+ |
38
+ +-- NO
39
+ |
40
+ v
41
+ Does it have a placeholder?
42
+ |
43
+ +-- YES --> Use getByPlaceholder('placeholder text')
44
+ | (Less preferred -- placeholders disappear on input)
45
+ |
46
+ +-- NO
47
+ |
48
+ v
49
+ Does it have a title attribute or alt text?
50
+ |
51
+ +-- YES --> Use getByTitle('...') or getByAltText('...')
52
+ |
53
+ +-- NO
54
+ |
55
+ v
56
+ Add data-testid="..." to the markup
57
+ Use getByTestId('identifier')
58
+ |
59
+ +--> NEVER fall back to CSS selectors or XPath.
60
+ Fix the markup instead.
61
+ ```
62
+
63
+ ## Decision Matrix
64
+
65
+ | Element Type | Recommended Locator | Fallback | Example |
66
+ |---|---|---|---|
67
+ | Button | `getByRole('button', { name })` | `getByText()` if role missing | `getByRole('button', { name: 'Submit' })` |
68
+ | Link | `getByRole('link', { name })` | `getByText()` for anchor text | `getByRole('link', { name: 'Sign up' })` |
69
+ | Text input | `getByLabel('...')` | `getByRole('textbox', { name })` | `getByLabel('Email address')` |
70
+ | Checkbox | `getByRole('checkbox', { name })` | `getByLabel()` | `getByRole('checkbox', { name: 'Accept terms' })` |
71
+ | Radio button | `getByRole('radio', { name })` | `getByLabel()` | `getByRole('radio', { name: 'Express shipping' })` |
72
+ | Dropdown / Select | `getByRole('combobox', { name })` | `getByLabel()` | `getByLabel('Country')` |
73
+ | Heading | `getByRole('heading', { name, level })` | `getByText()` | `getByRole('heading', { name: 'Dashboard', level: 1 })` |
74
+ | Nav link | chain: `getByRole('navigation').getByRole('link', { name })` | scope with `locator('nav')` | see detailed example below |
75
+ | Table cell | chain: `getByRole('row').filter().getByRole('cell')` | `locator('td')` scoped | see detailed example below |
76
+ | Image | `getByRole('img', { name })` | `getByAltText()` | `getByRole('img', { name: 'Company logo' })` |
77
+ | Modal / Dialog | `getByRole('dialog')` then chain within | `locator('[role="dialog"]')` | `getByRole('dialog').getByRole('button', { name: 'Confirm' })` |
78
+ | Dynamic list item | `.filter({ hasText })` or `.filter({ has })` | `nth()` as last resort | `getByRole('listitem').filter({ hasText: 'Milk' })` |
79
+ | Custom component | `getByTestId('...')` | Add `data-testid` to markup | `getByTestId('color-picker')` |
80
+
81
+ ## Detailed Analysis
82
+
83
+ ### Tier 1: `getByRole()` -- Use by Default
84
+
85
+ The strongest locator. It mirrors how assistive technology and real users perceive the page.
86
+
87
+ **Pros**
88
+ - Resilient to markup refactors (class names, tag changes)
89
+ - Enforces accessible markup -- if the locator breaks, your accessibility broke too
90
+ - Works across frameworks (React, Vue, Angular, plain HTML)
91
+ - Supports filtering by `name`, `level`, `checked`, `pressed`, `expanded`, `selected`, and (Playwright 1.60+) `description`
92
+
93
+ **Cons**
94
+ - Requires the element to have a valid ARIA role (implicit or explicit)
95
+ - Can match multiple elements when names are duplicated -- scope with chaining, `exact`, or `description`
96
+
97
+ **When it fails**: Custom components with `<div>` soup and no ARIA roles. Fix the component first; add `getByTestId()` only if you cannot change the markup.
98
+
99
+ **Disambiguating duplicates (Playwright 1.60+)**: when two controls share a name, prefer scoping/chaining first; if they differ only by accessible description (`aria-describedby`), use the `description` option rather than dropping to CSS — `getByRole('button', { name: 'Delete', description: 'Removes this project' })`. See [core/locators.md](locators.md#disambiguating-by-accessible-description-description-playwright-160).
100
+
101
+ ---
102
+
103
+ ### Tier 2: `getByLabel()` -- Form Fields
104
+
105
+ Queries by the associated `<label>` text. This is often the most readable locator for form fields.
106
+
107
+ **Pros**
108
+ - Extremely readable: `getByLabel('Password')` tells you exactly what field
109
+ - Works with `<label for="...">`, wrapping `<label>`, and `aria-labelledby`
110
+
111
+ **Cons**
112
+ - Only works for form elements with labels
113
+ - Breaks if someone changes label text (but that is usually intentional)
114
+
115
+ **Use over `getByRole('textbox')`** when the label text is clear and unique. Use `getByRole()` when you need to differentiate between multiple fields with similar labels by role type.
116
+
117
+ ---
118
+
119
+ ### Tier 3: `getByText()` -- Static Content
120
+
121
+ Finds elements by their visible text content.
122
+
123
+ **Pros**
124
+ - Intuitive for non-interactive text (paragraphs, spans, badges, status messages)
125
+ - Supports exact and substring matching
126
+
127
+ **Cons**
128
+ - Fragile if text is dynamic, translated, or duplicated
129
+ - Can match parent elements unintentionally -- use `{ exact: true }` or scope the query
130
+
131
+ **Rule of thumb**: Use `getByText()` for assertions and content verification, not for interactive elements. Interactive elements should use `getByRole()`.
132
+
133
+ ---
134
+
135
+ ### Tier 4: `getByPlaceholder()` -- Inputs Without Labels
136
+
137
+ Locates by placeholder attribute value.
138
+
139
+ **Pros**
140
+ - Works when labels are missing (search bars, minimal UIs)
141
+
142
+ **Cons**
143
+ - Placeholders disappear when the user types -- poor UX foundation
144
+ - Signals missing accessibility (no label)
145
+
146
+ **Treat as a yellow flag**: If you use this, consider filing a ticket to add a proper label.
147
+
148
+ ---
149
+
150
+ ### Tier 5: `getByTestId()` -- Last Resort
151
+
152
+ Locates by `data-testid` attribute.
153
+
154
+ **Pros**
155
+ - Fully decoupled from user-facing text and structure
156
+ - Stable under UI redesigns
157
+
158
+ **Cons**
159
+ - Invisible to users and assistive technology
160
+ - Pollutes production markup (unless stripped in build)
161
+ - Tells you nothing about what the element looks like or does
162
+
163
+ **Use only when**: The component has no semantic role, no label, no text, and you cannot change the markup. Common cases: canvas elements, third-party widgets, complex custom components.
164
+
165
+ ---
166
+
167
+ ### Never Use: Raw CSS Selectors or XPath
168
+
169
+ ```
170
+ // DO NOT do this
171
+ page.locator('.btn-primary'); // class names change
172
+ page.locator('#submit-btn'); // IDs are brittle
173
+ page.locator('div > span:nth-child(2)'); // structure changes break this
174
+ page.locator('xpath=//div[@class="foo"]'); // unreadable, fragile
175
+ ```
176
+
177
+ If you are reaching for a CSS selector, stop. Walk back up the flowchart and find a semantic locator. If none exists, add `data-testid`.
178
+
179
+ ## Real-World Examples
180
+
181
+ ### 1. Buttons
182
+
183
+ ```typescript
184
+ // TypeScript
185
+ // Standard button
186
+ await page.getByRole('button', { name: 'Submit' }).click();
187
+
188
+ // Icon-only button (uses aria-label)
189
+ await page.getByRole('button', { name: 'Close' }).click();
190
+
191
+ // Button inside a specific section
192
+ await page.getByRole('region', { name: 'Billing' })
193
+ .getByRole('button', { name: 'Update' }).click();
194
+ ```
195
+
196
+ ```javascript
197
+ // JavaScript
198
+ // Standard button
199
+ await page.getByRole('button', { name: 'Submit' }).click();
200
+
201
+ // Icon-only button (uses aria-label)
202
+ await page.getByRole('button', { name: 'Close' }).click();
203
+
204
+ // Button inside a specific section
205
+ await page.getByRole('region', { name: 'Billing' })
206
+ .getByRole('button', { name: 'Update' }).click();
207
+ ```
208
+
209
+ ---
210
+
211
+ ### 2. Links
212
+
213
+ ```typescript
214
+ // TypeScript
215
+ // Standard link
216
+ await page.getByRole('link', { name: 'Sign up' }).click();
217
+
218
+ // Link inside navigation
219
+ await page.getByRole('navigation')
220
+ .getByRole('link', { name: 'Pricing' }).click();
221
+
222
+ // Link with exact match (avoid partial hits)
223
+ await page.getByRole('link', { name: 'Log in', exact: true }).click();
224
+ ```
225
+
226
+ ```javascript
227
+ // JavaScript
228
+ await page.getByRole('link', { name: 'Sign up' }).click();
229
+
230
+ await page.getByRole('navigation')
231
+ .getByRole('link', { name: 'Pricing' }).click();
232
+
233
+ await page.getByRole('link', { name: 'Log in', exact: true }).click();
234
+ ```
235
+
236
+ ---
237
+
238
+ ### 3. Text Inputs
239
+
240
+ ```typescript
241
+ // TypeScript
242
+ // Input with a visible label -- preferred
243
+ await page.getByLabel('Email address').fill('user@example.com');
244
+
245
+ // When multiple textboxes exist and you need role specificity
246
+ await page.getByRole('textbox', { name: 'Email address' }).fill('user@example.com');
247
+
248
+ // Textarea
249
+ await page.getByLabel('Message').fill('Hello, world');
250
+
251
+ // Search input (role = searchbox)
252
+ await page.getByRole('searchbox', { name: 'Search' }).fill('playwright');
253
+ ```
254
+
255
+ ```javascript
256
+ // JavaScript
257
+ await page.getByLabel('Email address').fill('user@example.com');
258
+
259
+ await page.getByRole('textbox', { name: 'Email address' }).fill('user@example.com');
260
+
261
+ await page.getByLabel('Message').fill('Hello, world');
262
+
263
+ await page.getByRole('searchbox', { name: 'Search' }).fill('playwright');
264
+ ```
265
+
266
+ ---
267
+
268
+ ### 4. Checkboxes and Radios
269
+
270
+ ```typescript
271
+ // TypeScript
272
+ // Checkbox
273
+ await page.getByRole('checkbox', { name: 'Accept terms' }).check();
274
+
275
+ // Verify checked state
276
+ await expect(page.getByRole('checkbox', { name: 'Accept terms' })).toBeChecked();
277
+
278
+ // Radio button
279
+ await page.getByRole('radio', { name: 'Express shipping' }).check();
280
+
281
+ // Radio within a group (fieldset with legend)
282
+ await page.getByRole('group', { name: 'Shipping method' })
283
+ .getByRole('radio', { name: 'Express' }).check();
284
+ ```
285
+
286
+ ```javascript
287
+ // JavaScript
288
+ await page.getByRole('checkbox', { name: 'Accept terms' }).check();
289
+
290
+ await expect(page.getByRole('checkbox', { name: 'Accept terms' })).toBeChecked();
291
+
292
+ await page.getByRole('radio', { name: 'Express shipping' }).check();
293
+
294
+ await page.getByRole('group', { name: 'Shipping method' })
295
+ .getByRole('radio', { name: 'Express' }).check();
296
+ ```
297
+
298
+ ---
299
+
300
+ ### 5. Dropdowns and Selects
301
+
302
+ ```typescript
303
+ // TypeScript
304
+ // Native <select> element
305
+ await page.getByLabel('Country').selectOption('Canada');
306
+
307
+ // Custom combobox (ARIA combobox role)
308
+ await page.getByRole('combobox', { name: 'Country' }).click();
309
+ await page.getByRole('option', { name: 'Canada' }).click();
310
+
311
+ // Listbox pattern
312
+ await page.getByRole('combobox', { name: 'Font size' }).click();
313
+ await page.getByRole('listbox').getByRole('option', { name: '16px' }).click();
314
+ ```
315
+
316
+ ```javascript
317
+ // JavaScript
318
+ await page.getByLabel('Country').selectOption('Canada');
319
+
320
+ await page.getByRole('combobox', { name: 'Country' }).click();
321
+ await page.getByRole('option', { name: 'Canada' }).click();
322
+
323
+ await page.getByRole('combobox', { name: 'Font size' }).click();
324
+ await page.getByRole('listbox').getByRole('option', { name: '16px' }).click();
325
+ ```
326
+
327
+ ---
328
+
329
+ ### 6. Headings
330
+
331
+ ```typescript
332
+ // TypeScript
333
+ // Specific heading level
334
+ await expect(page.getByRole('heading', { name: 'Dashboard', level: 1 })).toBeVisible();
335
+
336
+ // Any heading with that name (when level does not matter)
337
+ await expect(page.getByRole('heading', { name: 'Recent activity' })).toBeVisible();
338
+
339
+ // Heading within a section
340
+ await page.getByRole('region', { name: 'Sidebar' })
341
+ .getByRole('heading', { name: 'Categories' });
342
+ ```
343
+
344
+ ```javascript
345
+ // JavaScript
346
+ await expect(page.getByRole('heading', { name: 'Dashboard', level: 1 })).toBeVisible();
347
+
348
+ await expect(page.getByRole('heading', { name: 'Recent activity' })).toBeVisible();
349
+
350
+ await page.getByRole('region', { name: 'Sidebar' })
351
+ .getByRole('heading', { name: 'Categories' });
352
+ ```
353
+
354
+ ---
355
+
356
+ ### 7. Navigation Items
357
+
358
+ ```typescript
359
+ // TypeScript
360
+ // Link inside the main nav
361
+ await page.getByRole('navigation')
362
+ .getByRole('link', { name: 'Pricing' }).click();
363
+
364
+ // When there are multiple navs, narrow by aria-label
365
+ await page.getByRole('navigation', { name: 'Main menu' })
366
+ .getByRole('link', { name: 'Pricing' }).click();
367
+
368
+ // Breadcrumb navigation
369
+ await page.getByRole('navigation', { name: 'Breadcrumb' })
370
+ .getByRole('link', { name: 'Products' }).click();
371
+ ```
372
+
373
+ ```javascript
374
+ // JavaScript
375
+ await page.getByRole('navigation')
376
+ .getByRole('link', { name: 'Pricing' }).click();
377
+
378
+ await page.getByRole('navigation', { name: 'Main menu' })
379
+ .getByRole('link', { name: 'Pricing' }).click();
380
+
381
+ await page.getByRole('navigation', { name: 'Breadcrumb' })
382
+ .getByRole('link', { name: 'Products' }).click();
383
+ ```
384
+
385
+ ---
386
+
387
+ ### 8. Table Cells
388
+
389
+ ```typescript
390
+ // TypeScript
391
+ // Find a cell in a specific row
392
+ await page.getByRole('row', { name: /Jane Smith/ })
393
+ .getByRole('cell', { name: '$120.00' });
394
+
395
+ // Click an action button in a specific row
396
+ await page.getByRole('row', { name: /Jane Smith/ })
397
+ .getByRole('button', { name: 'Edit' }).click();
398
+
399
+ // Filter rows using .filter() for complex matching
400
+ const row = page.getByRole('row').filter({ hasText: 'Pending' });
401
+ await row.getByRole('button', { name: 'Approve' }).click();
402
+
403
+ // Verify table header exists
404
+ await expect(page.getByRole('columnheader', { name: 'Status' })).toBeVisible();
405
+ ```
406
+
407
+ ```javascript
408
+ // JavaScript
409
+ await page.getByRole('row', { name: /Jane Smith/ })
410
+ .getByRole('cell', { name: '$120.00' });
411
+
412
+ await page.getByRole('row', { name: /Jane Smith/ })
413
+ .getByRole('button', { name: 'Edit' }).click();
414
+
415
+ const row = page.getByRole('row').filter({ hasText: 'Pending' });
416
+ await row.getByRole('button', { name: 'Approve' }).click();
417
+
418
+ await expect(page.getByRole('columnheader', { name: 'Status' })).toBeVisible();
419
+ ```
420
+
421
+ ---
422
+
423
+ ### 9. Images
424
+
425
+ ```typescript
426
+ // TypeScript
427
+ // Image with alt text
428
+ await expect(page.getByRole('img', { name: 'Company logo' })).toBeVisible();
429
+
430
+ // Alternative: getByAltText (same result, less preferred)
431
+ await expect(page.getByAltText('Company logo')).toBeVisible();
432
+
433
+ // Avatar image inside a card
434
+ await page.locator('article').filter({ hasText: 'Jane Smith' })
435
+ .getByRole('img', { name: "Jane Smith's avatar" });
436
+ ```
437
+
438
+ ```javascript
439
+ // JavaScript
440
+ await expect(page.getByRole('img', { name: 'Company logo' })).toBeVisible();
441
+
442
+ await expect(page.getByAltText('Company logo')).toBeVisible();
443
+
444
+ await page.locator('article').filter({ hasText: 'Jane Smith' })
445
+ .getByRole('img', { name: "Jane Smith's avatar" });
446
+ ```
447
+
448
+ ---
449
+
450
+ ### 10. Custom Components (No ARIA Role)
451
+
452
+ ```typescript
453
+ // TypeScript
454
+ // Third-party color picker with no semantic role
455
+ await page.getByTestId('color-picker').click();
456
+
457
+ // Custom drag-and-drop zone
458
+ await page.getByTestId('drop-zone').dispatchEvent('drop', { dataTransfer });
459
+
460
+ // Canvas-based chart
461
+ const chart = page.getByTestId('revenue-chart');
462
+ await expect(chart).toBeVisible();
463
+ ```
464
+
465
+ ```javascript
466
+ // JavaScript
467
+ await page.getByTestId('color-picker').click();
468
+
469
+ await page.getByTestId('drop-zone').dispatchEvent('drop', { dataTransfer });
470
+
471
+ const chart = page.getByTestId('revenue-chart');
472
+ await expect(chart).toBeVisible();
473
+ ```
474
+
475
+ **Before reaching for `getByTestId`, ask yourself**: Can I add `role` and `aria-label` to this component instead? If yes, do that and use `getByRole()`.
476
+
477
+ ---
478
+
479
+ ### 11. Dynamic Lists
480
+
481
+ ```typescript
482
+ // TypeScript
483
+ // Filter a list item by text
484
+ const item = page.getByRole('listitem').filter({ hasText: 'Milk' });
485
+ await item.getByRole('button', { name: 'Remove' }).click();
486
+
487
+ // Filter by a child locator
488
+ const card = page.locator('.product-card').filter({
489
+ has: page.getByText('Out of stock'),
490
+ });
491
+ await expect(card).toHaveCount(3);
492
+
493
+ // Count items in a list
494
+ await expect(page.getByRole('listitem')).toHaveCount(5);
495
+
496
+ // Iterate over list items for complex assertions
497
+ for (const item of await page.getByRole('listitem').all()) {
498
+ await expect(item).toContainText('$');
499
+ }
500
+ ```
501
+
502
+ ```javascript
503
+ // JavaScript
504
+ const item = page.getByRole('listitem').filter({ hasText: 'Milk' });
505
+ await item.getByRole('button', { name: 'Remove' }).click();
506
+
507
+ const card = page.locator('.product-card').filter({
508
+ has: page.getByText('Out of stock'),
509
+ });
510
+ await expect(card).toHaveCount(3);
511
+
512
+ await expect(page.getByRole('listitem')).toHaveCount(5);
513
+
514
+ for (const item of await page.getByRole('listitem').all()) {
515
+ await expect(item).toContainText('$');
516
+ }
517
+ ```
518
+
519
+ ---
520
+
521
+ ### 12. Modals and Dialogs
522
+
523
+ ```typescript
524
+ // TypeScript
525
+ // Wait for dialog to appear, then interact within it
526
+ const dialog = page.getByRole('dialog', { name: 'Confirm deletion' });
527
+ await expect(dialog).toBeVisible();
528
+ await dialog.getByRole('button', { name: 'Delete' }).click();
529
+
530
+ // Fill a form inside a dialog
531
+ const modal = page.getByRole('dialog', { name: 'Edit profile' });
532
+ await modal.getByLabel('Display name').fill('Jane');
533
+ await modal.getByRole('button', { name: 'Save' }).click();
534
+
535
+ // Verify dialog closed
536
+ await expect(dialog).toBeHidden();
537
+ ```
538
+
539
+ ```javascript
540
+ // JavaScript
541
+ const dialog = page.getByRole('dialog', { name: 'Confirm deletion' });
542
+ await expect(dialog).toBeVisible();
543
+ await dialog.getByRole('button', { name: 'Delete' }).click();
544
+
545
+ const modal = page.getByRole('dialog', { name: 'Edit profile' });
546
+ await modal.getByLabel('Display name').fill('Jane');
547
+ await modal.getByRole('button', { name: 'Save' }).click();
548
+
549
+ await expect(dialog).toBeHidden();
550
+ ```
551
+
552
+ ## Anti-Patterns to Avoid
553
+
554
+ | Anti-Pattern | Why It Fails | Use Instead |
555
+ |---|---|---|
556
+ | `page.locator('.btn-primary')` | Class names change during refactors and redesigns | `getByRole('button', { name: '...' })` |
557
+ | `page.locator('#email-input')` | IDs are implementation details, not user-visible | `getByLabel('Email')` |
558
+ | `page.locator('div > form > input:first-child')` | Any structural change breaks the selector | `getByLabel('...')` or `getByRole('textbox', { name: '...' })` |
559
+ | `page.locator('[data-testid="submit"]')` | Raw CSS for test IDs -- use the built-in method | `getByTestId('submit')` |
560
+ | `page.getByText('Submit')` for a button | Matches any element with that text, not just the button | `getByRole('button', { name: 'Submit' })` |
561
+ | `page.locator('button').nth(2)` | Index-based -- breaks when order changes | `getByRole('button', { name: '...' })` |
562
+
563
+ ## Scoping Strategy: When Multiple Elements Match
564
+
565
+ When a locator matches more than one element, narrow scope rather than using `nth()`:
566
+
567
+ ```typescript
568
+ // BAD: fragile index
569
+ page.getByRole('button', { name: 'Edit' }).nth(0);
570
+
571
+ // GOOD: scope to a parent section
572
+ page.getByRole('region', { name: 'Billing' })
573
+ .getByRole('button', { name: 'Edit' });
574
+
575
+ // GOOD: scope to a table row
576
+ page.getByRole('row', { name: /Order #1234/ })
577
+ .getByRole('button', { name: 'Edit' });
578
+
579
+ // GOOD: scope with filter
580
+ page.locator('article').filter({ hasText: 'Draft' })
581
+ .getByRole('button', { name: 'Edit' });
582
+ ```
583
+
584
+ ## Related
585
+
586
+ - [Playwright Locators documentation](https://playwright.dev/docs/locators)
587
+ - [Playwright Best Practices](https://playwright.dev/docs/best-practices)
588
+ - [ARIA Roles reference (MDN)](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)