@assertrx/dino 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. package/themes/dino.json +66 -0
@@ -0,0 +1,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