diff --git a/docs/framer-iframe-snapshot-guide.md b/docs/framer-iframe-snapshot-guide.md index ede734b..da49a5b 100644 --- a/docs/framer-iframe-snapshot-guide.md +++ b/docs/framer-iframe-snapshot-guide.md @@ -45,11 +45,16 @@ playwriter -s 1 -e "const iframe = page.locator(\"iframe[src*='plugins.framercdn playwriter -s 1 -e "const iframe = page.locator(\"iframe[src*='plugins.framercdn.com']\"); console.log(await iframe.count());" ``` -- Run the accessibility snapshot on that iframe: +- Run the accessibility snapshot on that iframe using `contentFrame()` (FrameLocator is auto-resolved to Frame): ```bash playwriter -s 1 -e "const frame = await page.locator(\"iframe[src*='plugins.framercdn.com']\").contentFrame(); console.log(await accessibilitySnapshot({ page, frame }));" ``` +- Alternative: use `page.frames()` to get the Frame directly: +```bash +playwriter -s 1 -e "const frame = page.frames().find(f => f.url().includes('plugins.framercdn.com')); console.log(await accessibilitySnapshot({ page, frame }));" +``` + - Validate the snapshot contains MCP UI text (confirms the panel is actually loaded): ```bash playwriter -s 1 -e "const frame = await page.locator(\"iframe[src*='plugins.framercdn.com']\").contentFrame(); console.log(await accessibilitySnapshot({ page, frame, search: /Control Framer with MCP|Login With Google/ }));" diff --git a/playwright b/playwright index 07e8ba0..4e6ceab 160000 --- a/playwright +++ b/playwright @@ -1 +1 @@ -Subproject commit 07e8ba0b8b6cd0828061a4d4aceec1cb82456b8c +Subproject commit 4e6ceabff8d574e36094267c5f4779b0b841fd1c diff --git a/playwriter/src/aria-snapshot.ts b/playwriter/src/aria-snapshot.ts index d0f44cd..4022037 100644 --- a/playwriter/src/aria-snapshot.ts +++ b/playwriter/src/aria-snapshot.ts @@ -1,7 +1,7 @@ // Accessibility snapshot pipeline: build raw AX tree, filter to a // tree (interactive-only, labels/contexts, wrapper hoisting, ignored // indent preservation), then render lines and locators. -import type { Page, Locator, ElementHandle, Frame } from '@xmorse/playwright-core' +import type { Page, Locator, ElementHandle, Frame, FrameLocator } from '@xmorse/playwright-core' import crypto from 'node:crypto' import fs from 'node:fs' import path from 'node:path' @@ -723,6 +723,44 @@ function isSubstringOfAny(needle: string, haystack: Set): boolean { return false } +// ============================================================================ +// Frame Resolution +// ============================================================================ + +/** + * Resolve a FrameLocator to an actual Frame object. FrameLocator (returned by + * locator.contentFrame()) is a scoping helper that doesn't have CDP methods like + * frameId(). We need the real Frame from page.frames() for OOPIF session attachment. + * + * If a Frame is passed directly, it's returned as-is. + * If a FrameLocator is passed, we find the matching Frame by locating the iframe + * element and matching its src URL against page.frames(). + */ +async function resolveFrame({ frame, page }: { frame?: Frame | FrameLocator; page: Page }): Promise { + if (!frame) { + return undefined + } + // Frame has frameId(), FrameLocator does not + if (typeof (frame as Frame).frameId === 'function') { + return frame as Frame + } + // It's a FrameLocator — resolve to a Frame via the owner iframe element. + // Use elementHandle().contentFrame() which returns the actual Frame object + // directly from the