@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,743 @@
1
+ # Locators
2
+
3
+ > **When to use**: Every time you need to find an element on the page. Start here before reaching for CSS or XPath.
4
+ > **Prerequisites**: [core/configuration.md](configuration.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Priority order — use the first one that works:
10
+ page.getByRole('button', { name: 'Submit' }) // 1. Role (default)
11
+ page.getByLabel('Email address') // 2. Label (form fields)
12
+ page.getByText('Welcome back') // 3. Text (non-interactive)
13
+ page.getByPlaceholder('Search...') // 4. Placeholder
14
+ page.getByAltText('Company logo') // 5. Alt text (images)
15
+ page.getByTitle('Close dialog') // 6. Title attribute
16
+ page.getByTestId('checkout-summary') // 7. Test ID (last semantic option)
17
+ page.locator('css=.legacy-widget >> internal:role=button') // 8. CSS/XPath (last resort)
18
+ ```
19
+
20
+ ## Recent Locator Helpers (Playwright 1.59+)
21
+
22
+ > Playwright 1.60 also adds a `description` option to `getByRole()` for disambiguating controls with the same name — see [Role-Based Locators](#disambiguating-by-accessible-description-description-playwright-160) below.
23
+
24
+ Playwright 1.59 added two useful locator-discovery helpers:
25
+
26
+ - Interactive locator picking for debugging and exploration
27
+ - `locator.normalize()` for rewriting brittle selectors toward Playwright best practices
28
+
29
+ Use these during debugging, refactors, and authoring. Do not treat them as a substitute for thinking through the most user-facing locator yourself. The best production locator is still usually `getByRole()`, `getByLabel()`, or `getByTestId()` when semantics are unavailable.
30
+
31
+ ### `page.pickLocator()` For Interactive Discovery
32
+
33
+ **Use when**: You are exploring a page, debugging a selector, or migrating a legacy suite and want Playwright to suggest a locator for the element under your cursor.
34
+ **Avoid when**: Writing final production tests without review. Treat the picked locator as a starting point, not the final answer.
35
+
36
+ **TypeScript**
37
+ ```typescript
38
+ import { test } from '@playwright/test';
39
+
40
+ test('pick a locator during debugging', async ({ page }) => {
41
+ await page.goto('/checkout');
42
+
43
+ const picked = await page.pickLocator();
44
+ console.log(picked);
45
+
46
+ await page.cancelPickLocator();
47
+ });
48
+ ```
49
+
50
+ **JavaScript**
51
+ ```javascript
52
+ const { test } = require('@playwright/test');
53
+
54
+ test('pick a locator during debugging', async ({ page }) => {
55
+ await page.goto('/checkout');
56
+
57
+ const picked = await page.pickLocator();
58
+ console.log(picked);
59
+
60
+ await page.cancelPickLocator();
61
+ });
62
+ ```
63
+
64
+ Review the suggested locator before keeping it. Prefer rewriting it to `getByRole()` or another semantic locator if that makes the test clearer.
65
+
66
+ ### `locator.normalize()` For Refactors
67
+
68
+ **Use when**: You inherit brittle CSS/XPath-heavy locators and want Playwright to suggest a more idiomatic form during cleanup work.
69
+ **Avoid when**: You have not yet thought through the element's semantics. A normalized locator can still be less clear than a hand-written role or label locator.
70
+
71
+ **TypeScript**
72
+ ```typescript
73
+ import { test } from '@playwright/test';
74
+
75
+ test('normalize a legacy locator', async ({ page }) => {
76
+ await page.goto('/settings');
77
+
78
+ const legacy = page.locator('.settings-panel button.save');
79
+ const normalized = await legacy.normalize();
80
+
81
+ console.log(normalized);
82
+ });
83
+ ```
84
+
85
+ **JavaScript**
86
+ ```javascript
87
+ const { test } = require('@playwright/test');
88
+
89
+ test('normalize a legacy locator', async ({ page }) => {
90
+ await page.goto('/settings');
91
+
92
+ const legacy = page.locator('.settings-panel button.save');
93
+ const normalized = await legacy.normalize();
94
+
95
+ console.log(normalized);
96
+ });
97
+ ```
98
+
99
+ Use `normalize()` as a refactoring assistant. The final test should still reflect user intent, not just "whatever selector Playwright could derive."
100
+
101
+ ## Patterns
102
+
103
+ ### Role-Based Locators (Default Choice)
104
+
105
+ **Use when**: Always. This is your starting point for every element.
106
+ **Avoid when**: The element has no ARIA role and adding one is outside your control.
107
+
108
+ Role-based locators mirror how assistive technology sees your page. They survive refactors, class renames, and component library swaps.
109
+
110
+ **TypeScript**
111
+ ```typescript
112
+ import { test, expect } from '@playwright/test';
113
+
114
+ test('role-based locators cover most UI elements', async ({ page }) => {
115
+ await page.goto('/dashboard');
116
+
117
+ // Buttons — matches <button>, <input type="submit">, role="button"
118
+ await page.getByRole('button', { name: 'Save changes' }).click();
119
+
120
+ // Links — matches <a href>
121
+ await page.getByRole('link', { name: 'View profile' }).click();
122
+
123
+ // Headings — use level to target specific h1-h6
124
+ await expect(page.getByRole('heading', { name: 'Dashboard', level: 1 })).toBeVisible();
125
+
126
+ // Text inputs — match by accessible name (label association)
127
+ await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
128
+
129
+ // Checkboxes and radios
130
+ await page.getByRole('checkbox', { name: 'Remember me' }).check();
131
+ await page.getByRole('radio', { name: 'Monthly billing' }).click();
132
+
133
+ // Dropdowns — <select> elements
134
+ await page.getByRole('combobox', { name: 'Country' }).selectOption('US');
135
+
136
+ // Navigation landmarks
137
+ const nav = page.getByRole('navigation', { name: 'Main' });
138
+ await expect(nav.getByRole('link', { name: 'Settings' })).toBeVisible();
139
+
140
+ // Tables
141
+ const table = page.getByRole('table', { name: 'Recent orders' });
142
+ await expect(table.getByRole('row')).toHaveCount(5);
143
+
144
+ // Dialogs
145
+ const dialog = page.getByRole('dialog', { name: 'Confirm deletion' });
146
+ await dialog.getByRole('button', { name: 'Delete' }).click();
147
+
148
+ // Exact matching — prevents "Log" from matching "Log out"
149
+ await page.getByRole('button', { name: 'Log', exact: true }).click();
150
+ });
151
+ ```
152
+
153
+ **JavaScript**
154
+ ```javascript
155
+ const { test, expect } = require('@playwright/test');
156
+
157
+ test('role-based locators cover most UI elements', async ({ page }) => {
158
+ await page.goto('/dashboard');
159
+
160
+ await page.getByRole('button', { name: 'Save changes' }).click();
161
+ await page.getByRole('link', { name: 'View profile' }).click();
162
+ await expect(page.getByRole('heading', { name: 'Dashboard', level: 1 })).toBeVisible();
163
+ await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
164
+ await page.getByRole('checkbox', { name: 'Remember me' }).check();
165
+ await page.getByRole('radio', { name: 'Monthly billing' }).click();
166
+ await page.getByRole('combobox', { name: 'Country' }).selectOption('US');
167
+
168
+ const nav = page.getByRole('navigation', { name: 'Main' });
169
+ await expect(nav.getByRole('link', { name: 'Settings' })).toBeVisible();
170
+
171
+ const dialog = page.getByRole('dialog', { name: 'Confirm deletion' });
172
+ await dialog.getByRole('button', { name: 'Delete' }).click();
173
+
174
+ await page.getByRole('button', { name: 'Log', exact: true }).click();
175
+ });
176
+ ```
177
+
178
+ #### Disambiguating by accessible description (`description`, Playwright 1.60+)
179
+
180
+ When two controls share the same accessible name but differ by their accessible *description* (`aria-describedby` / `aria-description`), Playwright 1.60's `description` option on `getByRole()` (and the other `getBy*` roles) targets the right one without falling back to CSS.
181
+
182
+ **TypeScript**
183
+ ```typescript
184
+ // Two "Delete" buttons, distinguished by their helper text / aria-describedby
185
+ await page
186
+ .getByRole('button', { name: 'Delete', description: 'Permanently removes this project' })
187
+ .click();
188
+
189
+ // Pair it with `exact` for tight matching
190
+ await page.getByRole('link', {
191
+ name: 'Download',
192
+ description: 'PDF, 2.3 MB',
193
+ exact: true,
194
+ }).click();
195
+ ```
196
+
197
+ **JavaScript**
198
+ ```javascript
199
+ await page
200
+ .getByRole('button', { name: 'Delete', description: 'Permanently removes this project' })
201
+ .click();
202
+ ```
203
+
204
+ Reach for `description` only when `name` + `role` (and `exact`) can't disambiguate — it's a precise tool, not a default.
205
+
206
+ ### Label-Based Locators
207
+
208
+ **Use when**: Targeting form fields that have associated `<label>` elements or `aria-label`.
209
+ **Avoid when**: The element is not a form control. Use `getByRole` with the accessible name instead — it covers labels too.
210
+
211
+ `getByLabel` is a shortcut for form fields. It matches `<label for="">`, wrapping `<label>`, and `aria-label` / `aria-labelledby`.
212
+
213
+ **TypeScript**
214
+ ```typescript
215
+ import { test, expect } from '@playwright/test';
216
+
217
+ test('fill a registration form using labels', async ({ page }) => {
218
+ await page.goto('/register');
219
+
220
+ await page.getByLabel('First name').fill('Jane');
221
+ await page.getByLabel('Last name').fill('Doe');
222
+ await page.getByLabel('Email address').fill('jane@example.com');
223
+ await page.getByLabel('Password', { exact: true }).fill('s3cure!Pass');
224
+ await page.getByLabel('Confirm password').fill('s3cure!Pass');
225
+ await page.getByLabel('I agree to the terms').check();
226
+
227
+ await page.getByRole('button', { name: 'Create account' }).click();
228
+ await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
229
+ });
230
+ ```
231
+
232
+ **JavaScript**
233
+ ```javascript
234
+ const { test, expect } = require('@playwright/test');
235
+
236
+ test('fill a registration form using labels', async ({ page }) => {
237
+ await page.goto('/register');
238
+
239
+ await page.getByLabel('First name').fill('Jane');
240
+ await page.getByLabel('Last name').fill('Doe');
241
+ await page.getByLabel('Email address').fill('jane@example.com');
242
+ await page.getByLabel('Password', { exact: true }).fill('s3cure!Pass');
243
+ await page.getByLabel('Confirm password').fill('s3cure!Pass');
244
+ await page.getByLabel('I agree to the terms').check();
245
+
246
+ await page.getByRole('button', { name: 'Create account' }).click();
247
+ await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
248
+ });
249
+ ```
250
+
251
+ ### Text-Based Locators
252
+
253
+ **Use when**: Targeting non-interactive content — status messages, paragraphs, banners, labels outside forms.
254
+ **Avoid when**: The element is a button, link, heading, or form field. Use `getByRole` instead.
255
+
256
+ **TypeScript**
257
+ ```typescript
258
+ import { test, expect } from '@playwright/test';
259
+
260
+ test('verify text content on the page', async ({ page }) => {
261
+ await page.goto('/order/confirmation');
262
+
263
+ // Substring match (default)
264
+ await expect(page.getByText('Order confirmed')).toBeVisible();
265
+
266
+ // Exact match — use when substring hits multiple elements
267
+ await expect(page.getByText('Order #12345', { exact: true })).toBeVisible();
268
+
269
+ // Regex match — for dynamic content patterns
270
+ await expect(page.getByText(/Order #\d+/)).toBeVisible();
271
+
272
+ // DO NOT use getByText for buttons or links:
273
+ // Bad: page.getByText('Submit')
274
+ // Good: page.getByRole('button', { name: 'Submit' })
275
+ });
276
+ ```
277
+
278
+ **JavaScript**
279
+ ```javascript
280
+ const { test, expect } = require('@playwright/test');
281
+
282
+ test('verify text content on the page', async ({ page }) => {
283
+ await page.goto('/order/confirmation');
284
+
285
+ await expect(page.getByText('Order confirmed')).toBeVisible();
286
+ await expect(page.getByText('Order #12345', { exact: true })).toBeVisible();
287
+ await expect(page.getByText(/Order #\d+/)).toBeVisible();
288
+ });
289
+ ```
290
+
291
+ ### Test ID Locators
292
+
293
+ **Use when**: No semantic locator works — the element has no accessible role, label, or stable text. Common with custom canvas-rendered components, complex data grids, or third-party widgets.
294
+ **Avoid when**: Any semantic locator (`getByRole`, `getByLabel`, `getByText`) can identify the element. Test IDs are invisible to users and assistive technology.
295
+
296
+ Configure the attribute name once in `playwright.config`:
297
+
298
+ **TypeScript**
299
+ ```typescript
300
+ // playwright.config.ts
301
+ import { defineConfig } from '@playwright/test';
302
+
303
+ export default defineConfig({
304
+ use: {
305
+ testIdAttribute: 'data-testid', // default; change to match your codebase
306
+ },
307
+ });
308
+ ```
309
+
310
+ ```typescript
311
+ import { test, expect } from '@playwright/test';
312
+
313
+ test('interact with a custom widget using test IDs', async ({ page }) => {
314
+ await page.goto('/analytics');
315
+
316
+ // Only when the chart component exposes no accessible roles
317
+ const chart = page.getByTestId('revenue-chart');
318
+ await expect(chart).toBeVisible();
319
+ await chart.click({ position: { x: 150, y: 75 } });
320
+
321
+ await expect(page.getByTestId('chart-tooltip')).toContainText('$12,400');
322
+ });
323
+ ```
324
+
325
+ **JavaScript**
326
+ ```javascript
327
+ // playwright.config.js
328
+ const { defineConfig } = require('@playwright/test');
329
+
330
+ module.exports = defineConfig({
331
+ use: {
332
+ testIdAttribute: 'data-testid',
333
+ },
334
+ });
335
+ ```
336
+
337
+ ```javascript
338
+ const { test, expect } = require('@playwright/test');
339
+
340
+ test('interact with a custom widget using test IDs', async ({ page }) => {
341
+ await page.goto('/analytics');
342
+
343
+ const chart = page.getByTestId('revenue-chart');
344
+ await expect(chart).toBeVisible();
345
+ await chart.click({ position: { x: 150, y: 75 } });
346
+
347
+ await expect(page.getByTestId('chart-tooltip')).toContainText('$12,400');
348
+ });
349
+ ```
350
+
351
+ ### CSS/XPath — Last Resort
352
+
353
+ **Use when**: You have zero control over the markup, no test IDs, no accessible names, and no way to add them. Legacy apps with generated class names and no semantic HTML.
354
+ **Avoid when**: Any other locator type works. Always.
355
+
356
+ **TypeScript**
357
+ ```typescript
358
+ import { test, expect } from '@playwright/test';
359
+
360
+ test('legacy app with no semantic markup', async ({ page }) => {
361
+ await page.goto('/legacy-admin');
362
+
363
+ // CSS — prefer short, structural selectors over fragile class chains
364
+ await page.locator('table.report-grid td:has-text("Overdue")').first().click();
365
+
366
+ // XPath — only when CSS cannot express the query (e.g., text + ancestor traversal)
367
+ await page.locator('xpath=//td[contains(text(),"Overdue")]/ancestor::tr//button').click();
368
+
369
+ // Combine CSS with Playwright pseudo-selectors for resilience
370
+ await page.locator('.sidebar >> role=button[name="Expand"]').click();
371
+ });
372
+ ```
373
+
374
+ **JavaScript**
375
+ ```javascript
376
+ const { test, expect } = require('@playwright/test');
377
+
378
+ test('legacy app with no semantic markup', async ({ page }) => {
379
+ await page.goto('/legacy-admin');
380
+
381
+ await page.locator('table.report-grid td:has-text("Overdue")').first().click();
382
+ await page.locator('xpath=//td[contains(text(),"Overdue")]/ancestor::tr//button').click();
383
+ await page.locator('.sidebar >> role=button[name="Expand"]').click();
384
+ });
385
+ ```
386
+
387
+ ### Locator Chaining and Filtering
388
+
389
+ **Use when**: A single locator matches multiple elements and you need to narrow down by context, content, or position.
390
+ **Avoid when**: A direct `getByRole` with `name` already uniquely identifies the element.
391
+
392
+ **TypeScript**
393
+ ```typescript
394
+ import { test, expect } from '@playwright/test';
395
+
396
+ test('chaining and filtering locators', async ({ page }) => {
397
+ await page.goto('/products');
398
+
399
+ // Chain: scope a locator within another
400
+ const productCard = page.getByRole('listitem').filter({ hasText: 'Running Shoes' });
401
+ await productCard.getByRole('button', { name: 'Add to cart' }).click();
402
+
403
+ // Filter by child locator — more precise than hasText
404
+ const row = page.getByRole('row').filter({
405
+ has: page.getByRole('cell', { name: 'Premium Plan' }),
406
+ });
407
+ await row.getByRole('button', { name: 'Upgrade' }).click();
408
+
409
+ // Filter with hasNot — exclude elements containing a child
410
+ const availableItems = page.getByRole('listitem').filter({
411
+ hasNot: page.getByText('Sold out'),
412
+ });
413
+ await expect(availableItems).toHaveCount(3);
414
+
415
+ // Filter with hasNotText — exclude by text content
416
+ const nonFeatured = page.getByRole('listitem').filter({
417
+ hasNotText: 'Featured',
418
+ });
419
+
420
+ // Positional: nth, first, last — use sparingly, only when order is stable
421
+ const thirdItem = page.getByRole('listitem').nth(2); // 0-indexed
422
+ const firstItem = page.getByRole('listitem').first();
423
+ const lastItem = page.getByRole('listitem').last();
424
+
425
+ // Combining multiple filters
426
+ const activeAdminRow = page
427
+ .getByRole('row')
428
+ .filter({ has: page.getByRole('cell', { name: 'Admin' }) })
429
+ .filter({ has: page.getByText('Active') });
430
+ await expect(activeAdminRow).toHaveCount(1);
431
+ });
432
+ ```
433
+
434
+ **JavaScript**
435
+ ```javascript
436
+ const { test, expect } = require('@playwright/test');
437
+
438
+ test('chaining and filtering locators', async ({ page }) => {
439
+ await page.goto('/products');
440
+
441
+ const productCard = page.getByRole('listitem').filter({ hasText: 'Running Shoes' });
442
+ await productCard.getByRole('button', { name: 'Add to cart' }).click();
443
+
444
+ const row = page.getByRole('row').filter({
445
+ has: page.getByRole('cell', { name: 'Premium Plan' }),
446
+ });
447
+ await row.getByRole('button', { name: 'Upgrade' }).click();
448
+
449
+ const availableItems = page.getByRole('listitem').filter({
450
+ hasNot: page.getByText('Sold out'),
451
+ });
452
+ await expect(availableItems).toHaveCount(3);
453
+
454
+ const thirdItem = page.getByRole('listitem').nth(2);
455
+ const firstItem = page.getByRole('listitem').first();
456
+ const lastItem = page.getByRole('listitem').last();
457
+
458
+ const activeAdminRow = page
459
+ .getByRole('row')
460
+ .filter({ has: page.getByRole('cell', { name: 'Admin' }) })
461
+ .filter({ has: page.getByText('Active') });
462
+ await expect(activeAdminRow).toHaveCount(1);
463
+ });
464
+ ```
465
+
466
+ ### Frame Locators
467
+
468
+ **Use when**: Interacting with content inside `<iframe>` or `<frame>` elements — payment widgets, embedded editors, third-party widgets.
469
+ **Avoid when**: The content is in the main frame or a Shadow DOM (use piercing instead).
470
+
471
+ **TypeScript**
472
+ ```typescript
473
+ import { test, expect } from '@playwright/test';
474
+
475
+ test('interact with content inside an iframe', async ({ page }) => {
476
+ await page.goto('/checkout');
477
+
478
+ // Locate the iframe, then use normal locators inside it
479
+ const paymentFrame = page.frameLocator('iframe[title="Payment"]');
480
+ await paymentFrame.getByLabel('Card number').fill('4242424242424242');
481
+ await paymentFrame.getByLabel('Expiration').fill('12/28');
482
+ await paymentFrame.getByLabel('CVC').fill('123');
483
+ await paymentFrame.getByRole('button', { name: 'Pay' }).click();
484
+
485
+ // Nested iframes — chain frameLocator calls
486
+ const nestedFrame = page
487
+ .frameLocator('#outer-frame')
488
+ .frameLocator('#inner-frame');
489
+ await expect(nestedFrame.getByText('Payment confirmed')).toBeVisible();
490
+
491
+ // Frame by nth index — when no better selector exists
492
+ const secondFrame = page.frameLocator('iframe').nth(1);
493
+ await expect(secondFrame.getByRole('heading')).toBeVisible();
494
+ });
495
+ ```
496
+
497
+ **JavaScript**
498
+ ```javascript
499
+ const { test, expect } = require('@playwright/test');
500
+
501
+ test('interact with content inside an iframe', async ({ page }) => {
502
+ await page.goto('/checkout');
503
+
504
+ const paymentFrame = page.frameLocator('iframe[title="Payment"]');
505
+ await paymentFrame.getByLabel('Card number').fill('4242424242424242');
506
+ await paymentFrame.getByLabel('Expiration').fill('12/28');
507
+ await paymentFrame.getByLabel('CVC').fill('123');
508
+ await paymentFrame.getByRole('button', { name: 'Pay' }).click();
509
+
510
+ const nestedFrame = page
511
+ .frameLocator('#outer-frame')
512
+ .frameLocator('#inner-frame');
513
+ await expect(nestedFrame.getByText('Payment confirmed')).toBeVisible();
514
+
515
+ const secondFrame = page.frameLocator('iframe').nth(1);
516
+ await expect(secondFrame.getByRole('heading')).toBeVisible();
517
+ });
518
+ ```
519
+
520
+ ### Shadow DOM Piercing
521
+
522
+ **Use when**: Targeting elements inside web components that use Shadow DOM (custom elements, design system components, Salesforce Lightning).
523
+ **Avoid when**: The element is in a regular DOM or iframe.
524
+
525
+ Playwright pierces open Shadow DOM automatically with `locator()`. The `getByRole` / `getByText` family also pierces shadow roots by default.
526
+
527
+ **TypeScript**
528
+ ```typescript
529
+ import { test, expect } from '@playwright/test';
530
+
531
+ test('interact with Shadow DOM elements', async ({ page }) => {
532
+ await page.goto('/design-system-demo');
533
+
534
+ // getByRole automatically pierces open Shadow DOM — just use it normally
535
+ await page.getByRole('button', { name: 'Toggle menu' }).click();
536
+
537
+ // locator() with CSS also pierces shadow roots by default
538
+ await page.locator('my-dropdown').getByRole('option', { name: 'Settings' }).click();
539
+
540
+ // Chain into nested shadow DOMs
541
+ await page
542
+ .locator('my-app')
543
+ .locator('my-sidebar')
544
+ .getByRole('link', { name: 'Profile' })
545
+ .click();
546
+
547
+ // If you explicitly need non-piercing behavior (rare), use css=light/
548
+ // await page.locator('css:light=.outer-only').click();
549
+ });
550
+ ```
551
+
552
+ **JavaScript**
553
+ ```javascript
554
+ const { test, expect } = require('@playwright/test');
555
+
556
+ test('interact with Shadow DOM elements', async ({ page }) => {
557
+ await page.goto('/design-system-demo');
558
+
559
+ await page.getByRole('button', { name: 'Toggle menu' }).click();
560
+ await page.locator('my-dropdown').getByRole('option', { name: 'Settings' }).click();
561
+
562
+ await page
563
+ .locator('my-app')
564
+ .locator('my-sidebar')
565
+ .getByRole('link', { name: 'Profile' })
566
+ .click();
567
+ });
568
+ ```
569
+
570
+ ### Dynamic Content — Waiting for Elements
571
+
572
+ **Use when**: Elements appear after API calls, animations, lazy loading, or route transitions.
573
+ **Avoid when**: The element is already on the page. Playwright auto-waits on actions, so explicit waits are rarely needed.
574
+
575
+ Never use `page.waitForTimeout()`. Use auto-waiting assertions or explicit event waits.
576
+
577
+ **TypeScript**
578
+ ```typescript
579
+ import { test, expect } from '@playwright/test';
580
+
581
+ test('handle dynamic content without manual waits', async ({ page }) => {
582
+ await page.goto('/search');
583
+
584
+ // Auto-waiting: actions like click(), fill() wait for the element automatically
585
+ await page.getByRole('textbox', { name: 'Search' }).fill('playwright');
586
+ await page.getByRole('button', { name: 'Search' }).click();
587
+
588
+ // Web-first assertion: auto-retries until timeout (default 5s)
589
+ await expect(page.getByRole('listitem')).toHaveCount(10);
590
+
591
+ // Wait for a specific element to appear after an async operation
592
+ await expect(page.getByRole('heading', { name: 'Results' })).toBeVisible();
593
+
594
+ // Wait for loading indicators to disappear
595
+ await expect(page.getByRole('progressbar')).toBeHidden();
596
+
597
+ // Wait for network-dependent content: wait for response then assert
598
+ const responsePromise = page.waitForResponse('**/api/search*');
599
+ await page.getByRole('button', { name: 'Load more' }).click();
600
+ await responsePromise;
601
+ await expect(page.getByRole('listitem')).toHaveCount(20);
602
+
603
+ // Wait for URL change after navigation
604
+ await page.getByRole('link', { name: 'First result' }).click();
605
+ await page.waitForURL('**/results/**');
606
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
607
+ });
608
+ ```
609
+
610
+ **JavaScript**
611
+ ```javascript
612
+ const { test, expect } = require('@playwright/test');
613
+
614
+ test('handle dynamic content without manual waits', async ({ page }) => {
615
+ await page.goto('/search');
616
+
617
+ await page.getByRole('textbox', { name: 'Search' }).fill('playwright');
618
+ await page.getByRole('button', { name: 'Search' }).click();
619
+
620
+ await expect(page.getByRole('listitem')).toHaveCount(10);
621
+ await expect(page.getByRole('heading', { name: 'Results' })).toBeVisible();
622
+ await expect(page.getByRole('progressbar')).toBeHidden();
623
+
624
+ const responsePromise = page.waitForResponse('**/api/search*');
625
+ await page.getByRole('button', { name: 'Load more' }).click();
626
+ await responsePromise;
627
+ await expect(page.getByRole('listitem')).toHaveCount(20);
628
+
629
+ await page.getByRole('link', { name: 'First result' }).click();
630
+ await page.waitForURL('**/results/**');
631
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
632
+ });
633
+ ```
634
+
635
+ ## Decision Guide
636
+
637
+ | Element Type | Recommended Locator | Example | Why |
638
+ |---|---|---|---|
639
+ | Button | `getByRole('button', { name })` | `getByRole('button', { name: 'Submit' })` | Matches `<button>`, `<input type="submit">`, `role="button"` |
640
+ | Link | `getByRole('link', { name })` | `getByRole('link', { name: 'Home' })` | Matches any `<a href>` regardless of styling |
641
+ | Text input | `getByRole('textbox', { name })` | `getByRole('textbox', { name: 'Email' })` | Matches by accessible name (label) |
642
+ | Password input | `getByLabel()` | `getByLabel('Password')` | Password fields have no distinct role; label is the best match |
643
+ | Checkbox | `getByRole('checkbox', { name })` | `getByRole('checkbox', { name: 'Agree' })` | Also use `.check()` / `.uncheck()` instead of `.click()` |
644
+ | Radio button | `getByRole('radio', { name })` | `getByRole('radio', { name: 'Express' })` | Group radios with `getByRole('radiogroup')` |
645
+ | Select/dropdown | `getByRole('combobox', { name })` | `getByRole('combobox', { name: 'Country' })` | Native `<select>` maps to combobox role |
646
+ | Custom dropdown | `getByRole('listbox')` + `getByRole('option')` | Click trigger, then `getByRole('option', { name })` | ARIA listbox pattern for custom dropdowns |
647
+ | Heading | `getByRole('heading', { name, level })` | `getByRole('heading', { name: 'Dashboard', level: 2 })` | Use `level` to distinguish h1-h6 |
648
+ | Table | `getByRole('table', { name })` | `getByRole('table', { name: 'Users' })` | Chain with `getByRole('row')`, `getByRole('cell')` |
649
+ | Table row | `getByRole('row').filter({ has })` | `.filter({ has: getByRole('cell', { name: 'Jane' }) })` | Filter rows by cell content |
650
+ | Navigation | `getByRole('navigation', { name })` | `getByRole('navigation', { name: 'Main' })` | Matches `<nav>` with `aria-label` |
651
+ | Dialog/modal | `getByRole('dialog', { name })` | `getByRole('dialog', { name: 'Confirm' })` | Scope all dialog interactions under this |
652
+ | Tab | `getByRole('tab', { name })` | `getByRole('tab', { name: 'Settings' })` | Use with `getByRole('tabpanel')` |
653
+ | Image | `getByAltText()` | `getByAltText('User avatar')` | Matches the `alt` attribute |
654
+ | Form field (any) | `getByLabel()` | `getByLabel('Date of birth')` | Fallback when role is ambiguous (date pickers, custom inputs) |
655
+ | Static text | `getByText()` | `getByText('No results found')` | Non-interactive content only |
656
+ | No semantic markup | `getByTestId()` | `getByTestId('sparkline-chart')` | Last semantic option before CSS |
657
+ | Iframe content | `frameLocator()` then any locator | `frameLocator('#payment').getByLabel('Card')` | Required for cross-frame access |
658
+ | Shadow DOM | `getByRole()` / `locator()` | Works automatically | Playwright pierces open shadow roots by default |
659
+
660
+ ## Anti-Patterns
661
+
662
+ | Don't Do This | Problem | Do This Instead |
663
+ |---|---|---|
664
+ | `page.locator('.btn-primary')` | Breaks when CSS classes change (renames, CSS modules, Tailwind) | `page.getByRole('button', { name: 'Save' })` |
665
+ | `page.locator('#submit-btn')` | IDs are implementation details; often auto-generated | `page.getByRole('button', { name: 'Submit' })` |
666
+ | `page.locator('div > span:nth-child(3)')` | Breaks on any DOM restructure | `page.getByText('Expected content')` or `getByTestId()` |
667
+ | `page.locator('xpath=//div[@class="form"]//input[2]')` | Fragile, unreadable, position-dependent | `page.getByLabel('Last name')` |
668
+ | `page.getByText('Submit')` for a button | Text locators don't assert the element is interactive | `page.getByRole('button', { name: 'Submit' })` |
669
+ | `page.locator('.item').nth(0)` on dynamic lists | Index changes when items are added/removed/reordered | `.filter({ hasText: 'Specific item' })` |
670
+ | `page.getByText('Aceptar')` hardcoded i18n text | Fails when locale changes | `page.getByRole('button', { name: /accept/i })` or `getByTestId('confirm-btn')` |
671
+ | `await page.waitForTimeout(3000)` | Arbitrary delay; too slow in fast environments, too short in slow ones | `await expect(locator).toBeVisible()` |
672
+ | `page.locator('.card').locator('.card-title').locator('a')` | Deep CSS chaining breaks on any structural change | `page.getByRole('link', { name: 'Card title text' })` |
673
+ | `page.$('selector')` (ElementHandle API) | Returns a snapshot, not auto-waiting; deprecated pattern | `page.locator('selector')` — locators are lazy and auto-wait |
674
+ | `page.locator('text=Click here')` | Legacy text selector syntax | `page.getByText('Click here')` or `getByRole` with name |
675
+ | Multiple locators for one element in sequence | Each `locator()` call in a chain restarts the search | Store as variable: `const btn = page.getByRole('button', { name: 'Save' })` |
676
+
677
+ ## Troubleshooting
678
+
679
+ ### "strict mode violation" — locator matches multiple elements
680
+
681
+ **Cause**: Your locator is not specific enough and Playwright refuses to pick one for you.
682
+
683
+ ```typescript
684
+ // Error: locator.click: strict mode violation, getByRole('button') resolved to 5 elements
685
+ await page.getByRole('button').click(); // Too broad
686
+
687
+ // Fix 1: Add a name filter
688
+ await page.getByRole('button', { name: 'Save' }).click();
689
+
690
+ // Fix 2: Scope within a parent
691
+ await page.getByRole('dialog').getByRole('button', { name: 'Save' }).click();
692
+
693
+ // Fix 3: Use exact matching when names are substrings of each other
694
+ await page.getByRole('button', { name: 'Save', exact: true }).click();
695
+
696
+ // Fix 4: Use filter to narrow down
697
+ await page.getByRole('button').filter({ hasText: 'Save draft' }).click();
698
+
699
+ // Debug: see what matched
700
+ console.log(await page.getByRole('button').all()); // list all matches
701
+ ```
702
+
703
+ ### Element exists but locator times out
704
+
705
+ **Cause**: The element is inside an iframe, Shadow DOM, or is obscured/hidden.
706
+
707
+ ```typescript
708
+ // Check if element is inside an iframe
709
+ const frame = page.frameLocator('iframe');
710
+ await frame.getByRole('button', { name: 'Submit' }).click();
711
+
712
+ // Check if element is in a Shadow DOM — getByRole pierces automatically,
713
+ // but page.$() and CSS selectors may not. Switch to getByRole.
714
+
715
+ // Check if element is hidden (display: none, visibility: hidden, opacity: 0)
716
+ // Use toBeVisible() to confirm, or toBeAttached() if hidden is expected
717
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeAttached();
718
+ ```
719
+
720
+ ### `getByRole` doesn't find the element
721
+
722
+ **Cause**: The element's implicit ARIA role doesn't match what you expect, or it has no role.
723
+
724
+ ```typescript
725
+ // Debug: inspect the accessibility tree
726
+ const snapshot = await page.accessibility.snapshot();
727
+ console.log(JSON.stringify(snapshot, null, 2));
728
+
729
+ // Common mismatches:
730
+ // - <div onclick="..."> has no button role → add role="button" or use <button>
731
+ // - <input type="text"> with no label has no accessible name → add <label> or aria-label
732
+ // - <a> without href has no link role → add href or role="link"
733
+
734
+ // Fallback chain: try getByLabel → getByText → getByTestId → locator()
735
+ ```
736
+
737
+ ## Related
738
+
739
+ - [core/locator-strategy.md](locator-strategy.md) — deep-dive decision framework for choosing locator strategies at the project level
740
+ - [core/assertions-and-waiting.md](assertions-and-waiting.md) — pair locators with web-first assertions
741
+ - [pom/page-object-model.md](../pom/page-object-model.md) — encapsulate locators in page objects
742
+ - [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) — advanced iframe and Shadow DOM patterns
743
+ - [core/i18n-and-localization.md](i18n-and-localization.md) — locator strategies for internationalized apps