fix iframe snapshots and document Framer MCP flow
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: Framer Plugin Iframe Snapshot Guide
|
||||||
|
description: Step-by-step instructions to open the Framer MCP plugin iframe and run accessibilitySnapshot on it.
|
||||||
|
prompt: |
|
||||||
|
Create a concise step-by-step guide to open the Framer plugin iframe and
|
||||||
|
verify accessibilitySnapshot on that iframe using playwriter CLI. Include the
|
||||||
|
exact Framer project URL and the plugins.framercdn iframe URL. Also document
|
||||||
|
the Command+K workflow to open the MCP plugin: press Command+K, search for MCP
|
||||||
|
in the command palette, press Enter, then wait ~1 second for the iframe to
|
||||||
|
appear. Reference any files read for context.
|
||||||
|
|
||||||
|
Sources:
|
||||||
|
- @/Users/morse/Documents/GitHub/playwriter/tmp/session-ses_437d9a10bffeFmy3k3hVcAFZnf.md
|
||||||
|
- @/Users/morse/Documents/GitHub/playwriter/tmp/session-ses_43807a563ffe1tCS0FWK79IpcV.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-step
|
||||||
|
|
||||||
|
- Open the Framer project URL in Chrome:
|
||||||
|
https://framer.com/projects/unframer-source--XOxwdyyCrFEE9uKnKFPq-6gX7n?node=augiA20Il
|
||||||
|
|
||||||
|
- Wait for the editor UI to finish loading (toolbar visible) before opening the command palette.
|
||||||
|
|
||||||
|
- Press Command+K to open the command palette.
|
||||||
|
|
||||||
|
- Verify the palette is open (search for the palette input in the snapshot output):
|
||||||
|
```bash
|
||||||
|
playwriter -s 1 -e "console.log(await accessibilitySnapshot({ page, search: /Command palette|Search commands|Type a command/ }));"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Search for **MCP**, press Enter, then wait about 1 second for the plugin iframe to appear.
|
||||||
|
|
||||||
|
- Verify the plugin iframe exists (should include `plugins.framercdn.com`):
|
||||||
|
```bash
|
||||||
|
playwriter -s 1 -e "const iframes = await page.locator('iframe').all(); for (const f of iframes) { console.log(await f.getAttribute('src')); }"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Wait until the MCP iframe is present (verifies the action worked):
|
||||||
|
```bash
|
||||||
|
playwriter -s 1 -e "const iframe = page.locator(\"iframe[src*='plugins.framercdn.com']\"); await iframe.first().waitFor({ timeout: 10000 }); console.log('iframe ready');"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Grab the iframe’s locator by URL:
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
```bash
|
||||||
|
playwriter -s 1 -e "const iframe = page.locator(\"iframe[src*='plugins.framercdn.com']\"); console.log(await accessibilitySnapshot({ page, iframe }));"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Validate the snapshot contains MCP UI text (confirms the panel is actually loaded):
|
||||||
|
```bash
|
||||||
|
playwriter -s 1 -e "const iframe = page.locator(\"iframe[src*='plugins.framercdn.com']\"); console.log(await accessibilitySnapshot({ page, iframe, search: /Control Framer with MCP|Login With Google/ }));"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected iframe URL
|
||||||
|
|
||||||
|
- https://nw12xtr7iedsczg1le9s9pqfl.plugins.framercdn.com/?mode=canvas
|
||||||
@@ -178,16 +178,9 @@ describe('aria-snapshot', () => {
|
|||||||
await page.locator('[data-testid="external-iframe"]').waitFor({ timeout: 5000 })
|
await page.locator('[data-testid="external-iframe"]').waitFor({ timeout: 5000 })
|
||||||
await page.frameLocator('[data-testid="external-iframe"]').locator('[data-testid="iframe-button"]').waitFor({ timeout: 5000 })
|
await page.frameLocator('[data-testid="external-iframe"]').locator('[data-testid="iframe-button"]').waitFor({ timeout: 5000 })
|
||||||
|
|
||||||
const frame = page.frames().find((candidate) => {
|
|
||||||
return candidate.url().includes('/iframe.html')
|
|
||||||
})
|
|
||||||
if (!frame) {
|
|
||||||
throw new Error('Iframe frame not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { snapshot } = await getAriaSnapshot({
|
const { snapshot } = await getAriaSnapshot({
|
||||||
page,
|
page,
|
||||||
frame,
|
iframe: page.locator('[data-testid="external-iframe"]'),
|
||||||
wsUrl: getCdpUrl({ port: TEST_PORT }),
|
wsUrl: getCdpUrl({ port: TEST_PORT }),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+139
-10
@@ -206,6 +206,105 @@ function toAttributeMap(attributes?: string[]): Map<string, string> {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAttributeValue(attributes: string[] | undefined, name: string): string | null {
|
||||||
|
if (!attributes) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const index = attributes.indexOf(name)
|
||||||
|
if (index === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return attributes[index + 1] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUrlForCompare(url: string): string {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url)
|
||||||
|
return `${parsed.origin}${parsed.pathname}`
|
||||||
|
} catch {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a locator -> iframe target session.
|
||||||
|
// CDP flow:
|
||||||
|
// - DOM.getDocument + DOM.querySelector + DOM.describeNode: find the iframe element and read its src + frameId.
|
||||||
|
// - Target.getTargets: list all CDP targets (including OOPIF iframe targets).
|
||||||
|
// - Target.attachToTarget: attach to the matching iframe target to obtain the child sessionId.
|
||||||
|
// If no iframe target is found, we fall back to the iframe element's frameId for same-process iframes.
|
||||||
|
async function resolveIframeTarget({
|
||||||
|
session,
|
||||||
|
iframe,
|
||||||
|
}: {
|
||||||
|
session: ICDPSession
|
||||||
|
iframe: Locator
|
||||||
|
}): Promise<{ sessionId: string | null; targetId: string | null; frameId: Protocol.Page.FrameId | null }> {
|
||||||
|
const attrName = 'data-pw-iframe-scope'
|
||||||
|
const attrValue = crypto.randomUUID()
|
||||||
|
|
||||||
|
await iframe.evaluate((element, data) => {
|
||||||
|
element.setAttribute(data.name, data.value)
|
||||||
|
}, { name: attrName, value: attrValue })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { root } = await session.send('DOM.getDocument') as Protocol.DOM.GetDocumentResponse
|
||||||
|
const { nodeId } = await session.send('DOM.querySelector', {
|
||||||
|
nodeId: root.nodeId,
|
||||||
|
selector: `[${attrName}="${attrValue}"]`,
|
||||||
|
}) as Protocol.DOM.QuerySelectorResponse
|
||||||
|
|
||||||
|
if (!nodeId) {
|
||||||
|
throw new Error('Iframe locator did not resolve to a DOM node')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { node } = await session.send('DOM.describeNode', { nodeId }) as Protocol.DOM.DescribeNodeResponse
|
||||||
|
const iframeSrc = getAttributeValue(node.attributes, 'src')
|
||||||
|
if (!iframeSrc) {
|
||||||
|
throw new Error('Iframe element has no src attribute')
|
||||||
|
}
|
||||||
|
const frameId = node.frameId ?? null
|
||||||
|
|
||||||
|
const { targetInfos } = await session.send('Target.getTargets') as Protocol.Target.GetTargetsResponse
|
||||||
|
const iframeTargets = targetInfos.filter((target) => target.type === 'iframe')
|
||||||
|
const normalizedSrc = normalizeUrlForCompare(iframeSrc)
|
||||||
|
|
||||||
|
const exactMatch = iframeTargets.find((target) => target.url === iframeSrc)
|
||||||
|
const normalizedMatch = iframeTargets.find((target) => normalizeUrlForCompare(target.url) === normalizedSrc)
|
||||||
|
const originMatch = (() => {
|
||||||
|
try {
|
||||||
|
const { origin } = new URL(iframeSrc)
|
||||||
|
return iframeTargets.find((target) => target.url.startsWith(origin))
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
const target = exactMatch || normalizedMatch || originMatch
|
||||||
|
if (!target) {
|
||||||
|
if (!frameId) {
|
||||||
|
throw new Error(`No iframe target found for src: ${iframeSrc}`)
|
||||||
|
}
|
||||||
|
return { sessionId: null, targetId: null, frameId }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sessionId } = await session.send('Target.attachToTarget', {
|
||||||
|
targetId: target.targetId,
|
||||||
|
flatten: true,
|
||||||
|
}) as Protocol.Target.AttachToTargetResponse
|
||||||
|
|
||||||
|
return { sessionId, targetId: target.targetId, frameId }
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await iframe.evaluate((element, data) => {
|
||||||
|
element.removeAttribute(data.name)
|
||||||
|
}, { name: attrName })
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to cleanup iframe scope marker', { cause: error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function getStableRefFromAttributes(attributes: Map<string, string>): { value: string; attr: string } | null {
|
function getStableRefFromAttributes(attributes: Map<string, string>): { value: string; attr: string } | null {
|
||||||
const id = attributes.get('id')
|
const id = attributes.get('id')
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -746,8 +845,9 @@ function isSubstringOfAny(needle: string, haystack: Set<string>): boolean {
|
|||||||
* await page.locator(selector).click()
|
* await page.locator(selector).click()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function getAriaSnapshot({ page, locator, refFilter, wsUrl, interactiveOnly = false, cdp }: {
|
export async function getAriaSnapshot({ page, iframe, locator, refFilter, wsUrl, interactiveOnly = false, cdp }: {
|
||||||
page: Page
|
page: Page
|
||||||
|
iframe?: Locator
|
||||||
locator?: Locator
|
locator?: Locator
|
||||||
refFilter?: (info: { role: string; name: string }) => boolean
|
refFilter?: (info: { role: string; name: string }) => boolean
|
||||||
wsUrl?: string
|
wsUrl?: string
|
||||||
@@ -755,26 +855,47 @@ export async function getAriaSnapshot({ page, locator, refFilter, wsUrl, interac
|
|||||||
cdp?: ICDPSession
|
cdp?: ICDPSession
|
||||||
}): Promise<AriaSnapshotResult> {
|
}): Promise<AriaSnapshotResult> {
|
||||||
const session = cdp || await getCDPSessionForPage({ page, wsUrl })
|
const session = cdp || await getCDPSessionForPage({ page, wsUrl })
|
||||||
await session.send('DOM.enable')
|
let iframeSessionId: string | null = null
|
||||||
await session.send('Accessibility.enable')
|
let iframeTargetId: string | null = null
|
||||||
|
let iframeFrameId: Protocol.Page.FrameId | null = null
|
||||||
|
if (iframe) {
|
||||||
|
const resolved = await resolveIframeTarget({ session, iframe })
|
||||||
|
iframeSessionId = resolved.sessionId
|
||||||
|
iframeTargetId = resolved.targetId
|
||||||
|
iframeFrameId = resolved.frameId
|
||||||
|
if (!iframeSessionId && !iframeFrameId) {
|
||||||
|
throw new Error('Failed to resolve iframe target or frameId')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iframeSessionId) {
|
||||||
|
try {
|
||||||
|
await session.send('Runtime.runIfWaitingForDebugger', undefined, iframeSessionId)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to resume iframe target session', { cause: error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await session.send('DOM.enable', undefined, iframeSessionId)
|
||||||
|
await session.send('Accessibility.enable', undefined, iframeSessionId)
|
||||||
const scopeAttr = 'data-pw-scope'
|
const scopeAttr = 'data-pw-scope'
|
||||||
const scopeValue = crypto.randomUUID()
|
const scopeValue = crypto.randomUUID()
|
||||||
let scopeApplied = false
|
let scopeApplied = false
|
||||||
|
const scopeLocator = locator
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (locator) {
|
if (scopeLocator) {
|
||||||
await locator.evaluate((element, data) => {
|
await scopeLocator.evaluate((element, data) => {
|
||||||
element.setAttribute(data.attr, data.value)
|
element.setAttribute(data.attr, data.value)
|
||||||
}, { attr: scopeAttr, value: scopeValue })
|
}, { attr: scopeAttr, value: scopeValue })
|
||||||
scopeApplied = true
|
scopeApplied = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nodes: domNodes } = await session.send('DOM.getFlattenedDocument', { depth: -1, pierce: true }) as Protocol.DOM.GetFlattenedDocumentResponse
|
const { nodes: domNodes } = await session.send('DOM.getFlattenedDocument', { depth: -1, pierce: true }, iframeSessionId) as Protocol.DOM.GetFlattenedDocumentResponse
|
||||||
const { domById, domByBackendId, childrenByParent } = buildDomIndex(domNodes)
|
const { domById, domByBackendId, childrenByParent } = buildDomIndex(domNodes)
|
||||||
|
|
||||||
let scopeRootNodeId: Protocol.DOM.NodeId | null = null
|
let scopeRootNodeId: Protocol.DOM.NodeId | null = null
|
||||||
let scopeRootBackendId: Protocol.DOM.BackendNodeId | null = null
|
let scopeRootBackendId: Protocol.DOM.BackendNodeId | null = null
|
||||||
if (locator) {
|
if (scopeLocator) {
|
||||||
scopeRootNodeId = findScopeRootNodeId(domNodes, scopeAttr, scopeValue)
|
scopeRootNodeId = findScopeRootNodeId(domNodes, scopeAttr, scopeValue)
|
||||||
if (scopeRootNodeId) {
|
if (scopeRootNodeId) {
|
||||||
const scopeNode = domById.get(scopeRootNodeId)
|
const scopeNode = domById.get(scopeRootNodeId)
|
||||||
@@ -788,7 +909,8 @@ export async function getAriaSnapshot({ page, locator, refFilter, wsUrl, interac
|
|||||||
? buildBackendIdSet(scopeRootNodeId, childrenByParent, domById)
|
? buildBackendIdSet(scopeRootNodeId, childrenByParent, domById)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const { nodes: axNodes } = await session.send('Accessibility.getFullAXTree') as Protocol.Accessibility.GetFullAXTreeResponse
|
const axParams = !iframeSessionId && iframeFrameId ? { frameId: iframeFrameId } : undefined
|
||||||
|
const { nodes: axNodes } = await session.send('Accessibility.getFullAXTree', axParams, iframeSessionId) as Protocol.Accessibility.GetFullAXTreeResponse
|
||||||
|
|
||||||
const axById = new Map<Protocol.Accessibility.AXNodeId, Protocol.Accessibility.AXNode>()
|
const axById = new Map<Protocol.Accessibility.AXNodeId, Protocol.Accessibility.AXNode>()
|
||||||
for (const node of axNodes) {
|
for (const node of axNodes) {
|
||||||
@@ -1021,11 +1143,18 @@ export async function getAriaSnapshot({ page, locator, refFilter, wsUrl, interac
|
|||||||
getRefStringForLocator: async (loc) => (await getRefsForLocators([loc]))[0]?.ref ?? null,
|
getRefStringForLocator: async (loc) => (await getRefsForLocators([loc]))[0]?.ref ?? null,
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (scopeApplied && locator) {
|
if (scopeApplied && scopeLocator) {
|
||||||
await locator.evaluate((element, attr) => {
|
await scopeLocator.evaluate((element, attr) => {
|
||||||
element.removeAttribute(attr)
|
element.removeAttribute(attr)
|
||||||
}, scopeAttr)
|
}, scopeAttr)
|
||||||
}
|
}
|
||||||
|
if (iframeSessionId) {
|
||||||
|
try {
|
||||||
|
await session.send('Target.detachFromTarget', { sessionId: iframeSessionId })
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to detach iframe target ${iframeTargetId ?? 'unknown'}`, { cause: error })
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!cdp) {
|
if (!cdp) {
|
||||||
await session.detach()
|
await session.detach()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1153,6 +1153,7 @@ export async function startPlayWriterCDPRelayServer({
|
|||||||
|
|
||||||
if (method === 'Target.attachedToTarget') {
|
if (method === 'Target.attachedToTarget') {
|
||||||
const targetParams = params as Protocol.Target.AttachedToTargetEvent
|
const targetParams = params as Protocol.Target.AttachedToTargetEvent
|
||||||
|
const incomingSessionId = sessionId
|
||||||
const iframeParentFrameId = targetParams.targetInfo.parentFrameId
|
const iframeParentFrameId = targetParams.targetInfo.parentFrameId
|
||||||
const iframeOwnerSessionId = targetParams.targetInfo.type === 'iframe' && iframeParentFrameId
|
const iframeOwnerSessionId = targetParams.targetInfo.type === 'iframe' && iframeParentFrameId
|
||||||
? getPageTargetForFrameId({ connection, frameId: iframeParentFrameId })?.sessionId
|
? getPageTargetForFrameId({ connection, frameId: iframeParentFrameId })?.sessionId
|
||||||
@@ -1200,7 +1201,13 @@ export async function startPlayWriterCDPRelayServer({
|
|||||||
if (!alreadyConnected) {
|
if (!alreadyConnected) {
|
||||||
sendToPlaywright({
|
sendToPlaywright({
|
||||||
message: {
|
message: {
|
||||||
sessionId: iframeOwnerSessionId,
|
// Iframe targets must be routed to the parent page sessionId so Playwright attaches them under the right page.
|
||||||
|
// - iframeOwnerSessionId: derived parent session via parentFrameId -> page sessionId (frameId tracking).
|
||||||
|
// - incomingSessionId: extension event sessionId for the parent tab.
|
||||||
|
// The frameId mapping is racy: Target.attachedToTarget can arrive before Page.frameAttached/Page.frameNavigated populate frameIds.
|
||||||
|
// When iframeOwnerSessionId is missing we must fall back to incomingSessionId, otherwise Playwright receives the attach on the root
|
||||||
|
// session, detaches it, and the iframe stays paused (waitingForDebugger) which can hang navigations.
|
||||||
|
sessionId: iframeOwnerSessionId ?? incomingSessionId,
|
||||||
method: 'Target.attachedToTarget',
|
method: 'Target.attachedToTarget',
|
||||||
params: targetParams
|
params: targetParams
|
||||||
} as CDPEventBase,
|
} as CDPEventBase,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getCdpUrl } from './utils.js'
|
|||||||
* is assignable to this interface.
|
* is assignable to this interface.
|
||||||
*/
|
*/
|
||||||
export interface ICDPSession {
|
export interface ICDPSession {
|
||||||
send(method: string, params?: object): Promise<unknown>
|
send(method: string, params?: object, sessionId?: string | null): Promise<unknown>
|
||||||
on(event: string, callback: (params: any) => void): unknown
|
on(event: string, callback: (params: any) => void): unknown
|
||||||
off(event: string, callback: (params: any) => void): unknown
|
off(event: string, callback: (params: any) => void): unknown
|
||||||
detach(): Promise<void>
|
detach(): Promise<void>
|
||||||
@@ -76,6 +76,12 @@ export class CDPSession implements ICDPSession {
|
|||||||
send<K extends keyof ProtocolMapping.Commands>(
|
send<K extends keyof ProtocolMapping.Commands>(
|
||||||
method: K,
|
method: K,
|
||||||
params?: ProtocolMapping.Commands[K]['paramsType'][0],
|
params?: ProtocolMapping.Commands[K]['paramsType'][0],
|
||||||
|
// Some iframes are OOPIF targets with their own CDP session. Their frameId
|
||||||
|
// only exists inside that target session, so AX/DOM commands must be sent
|
||||||
|
// with the iframe's Target.attachToTarget sessionId instead of the page
|
||||||
|
// sessionId. This override lets us reuse the same websocket while routing
|
||||||
|
// a single command to the correct target session.
|
||||||
|
sessionId?: string | null,
|
||||||
): Promise<ProtocolMapping.Commands[K]['returnType']> {
|
): Promise<ProtocolMapping.Commands[K]['returnType']> {
|
||||||
const id = ++this.messageId
|
const id = ++this.messageId
|
||||||
const message: {
|
const message: {
|
||||||
@@ -85,8 +91,9 @@ export class CDPSession implements ICDPSession {
|
|||||||
sessionId?: string
|
sessionId?: string
|
||||||
source?: 'playwriter'
|
source?: 'playwriter'
|
||||||
} = { id, method, params, source: 'playwriter' }
|
} = { id, method, params, source: 'playwriter' }
|
||||||
if (this.sessionId) {
|
const resolvedSessionId = sessionId ?? this.sessionId
|
||||||
message.sessionId = this.sessionId
|
if (resolvedSessionId) {
|
||||||
|
message.sessionId = resolvedSessionId
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Used by both MCP and CLI to execute Playwright code with persistent state.
|
* Used by both MCP and CLI to execute Playwright code with persistent state.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Page, Browser, BrowserContext, chromium, Locator } from 'playwright-core'
|
import { Page, Frame, Browser, BrowserContext, chromium, Locator } from 'playwright-core'
|
||||||
import crypto from 'node:crypto'
|
import crypto from 'node:crypto'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
@@ -531,7 +531,8 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const accessibilitySnapshot = async (options: {
|
const accessibilitySnapshot = async (options: {
|
||||||
page: Page
|
page?: Page
|
||||||
|
iframe?: Locator
|
||||||
/** Optional locator to scope the snapshot to a subtree */
|
/** Optional locator to scope the snapshot to a subtree */
|
||||||
locator?: Locator
|
locator?: Locator
|
||||||
search?: string | RegExp
|
search?: string | RegExp
|
||||||
@@ -541,11 +542,16 @@ export class PlaywrightExecutor {
|
|||||||
/** Only include interactive elements (default: true) */
|
/** Only include interactive elements (default: true) */
|
||||||
interactiveOnly?: boolean
|
interactiveOnly?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { page: targetPage, locator, search, showDiffSinceLastCall = true, interactiveOnly = false } = options
|
const { page: targetPage, iframe, locator, search, showDiffSinceLastCall = true, interactiveOnly = false } = options
|
||||||
|
const resolvedPage = targetPage || page
|
||||||
|
if (!resolvedPage) {
|
||||||
|
throw new Error('accessibilitySnapshot requires a page')
|
||||||
|
}
|
||||||
|
|
||||||
// Use new in-page implementation via getAriaSnapshot
|
// Use new in-page implementation via getAriaSnapshot
|
||||||
const { snapshot: rawSnapshot, refs, getSelectorForRef } = await getAriaSnapshot({
|
const { snapshot: rawSnapshot, refs, getSelectorForRef } = await getAriaSnapshot({
|
||||||
page: targetPage,
|
page: resolvedPage,
|
||||||
|
iframe,
|
||||||
locator,
|
locator,
|
||||||
wsUrl: getCdpUrl(this.cdpConfig),
|
wsUrl: getCdpUrl(this.cdpConfig),
|
||||||
interactiveOnly,
|
interactiveOnly,
|
||||||
@@ -559,13 +565,16 @@ export class PlaywrightExecutor {
|
|||||||
refToLocator.set(entry.shortRef, locatorStr)
|
refToLocator.set(entry.shortRef, locatorStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.lastRefToLocator.set(targetPage, refToLocator)
|
this.lastRefToLocator.set(resolvedPage, refToLocator)
|
||||||
|
|
||||||
const previousSnapshot = this.lastSnapshots.get(targetPage)
|
const shouldCacheSnapshot = !iframe
|
||||||
this.lastSnapshots.set(targetPage, snapshotStr)
|
const previousSnapshot = shouldCacheSnapshot ? this.lastSnapshots.get(resolvedPage) : undefined
|
||||||
|
if (shouldCacheSnapshot) {
|
||||||
|
this.lastSnapshots.set(resolvedPage, snapshotStr)
|
||||||
|
}
|
||||||
|
|
||||||
// Return diff if we have a previous snapshot and diff mode is enabled
|
// Return diff if we have a previous snapshot and diff mode is enabled
|
||||||
if (showDiffSinceLastCall && previousSnapshot) {
|
if (showDiffSinceLastCall && previousSnapshot && shouldCacheSnapshot) {
|
||||||
const diffResult = createSmartDiff({
|
const diffResult = createSmartDiff({
|
||||||
oldContent: previousSnapshot,
|
oldContent: previousSnapshot,
|
||||||
newContent: snapshotStr,
|
newContent: snapshotStr,
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ playwriter -s 1 -e "await page.screenshot({ path: 'screenshot.png', scale: 'css'
|
|||||||
|
|
||||||
# Get accessibility snapshot
|
# Get accessibility snapshot
|
||||||
playwriter -s 1 -e "await accessibilitySnapshot({ page })"
|
playwriter -s 1 -e "await accessibilitySnapshot({ page })"
|
||||||
|
|
||||||
|
# Get accessibility snapshot for a specific iframe
|
||||||
|
await accessibilitySnapshot({ iframe: page.locator('iframe[src*="iframe.html"]') })
|
||||||
```
|
```
|
||||||
|
|
||||||
**Multiline code:**
|
**Multiline code:**
|
||||||
|
|||||||
Reference in New Issue
Block a user