@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,488 @@
|
|
|
1
|
+
# Iframes and Shadow DOM
|
|
2
|
+
|
|
3
|
+
> **When to use**: When your application embeds content in `<iframe>` elements (payment widgets, third-party embeds, legacy modules) or uses Web Components with Shadow DOM (design systems, custom elements, Salesforce Lightning).
|
|
4
|
+
> **Prerequisites**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Iframes — use frameLocator to reach inside
|
|
10
|
+
const frame = page.frameLocator('iframe[title="Payment"]');
|
|
11
|
+
await frame.getByLabel('Card number').fill('4242424242424242');
|
|
12
|
+
|
|
13
|
+
// Nested iframes — chain frameLocator calls
|
|
14
|
+
const inner = page.frameLocator('#outer').frameLocator('#inner');
|
|
15
|
+
await inner.getByRole('button', { name: 'Submit' }).click();
|
|
16
|
+
|
|
17
|
+
// Shadow DOM — Playwright pierces open shadow roots automatically
|
|
18
|
+
await page.getByRole('button', { name: 'Toggle' }).click(); // auto-pierces
|
|
19
|
+
await page.locator('my-component').getByText('Hello').click(); // auto-pierces
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Patterns
|
|
23
|
+
|
|
24
|
+
### Basic iframe Interaction with `frameLocator()`
|
|
25
|
+
|
|
26
|
+
**Use when**: You need to interact with content inside an `<iframe>` -- payment forms, embedded editors, captchas, third-party widgets.
|
|
27
|
+
**Avoid when**: The content is in the main frame. Never use `frameLocator` for Shadow DOM.
|
|
28
|
+
|
|
29
|
+
`frameLocator()` returns a locator-like object scoped to the iframe's document. All standard locator methods work inside it.
|
|
30
|
+
|
|
31
|
+
**TypeScript**
|
|
32
|
+
```typescript
|
|
33
|
+
import { test, expect } from '@playwright/test';
|
|
34
|
+
|
|
35
|
+
test('complete payment inside Stripe iframe', async ({ page }) => {
|
|
36
|
+
await page.goto('/checkout');
|
|
37
|
+
|
|
38
|
+
// Locate the iframe by its title, name, or a CSS selector
|
|
39
|
+
const paymentFrame = page.frameLocator('iframe[title="Secure payment"]');
|
|
40
|
+
|
|
41
|
+
// Use normal locators inside the frame
|
|
42
|
+
await paymentFrame.getByLabel('Card number').fill('4242424242424242');
|
|
43
|
+
await paymentFrame.getByLabel('Expiry').fill('12/28');
|
|
44
|
+
await paymentFrame.getByLabel('CVC').fill('123');
|
|
45
|
+
await paymentFrame.getByRole('button', { name: 'Pay' }).click();
|
|
46
|
+
|
|
47
|
+
// Assertion on content inside the iframe
|
|
48
|
+
await expect(paymentFrame.getByText('Payment successful')).toBeVisible();
|
|
49
|
+
|
|
50
|
+
// Assertion on the parent page (outside the iframe)
|
|
51
|
+
await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**JavaScript**
|
|
56
|
+
```javascript
|
|
57
|
+
const { test, expect } = require('@playwright/test');
|
|
58
|
+
|
|
59
|
+
test('complete payment inside Stripe iframe', async ({ page }) => {
|
|
60
|
+
await page.goto('/checkout');
|
|
61
|
+
|
|
62
|
+
const paymentFrame = page.frameLocator('iframe[title="Secure payment"]');
|
|
63
|
+
await paymentFrame.getByLabel('Card number').fill('4242424242424242');
|
|
64
|
+
await paymentFrame.getByLabel('Expiry').fill('12/28');
|
|
65
|
+
await paymentFrame.getByLabel('CVC').fill('123');
|
|
66
|
+
await paymentFrame.getByRole('button', { name: 'Pay' }).click();
|
|
67
|
+
|
|
68
|
+
await expect(paymentFrame.getByText('Payment successful')).toBeVisible();
|
|
69
|
+
await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Selecting the Right iframe
|
|
74
|
+
|
|
75
|
+
**Use when**: Multiple iframes exist on the page or the iframe has no obvious identifier.
|
|
76
|
+
**Avoid when**: There is only one iframe and a simple `page.frameLocator('iframe')` works.
|
|
77
|
+
|
|
78
|
+
**TypeScript**
|
|
79
|
+
```typescript
|
|
80
|
+
import { test, expect } from '@playwright/test';
|
|
81
|
+
|
|
82
|
+
test('interact with the correct iframe among many', async ({ page }) => {
|
|
83
|
+
await page.goto('/dashboard');
|
|
84
|
+
|
|
85
|
+
// By title attribute (best — accessible and stable)
|
|
86
|
+
const chatFrame = page.frameLocator('iframe[title="Live chat"]');
|
|
87
|
+
|
|
88
|
+
// By name attribute
|
|
89
|
+
const reportFrame = page.frameLocator('iframe[name="analytics-report"]');
|
|
90
|
+
|
|
91
|
+
// By src URL pattern
|
|
92
|
+
const adFrame = page.frameLocator('iframe[src*="ads.example.com"]');
|
|
93
|
+
|
|
94
|
+
// By index — when nothing else works (0-indexed)
|
|
95
|
+
const thirdFrame = page.frameLocator('iframe').nth(2);
|
|
96
|
+
|
|
97
|
+
// By parent container — scope to a section first
|
|
98
|
+
const sidebar = page.getByRole('complementary');
|
|
99
|
+
const sidebarFrame = sidebar.frameLocator('iframe');
|
|
100
|
+
|
|
101
|
+
await chatFrame.getByRole('textbox', { name: 'Message' }).fill('Help');
|
|
102
|
+
await expect(reportFrame.getByRole('heading')).toBeVisible();
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**JavaScript**
|
|
107
|
+
```javascript
|
|
108
|
+
const { test, expect } = require('@playwright/test');
|
|
109
|
+
|
|
110
|
+
test('interact with the correct iframe among many', async ({ page }) => {
|
|
111
|
+
await page.goto('/dashboard');
|
|
112
|
+
|
|
113
|
+
const chatFrame = page.frameLocator('iframe[title="Live chat"]');
|
|
114
|
+
const reportFrame = page.frameLocator('iframe[name="analytics-report"]');
|
|
115
|
+
const adFrame = page.frameLocator('iframe[src*="ads.example.com"]');
|
|
116
|
+
const thirdFrame = page.frameLocator('iframe').nth(2);
|
|
117
|
+
|
|
118
|
+
await chatFrame.getByRole('textbox', { name: 'Message' }).fill('Help');
|
|
119
|
+
await expect(reportFrame.getByRole('heading')).toBeVisible();
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Nested Iframes
|
|
124
|
+
|
|
125
|
+
**Use when**: An iframe contains another iframe (common in complex widget hierarchies, ad containers, or embedded third-party tools).
|
|
126
|
+
**Avoid when**: There is only one level of iframe nesting.
|
|
127
|
+
|
|
128
|
+
**TypeScript**
|
|
129
|
+
```typescript
|
|
130
|
+
import { test, expect } from '@playwright/test';
|
|
131
|
+
|
|
132
|
+
test('interact with deeply nested iframe content', async ({ page }) => {
|
|
133
|
+
await page.goto('/embed-page');
|
|
134
|
+
|
|
135
|
+
// Chain frameLocator calls for each level of nesting
|
|
136
|
+
const outerFrame = page.frameLocator('#widget-container');
|
|
137
|
+
const innerFrame = outerFrame.frameLocator('#payment-form');
|
|
138
|
+
|
|
139
|
+
await innerFrame.getByLabel('Amount').fill('99.99');
|
|
140
|
+
await innerFrame.getByRole('button', { name: 'Confirm' }).click();
|
|
141
|
+
|
|
142
|
+
// Three levels deep
|
|
143
|
+
const deepFrame = page
|
|
144
|
+
.frameLocator('#level-1')
|
|
145
|
+
.frameLocator('#level-2')
|
|
146
|
+
.frameLocator('#level-3');
|
|
147
|
+
await expect(deepFrame.getByText('Success')).toBeVisible();
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**JavaScript**
|
|
152
|
+
```javascript
|
|
153
|
+
const { test, expect } = require('@playwright/test');
|
|
154
|
+
|
|
155
|
+
test('interact with deeply nested iframe content', async ({ page }) => {
|
|
156
|
+
await page.goto('/embed-page');
|
|
157
|
+
|
|
158
|
+
const outerFrame = page.frameLocator('#widget-container');
|
|
159
|
+
const innerFrame = outerFrame.frameLocator('#payment-form');
|
|
160
|
+
|
|
161
|
+
await innerFrame.getByLabel('Amount').fill('99.99');
|
|
162
|
+
await innerFrame.getByRole('button', { name: 'Confirm' }).click();
|
|
163
|
+
|
|
164
|
+
const deepFrame = page
|
|
165
|
+
.frameLocator('#level-1')
|
|
166
|
+
.frameLocator('#level-2')
|
|
167
|
+
.frameLocator('#level-3');
|
|
168
|
+
await expect(deepFrame.getByText('Success')).toBeVisible();
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Cross-Origin Iframes
|
|
173
|
+
|
|
174
|
+
**Use when**: The iframe loads content from a different domain (payment providers, OAuth flows, third-party embeds).
|
|
175
|
+
**Avoid when**: The iframe is same-origin.
|
|
176
|
+
|
|
177
|
+
Playwright handles cross-origin iframes transparently. `frameLocator()` works regardless of origin. No special configuration is needed.
|
|
178
|
+
|
|
179
|
+
**TypeScript**
|
|
180
|
+
```typescript
|
|
181
|
+
import { test, expect } from '@playwright/test';
|
|
182
|
+
|
|
183
|
+
test('complete OAuth login in cross-origin iframe', async ({ page }) => {
|
|
184
|
+
await page.goto('/login');
|
|
185
|
+
await page.getByRole('button', { name: 'Sign in with Google' }).click();
|
|
186
|
+
|
|
187
|
+
// The OAuth provider renders in a cross-origin iframe or popup
|
|
188
|
+
// For iframes:
|
|
189
|
+
const oauthFrame = page.frameLocator('iframe[src*="accounts.google.com"]');
|
|
190
|
+
await oauthFrame.getByLabel('Email').fill('user@gmail.com');
|
|
191
|
+
await oauthFrame.getByRole('button', { name: 'Next' }).click();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('cross-origin payment widget', async ({ page }) => {
|
|
195
|
+
await page.goto('/checkout');
|
|
196
|
+
|
|
197
|
+
// Stripe, PayPal, etc. load in cross-origin iframes
|
|
198
|
+
const stripeFrame = page.frameLocator('iframe[src*="js.stripe.com"]');
|
|
199
|
+
|
|
200
|
+
// All locator methods work across origins
|
|
201
|
+
await stripeFrame.getByLabel('Card number').fill('4242424242424242');
|
|
202
|
+
await stripeFrame.getByLabel('MM / YY').fill('12 / 28');
|
|
203
|
+
await stripeFrame.getByLabel('CVC').fill('123');
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**JavaScript**
|
|
208
|
+
```javascript
|
|
209
|
+
const { test, expect } = require('@playwright/test');
|
|
210
|
+
|
|
211
|
+
test('cross-origin payment widget', async ({ page }) => {
|
|
212
|
+
await page.goto('/checkout');
|
|
213
|
+
|
|
214
|
+
const stripeFrame = page.frameLocator('iframe[src*="js.stripe.com"]');
|
|
215
|
+
await stripeFrame.getByLabel('Card number').fill('4242424242424242');
|
|
216
|
+
await stripeFrame.getByLabel('MM / YY').fill('12 / 28');
|
|
217
|
+
await stripeFrame.getByLabel('CVC').fill('123');
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Using the Frame API for Advanced Scenarios
|
|
222
|
+
|
|
223
|
+
**Use when**: You need to access the frame's URL, wait for frame navigation, or run `evaluate` inside the frame.
|
|
224
|
+
**Avoid when**: `frameLocator()` covers your needs. It is simpler and auto-waits.
|
|
225
|
+
|
|
226
|
+
**TypeScript**
|
|
227
|
+
```typescript
|
|
228
|
+
import { test, expect } from '@playwright/test';
|
|
229
|
+
|
|
230
|
+
test('use Frame API for URL checks and evaluate', async ({ page }) => {
|
|
231
|
+
await page.goto('/dashboard');
|
|
232
|
+
|
|
233
|
+
// Get the Frame object (not FrameLocator)
|
|
234
|
+
const frame = page.frame({ url: /analytics\.example\.com/ });
|
|
235
|
+
expect(frame).not.toBeNull();
|
|
236
|
+
|
|
237
|
+
// Check the frame's URL
|
|
238
|
+
expect(frame!.url()).toContain('analytics.example.com');
|
|
239
|
+
|
|
240
|
+
// Run JavaScript inside the frame
|
|
241
|
+
const title = await frame!.evaluate(() => document.title);
|
|
242
|
+
expect(title).toBe('Analytics Dashboard');
|
|
243
|
+
|
|
244
|
+
// Wait for a frame to navigate
|
|
245
|
+
const frameNavPromise = page.waitForEvent('framenavigated', {
|
|
246
|
+
predicate: (f) => f.url().includes('/reports'),
|
|
247
|
+
});
|
|
248
|
+
await page.frameLocator('iframe[name="analytics"]')
|
|
249
|
+
.getByRole('link', { name: 'Reports' }).click();
|
|
250
|
+
await frameNavPromise;
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**JavaScript**
|
|
255
|
+
```javascript
|
|
256
|
+
const { test, expect } = require('@playwright/test');
|
|
257
|
+
|
|
258
|
+
test('use Frame API for URL checks and evaluate', async ({ page }) => {
|
|
259
|
+
await page.goto('/dashboard');
|
|
260
|
+
|
|
261
|
+
const frame = page.frame({ url: /analytics\.example\.com/ });
|
|
262
|
+
expect(frame).not.toBeNull();
|
|
263
|
+
|
|
264
|
+
expect(frame.url()).toContain('analytics.example.com');
|
|
265
|
+
|
|
266
|
+
const title = await frame.evaluate(() => document.title);
|
|
267
|
+
expect(title).toBe('Analytics Dashboard');
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Shadow DOM -- Automatic Piercing
|
|
272
|
+
|
|
273
|
+
**Use when**: Your app uses Web Components with open Shadow DOM. This is the default behavior -- no special configuration needed.
|
|
274
|
+
**Avoid when**: The shadow root is closed (see workaround below).
|
|
275
|
+
|
|
276
|
+
Playwright's `locator()`, `getByRole()`, `getByText()`, and all semantic locators pierce open Shadow DOM by default.
|
|
277
|
+
|
|
278
|
+
**TypeScript**
|
|
279
|
+
```typescript
|
|
280
|
+
import { test, expect } from '@playwright/test';
|
|
281
|
+
|
|
282
|
+
test('interact with web components using Shadow DOM', async ({ page }) => {
|
|
283
|
+
await page.goto('/design-system-demo');
|
|
284
|
+
|
|
285
|
+
// getByRole pierces shadow roots automatically
|
|
286
|
+
await page.getByRole('button', { name: 'Open menu' }).click();
|
|
287
|
+
|
|
288
|
+
// locator() with CSS also pierces
|
|
289
|
+
await page.locator('my-dropdown').getByRole('option', { name: 'Settings' }).click();
|
|
290
|
+
|
|
291
|
+
// Nested web components — each shadow root is pierced
|
|
292
|
+
await page
|
|
293
|
+
.locator('my-app')
|
|
294
|
+
.locator('my-sidebar')
|
|
295
|
+
.getByRole('link', { name: 'Dashboard' })
|
|
296
|
+
.click();
|
|
297
|
+
|
|
298
|
+
// Assertions pierce too
|
|
299
|
+
await expect(page.locator('my-card').getByText('Welcome back')).toBeVisible();
|
|
300
|
+
|
|
301
|
+
// getByTestId pierces shadow DOM
|
|
302
|
+
await expect(page.getByTestId('user-avatar')).toBeVisible();
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**JavaScript**
|
|
307
|
+
```javascript
|
|
308
|
+
const { test, expect } = require('@playwright/test');
|
|
309
|
+
|
|
310
|
+
test('interact with web components using Shadow DOM', async ({ page }) => {
|
|
311
|
+
await page.goto('/design-system-demo');
|
|
312
|
+
|
|
313
|
+
await page.getByRole('button', { name: 'Open menu' }).click();
|
|
314
|
+
await page.locator('my-dropdown').getByRole('option', { name: 'Settings' }).click();
|
|
315
|
+
|
|
316
|
+
await page
|
|
317
|
+
.locator('my-app')
|
|
318
|
+
.locator('my-sidebar')
|
|
319
|
+
.getByRole('link', { name: 'Dashboard' })
|
|
320
|
+
.click();
|
|
321
|
+
|
|
322
|
+
await expect(page.locator('my-card').getByText('Welcome back')).toBeVisible();
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Closed Shadow DOM Workaround
|
|
327
|
+
|
|
328
|
+
**Use when**: A third-party component uses `attachShadow({ mode: 'closed' })`, which blocks Playwright's auto-piercing.
|
|
329
|
+
**Avoid when**: The shadow root is open (the default). Auto-piercing handles open roots.
|
|
330
|
+
|
|
331
|
+
Override `attachShadow` before the page loads to force open mode.
|
|
332
|
+
|
|
333
|
+
**TypeScript**
|
|
334
|
+
```typescript
|
|
335
|
+
import { test, expect } from '@playwright/test';
|
|
336
|
+
|
|
337
|
+
test('access closed shadow DOM by forcing open mode', async ({ page }) => {
|
|
338
|
+
// Intercept attachShadow before the page scripts run
|
|
339
|
+
await page.addInitScript(() => {
|
|
340
|
+
const originalAttachShadow = Element.prototype.attachShadow;
|
|
341
|
+
Element.prototype.attachShadow = function (init: ShadowRootInit) {
|
|
342
|
+
return originalAttachShadow.call(this, { ...init, mode: 'open' });
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await page.goto('/third-party-widget');
|
|
347
|
+
|
|
348
|
+
// Now the previously closed shadow root is accessible
|
|
349
|
+
await page.locator('closed-component').getByRole('button', { name: 'Action' }).click();
|
|
350
|
+
await expect(page.locator('closed-component').getByText('Done')).toBeVisible();
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**JavaScript**
|
|
355
|
+
```javascript
|
|
356
|
+
const { test, expect } = require('@playwright/test');
|
|
357
|
+
|
|
358
|
+
test('access closed shadow DOM by forcing open mode', async ({ page }) => {
|
|
359
|
+
await page.addInitScript(() => {
|
|
360
|
+
const originalAttachShadow = Element.prototype.attachShadow;
|
|
361
|
+
Element.prototype.attachShadow = function (init) {
|
|
362
|
+
return originalAttachShadow.call(this, { ...init, mode: 'open' });
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await page.goto('/third-party-widget');
|
|
367
|
+
|
|
368
|
+
await page.locator('closed-component').getByRole('button', { name: 'Action' }).click();
|
|
369
|
+
await expect(page.locator('closed-component').getByText('Done')).toBeVisible();
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Web Components with Slots and Custom Events
|
|
374
|
+
|
|
375
|
+
**Use when**: Testing web components that use `<slot>` for content projection or dispatch custom events.
|
|
376
|
+
**Avoid when**: The component does not use slots or custom events.
|
|
377
|
+
|
|
378
|
+
**TypeScript**
|
|
379
|
+
```typescript
|
|
380
|
+
import { test, expect } from '@playwright/test';
|
|
381
|
+
|
|
382
|
+
test('slotted content is visible through web component', async ({ page }) => {
|
|
383
|
+
await page.goto('/components-demo');
|
|
384
|
+
|
|
385
|
+
// Content projected into a <slot> is in the light DOM (not shadow)
|
|
386
|
+
// Playwright sees it at its original location
|
|
387
|
+
const card = page.locator('my-card');
|
|
388
|
+
await expect(card.getByRole('heading', { name: 'Product Title' })).toBeVisible();
|
|
389
|
+
await expect(card.getByText('Product description here')).toBeVisible();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('listen for custom events from web components', async ({ page }) => {
|
|
393
|
+
await page.goto('/components-demo');
|
|
394
|
+
|
|
395
|
+
// Set up a listener for a custom event
|
|
396
|
+
const eventPromise = page.evaluate(() => {
|
|
397
|
+
return new Promise<{ detail: unknown }>((resolve) => {
|
|
398
|
+
document.querySelector('my-color-picker')!.addEventListener(
|
|
399
|
+
'color-change',
|
|
400
|
+
(e: Event) => resolve({ detail: (e as CustomEvent).detail }),
|
|
401
|
+
{ once: true }
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Trigger the event by interacting with the component
|
|
407
|
+
await page.locator('my-color-picker').getByRole('button', { name: 'Red' }).click();
|
|
408
|
+
|
|
409
|
+
const event = await eventPromise;
|
|
410
|
+
expect(event.detail).toEqual({ color: '#ff0000' });
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**JavaScript**
|
|
415
|
+
```javascript
|
|
416
|
+
const { test, expect } = require('@playwright/test');
|
|
417
|
+
|
|
418
|
+
test('slotted content is visible through web component', async ({ page }) => {
|
|
419
|
+
await page.goto('/components-demo');
|
|
420
|
+
|
|
421
|
+
const card = page.locator('my-card');
|
|
422
|
+
await expect(card.getByRole('heading', { name: 'Product Title' })).toBeVisible();
|
|
423
|
+
await expect(card.getByText('Product description here')).toBeVisible();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('listen for custom events from web components', async ({ page }) => {
|
|
427
|
+
await page.goto('/components-demo');
|
|
428
|
+
|
|
429
|
+
const eventPromise = page.evaluate(() => {
|
|
430
|
+
return new Promise((resolve) => {
|
|
431
|
+
document.querySelector('my-color-picker').addEventListener(
|
|
432
|
+
'color-change',
|
|
433
|
+
(e) => resolve({ detail: e.detail }),
|
|
434
|
+
{ once: true }
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await page.locator('my-color-picker').getByRole('button', { name: 'Red' }).click();
|
|
440
|
+
|
|
441
|
+
const event = await eventPromise;
|
|
442
|
+
expect(event.detail).toEqual({ color: '#ff0000' });
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Decision Guide
|
|
447
|
+
|
|
448
|
+
| Scenario | Approach | Why |
|
|
449
|
+
|---|---|---|
|
|
450
|
+
| Content inside `<iframe>` | `page.frameLocator('selector')` | Returns a scoped locator for the iframe document |
|
|
451
|
+
| Multiple iframes on page | Use `title`, `name`, or `src` attribute selectors | More stable than index-based `nth()` |
|
|
452
|
+
| Nested iframes | Chain `frameLocator().frameLocator()` | Each call scopes one level deeper |
|
|
453
|
+
| Cross-origin iframe | Same as any iframe -- `frameLocator()` | Playwright handles cross-origin transparently |
|
|
454
|
+
| URL check or `evaluate` inside frame | `page.frame({ url })` (Frame API) | FrameLocator does not expose URL or evaluate |
|
|
455
|
+
| Open Shadow DOM | Standard locators -- no changes needed | Playwright pierces open shadow roots by default |
|
|
456
|
+
| Closed Shadow DOM | `addInitScript` to override `attachShadow` | Forces closed roots to open before page loads |
|
|
457
|
+
| Slotted content in Web Components | Locate within the custom element tag | Slotted content is light DOM, accessible normally |
|
|
458
|
+
| Non-piercing CSS (rare) | `css:light=selector` | Explicitly restricts to light DOM only |
|
|
459
|
+
|
|
460
|
+
## Anti-Patterns
|
|
461
|
+
|
|
462
|
+
| Don't Do This | Problem | Do This Instead |
|
|
463
|
+
|---|---|---|
|
|
464
|
+
| `page.locator('#element-inside-iframe')` | Locators do not cross iframe boundaries | `page.frameLocator('iframe').locator('#element')` |
|
|
465
|
+
| `page.frameLocator('iframe').frameLocator('iframe')` without specific selectors | Matches wrong iframes when multiple exist | Use specific attributes: `frameLocator('iframe[title="..."]')` |
|
|
466
|
+
| `page.$('>>> .shadow-element')` | `>>>` piercing selector is not standard in Playwright | Use `page.locator('host-element').getByRole(...)` -- auto-piercing works |
|
|
467
|
+
| Using `contentFrame()` on a locator for routine interactions | More complex API than `frameLocator` for simple cases | Use `frameLocator()` -- simpler, auto-waits |
|
|
468
|
+
| `page.evaluate` to query inside shadow DOM | Bypasses Playwright's auto-waiting and retry logic | Use `page.locator()` which auto-pierces |
|
|
469
|
+
| Hardcoding iframe index (`nth(0)`) when attributes are available | Index changes when iframes are added/removed | Use `title`, `name`, or `src` pattern |
|
|
470
|
+
|
|
471
|
+
## Troubleshooting
|
|
472
|
+
|
|
473
|
+
| Symptom | Cause | Fix |
|
|
474
|
+
|---|---|---|
|
|
475
|
+
| Locator times out inside iframe | Using `page.locator()` instead of `frameLocator().locator()` | Switch to `page.frameLocator('selector').locator(...)` |
|
|
476
|
+
| `frameLocator` returns no elements | Iframe not yet loaded when locator resolves | `frameLocator` auto-waits; check that the iframe selector matches |
|
|
477
|
+
| Cross-origin iframe content inaccessible | Rare: specific browser security policy | Playwright handles cross-origin; ensure you are not using `page.frame()` with wrong URL |
|
|
478
|
+
| Shadow DOM element not found with `locator()` | Shadow root is closed (`mode: 'closed'`) | Use `addInitScript` to override `attachShadow` to force open mode |
|
|
479
|
+
| `getByRole` finds elements from wrong shadow root | Multiple web components have elements with the same role and name | Scope the locator: `page.locator('my-specific-component').getByRole(...)` |
|
|
480
|
+
| Slotted content not found | Searching inside shadow root instead of light DOM | Slotted content stays in light DOM; locate it through the parent custom element |
|
|
481
|
+
| `frame.evaluate()` returns null | Frame navigated away or was removed from DOM | Re-acquire the frame reference after navigation |
|
|
482
|
+
|
|
483
|
+
## Related
|
|
484
|
+
|
|
485
|
+
- [core/locators.md](locators.md) -- locator fundamentals including frame and shadow DOM basics
|
|
486
|
+
- [core/browser-apis.md](browser-apis.md) -- testing browser APIs that may live inside iframes
|
|
487
|
+
- [core/canvas-and-webgl.md](canvas-and-webgl.md) -- canvas elements inside iframes or web components
|
|
488
|
+
- [core/debugging.md](debugging.md) -- using Playwright Inspector to identify iframe boundaries and shadow roots
|