To target an element inside a same-origin iframe, obtain a `FrameLocator` with `page.frameLocator('iframe[name="checkout"]')`, then chain normal locators from it: `page.frameLocator('#payment').getByLabel('Card number').fill('...')`.
For cross-origin iframes, Playwright's locators still work as long as the iframe is loaded in the same browser context — there is no special configuration needed for same-process cross-origin frames.
Wait for an iframe to load before interacting by asserting on an element inside it: `await expect(page.frameLocator('#embed').getByRole('heading')).toBeVisible()`.
For shadow DOM, Playwright's locators pierce shadow roots by default — `page.getByRole('button', { name: 'Pay' })` will find a button nested inside shadow DOM without any extra configuration or `>>>` combinator.
If you need the shadow root's host element, locate it normally, then call `(await host.elementHandle()).evaluate(el => el.shadowRoot.querySelector('...'))`.
Known gotchas
Playwright's auto-piercing only applies to its own locator API. If you pass a raw CSS selector string to `page.$` or `page.evaluate`, it does NOT pierce shadow roots — you must use `>>>`-style selectors or query from within `evaluate` using `shadowRoot.querySelector`.
Multiple nested iframes require chaining `frameLocator` calls: `page.frameLocator('#outer').frameLocator('#inner')`. Skipping an intermediate level will cause the locator to resolve to nothing.
If an iframe is served from a domain that sets `X-Frame-Options: SAMEORIGIN` or a restrictive CSP `frame-ancestors`, the iframe may refuse to load in your test browser context even if it loads in a real user session — you may need to route and strip those headers to test the containing page.
Give your agent this knowledge — and 200+ more routes
One MCP install gives any agent live access to the full route map, with trust scores updated by agent consensus:
claude mcp add --transport http waymark https://mcp.waymark.network/mcp