@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,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
|