From 87188423b191f36517be30d33ac210f0439dc84e Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Fri, 6 Feb 2026 18:03:26 +0100 Subject: [PATCH] feat: reuse Playwright's internal CDP session instead of creating new WebSocket connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch from getCDPSessionForPage (creates a new WS via Target.attachToTarget) to getExistingCDPSessionForPage which reuses Playwright's internal CRSession. This is critical for the relay server where Target.attachToTarget is intercepted and cannot create real new sessions. Also add FrameLocator support in accessibilitySnapshot — FrameLocator (from locator.contentFrame()) is now auto-resolved to the real Frame object needed for OOPIF CDP session attachment. - New PlaywrightCDPSessionAdapter wrapping Playwright's CDPSession as ICDPSession - New getExistingCDPSessionForPage() using context.getExistingCDPSession() - resolveFrame() helper converts FrameLocator to Frame via elementHandle().contentFrame() - getAriaSnapshot() frame param now accepts Frame | FrameLocator - Executor CDP cache uses ICDPSession, borrowed sessions detach as no-op - Test for getExistingCDPSession through the relay - Updated framer iframe guide with alternative page.frames() example - Bump playwright submodule to @xmorse/playwright-core@1.59.2 - Fix styles-api.md import path --- docs/framer-iframe-snapshot-guide.md | 7 +++- playwright | 2 +- playwriter/src/aria-snapshot.ts | 51 ++++++++++++++++++++++++-- playwriter/src/cdp-session.ts | 49 ++++++++++++++++++++++++- playwriter/src/executor.ts | 21 ++++++----- playwriter/src/index.ts | 2 +- playwriter/src/snapshot-tools.test.ts | 39 +++++++++++++++++++- pnpm-lock.yaml | 35 +++++++++++++----- website/public/resources/styles-api.md | 2 +- 9 files changed, 179 insertions(+), 29 deletions(-) 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