For iframes, use page.frameLocator('iframe[name=payment]') to create a FrameLocator scoped to the iframe, then chain locators inside it: page.frameLocator('iframe').getByRole('textbox', { name: 'Card number' })
For nested iframes, chain frameLocator calls: page.frameLocator('iframe#outer').frameLocator('iframe#inner').getByLabel('CVV')
For open shadow DOM, Playwright locators pierce shadow boundaries automatically — getByRole and CSS selectors work through shadow roots without any special configuration
For closed shadow DOM (shadowRoot mode 'closed'), direct Playwright locator piercing does not work; you must use page.evaluate() to access the shadow root via JavaScript if the application exposes a reference
Verify the correct frame context is active by asserting on a unique element within the frame before interacting; cross-origin iframes may restrict content access
Known gotchas
frameLocator() returns a FrameLocator, not a Frame; locators chained from it remain lazy (not yet resolved) and still benefit from Playwright's auto-waiting — do not call .frame() expecting an immediate Frame object
Cross-origin iframes (a different domain than the parent) are subject to same-origin policy; Playwright can still interact with elements inside them, but JavaScript-level access via evaluate() is blocked by the browser
Closed shadow DOM (/mode: 'closed'/) is intentionally inaccessible from outside the component — if your automation target uses closed shadow roots, the component author must provide a testing hook; Playwright cannot pierce closed roots
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