@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.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- 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)
|