feat: reuse Playwright's internal CDP session instead of creating new WebSocket connections
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
This commit is contained in:
@@ -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/ }));"
|
||||
|
||||
+1
-1
Submodule playwright updated: 07e8ba0b8b...4e6ceabff8
@@ -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<string>): 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<Frame | undefined> {
|
||||
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 <iframe> element, avoiding ambiguity when multiple
|
||||
// frames share the same origin (e.g. framer plugin iframes).
|
||||
const frameLocator = frame as FrameLocator
|
||||
const owner = frameLocator.owner()
|
||||
const handle = await owner.elementHandle()
|
||||
if (!handle) {
|
||||
throw new Error('Could not resolve FrameLocator to a Frame: iframe element not found')
|
||||
}
|
||||
const resolved = await handle.contentFrame()
|
||||
if (!resolved) {
|
||||
throw new Error('Could not resolve FrameLocator to a Frame: contentFrame() returned null')
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Functions
|
||||
// ============================================================================
|
||||
@@ -748,7 +786,7 @@ function isSubstringOfAny(needle: string, haystack: Set<string>): boolean {
|
||||
*/
|
||||
export async function getAriaSnapshot({ page, frame, locator, refFilter, wsUrl, interactiveOnly = false, cdp }: {
|
||||
page: Page
|
||||
frame?: Frame
|
||||
frame?: Frame | FrameLocator
|
||||
locator?: Locator
|
||||
refFilter?: (info: { role: string; name: string }) => boolean
|
||||
wsUrl?: string
|
||||
@@ -756,15 +794,20 @@ export async function getAriaSnapshot({ page, frame, locator, refFilter, wsUrl,
|
||||
cdp?: ICDPSession
|
||||
}): Promise<AriaSnapshotResult> {
|
||||
const session = cdp || await getCDPSessionForPage({ page, wsUrl })
|
||||
|
||||
// Resolve FrameLocator to an actual Frame. FrameLocator (from locator.contentFrame())
|
||||
// is a scoping helper without CDP access. We need the real Frame from page.frames()
|
||||
// which has frameId() for OOPIF session attachment.
|
||||
const resolvedFrame = await resolveFrame({ frame, page })
|
||||
|
||||
// For cross-origin iframes (OOPIFs), we need to attach to the iframe's target
|
||||
// to get a separate CDP session. Same-origin iframes can use frameId directly.
|
||||
let oopifSessionId: string | null = null
|
||||
const frameId = frame?.frameId() ?? null
|
||||
const frameId = resolvedFrame?.frameId() ?? null
|
||||
|
||||
if (frameId) {
|
||||
const { targetInfos } = await session.send('Target.getTargets') as Protocol.Target.GetTargetsResponse
|
||||
const frameUrl = frame!.url()
|
||||
const frameUrl = resolvedFrame!.url()
|
||||
const iframeTarget = targetInfos.find((t) => {
|
||||
return t.type === 'iframe' && t.url === frameUrl
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import WebSocket from 'ws'
|
||||
import type { Page } from '@xmorse/playwright-core'
|
||||
import type { Page, CDPSession as PlaywrightCDPSession } from '@xmorse/playwright-core'
|
||||
import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'
|
||||
import type { CDPResponseBase, CDPEventBase } from './cdp-types.js'
|
||||
import { getCdpUrl } from './utils.js'
|
||||
@@ -233,3 +233,50 @@ export async function getCDPSessionForPage({ page, wsUrl }: { page: Page; wsUrl?
|
||||
|
||||
return cdp
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps Playwright's CDPSession (from context.getExistingCDPSession) into an ICDPSession.
|
||||
* This reuses Playwright's internal CDP WebSocket instead of creating a new one,
|
||||
* which is important for the relay server where Target.attachToTarget is intercepted.
|
||||
*
|
||||
* The adapter maps between devtools-protocol's ProtocolMapping types (used by our CDPSession)
|
||||
* and Playwright's Protocol types (used by their CDPSession). Both are compatible since
|
||||
* ICDPSession uses loose string-based method names.
|
||||
*/
|
||||
export class PlaywrightCDPSessionAdapter implements ICDPSession {
|
||||
private _playwrightSession: PlaywrightCDPSession
|
||||
|
||||
constructor(playwrightSession: PlaywrightCDPSession) {
|
||||
this._playwrightSession = playwrightSession
|
||||
}
|
||||
|
||||
async send(method: string, params?: object): Promise<unknown> {
|
||||
return await this._playwrightSession.send(method as never, params as never)
|
||||
}
|
||||
|
||||
on(event: string, callback: (params: unknown) => void): this {
|
||||
this._playwrightSession.on(event as never, callback as never)
|
||||
return this
|
||||
}
|
||||
|
||||
off(event: string, callback: (params: unknown) => void): this {
|
||||
this._playwrightSession.off(event as never, callback as never)
|
||||
return this
|
||||
}
|
||||
|
||||
async detach(): Promise<void> {
|
||||
await this._playwrightSession.detach()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a CDP session for a page by reusing Playwright's internal existing CDP session.
|
||||
* Unlike getCDPSessionForPage which creates a new WebSocket, this uses the same WS
|
||||
* Playwright already has. Works through the relay because it doesn't call
|
||||
* Target.attachToTarget.
|
||||
*/
|
||||
export async function getExistingCDPSessionForPage({ page }: { page: Page }): Promise<PlaywrightCDPSessionAdapter> {
|
||||
const context = page.context()
|
||||
const playwrightSession = await context.getExistingCDPSession(page)
|
||||
return new PlaywrightCDPSessionAdapter(playwrightSession)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Used by both MCP and CLI to execute Playwright code with persistent state.
|
||||
*/
|
||||
|
||||
import { Page, Frame, Browser, BrowserContext, chromium, Locator } from '@xmorse/playwright-core'
|
||||
import { Page, Frame, Browser, BrowserContext, chromium, Locator, FrameLocator } from '@xmorse/playwright-core'
|
||||
import crypto from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
@@ -16,7 +16,7 @@ import * as acorn from 'acorn'
|
||||
import { createSmartDiff } from './diff-utils.js'
|
||||
import { getCdpUrl } from './utils.js'
|
||||
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
|
||||
import { getCDPSessionForPage, CDPSession, ICDPSession } from './cdp-session.js'
|
||||
import { getCDPSessionForPage, CDPSession, ICDPSession, getExistingCDPSessionForPage } from './cdp-session.js'
|
||||
import { Debugger } from './debugger.js'
|
||||
import { Editor } from './editor.js'
|
||||
import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js'
|
||||
@@ -218,7 +218,7 @@ export class PlaywrightExecutor {
|
||||
private browserLogs: Map<string, string[]> = new Map()
|
||||
private lastSnapshots: WeakMap<Page, string> = new WeakMap()
|
||||
private lastRefToLocator: WeakMap<Page, Map<string, string>> = new WeakMap()
|
||||
private cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
|
||||
private cdpSessionCache: WeakMap<Page, ICDPSession> = new WeakMap()
|
||||
private scopedFs: ScopedFS
|
||||
private sandboxedRequire: NodeRequire
|
||||
|
||||
@@ -533,8 +533,8 @@ export class PlaywrightExecutor {
|
||||
|
||||
const accessibilitySnapshot = async (options: {
|
||||
page?: Page
|
||||
/** Optional frame to scope the snapshot (e.g. from iframe.contentFrame()) */
|
||||
frame?: Frame
|
||||
/** Optional frame to scope the snapshot (e.g. from iframe.contentFrame() or page.frames()) */
|
||||
frame?: Frame | FrameLocator
|
||||
/** Optional locator to scope the snapshot to a subtree */
|
||||
locator?: Locator
|
||||
search?: string | RegExp
|
||||
@@ -723,9 +723,10 @@ export class PlaywrightExecutor {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Generate a fresh unique URL for each CDP session to avoid client ID conflicts
|
||||
const wsUrl = getCdpUrl(this.cdpConfig)
|
||||
const session = await getCDPSessionForPage({ page: options.page, wsUrl })
|
||||
// Reuse Playwright's internal CDP session over the same WebSocket instead of
|
||||
// creating a new WS connection. This is critical for the relay where
|
||||
// Target.attachToTarget is intercepted and can't create real new sessions.
|
||||
const session = await getExistingCDPSessionForPage({ page: options.page })
|
||||
this.cdpSessionCache.set(options.page, session)
|
||||
|
||||
options.page.on('close', () => {
|
||||
@@ -734,7 +735,9 @@ export class PlaywrightExecutor {
|
||||
return
|
||||
}
|
||||
this.cdpSessionCache.delete(options.page)
|
||||
cachedSession.close()
|
||||
// Borrowed sessions: detach is a no-op since Playwright owns the underlying session.
|
||||
// We just clean up the cache reference.
|
||||
cachedSession.detach().catch(() => {})
|
||||
})
|
||||
|
||||
return session
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './cdp-relay.js'
|
||||
export * from './utils.js'
|
||||
export { CDPSession, getCDPSessionForPage } from './cdp-session.js'
|
||||
export { CDPSession, getCDPSessionForPage, getExistingCDPSessionForPage, PlaywrightCDPSessionAdapter } from './cdp-session.js'
|
||||
export type { ICDPSession } from './cdp-session.js'
|
||||
export { Editor } from './editor.js'
|
||||
export type { ReadResult, SearchMatch, EditResult } from './editor.js'
|
||||
|
||||
@@ -8,7 +8,7 @@ import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import { imageSize } from 'image-size'
|
||||
import { getCdpUrl } from './utils.js'
|
||||
import { getCDPSessionForPage } from './cdp-session.js'
|
||||
import { getCDPSessionForPage, getExistingCDPSessionForPage } from './cdp-session.js'
|
||||
import type { CDPCommand } from './cdp-types.js'
|
||||
import { screenshotWithAccessibilityLabels } from './aria-snapshot.js'
|
||||
import { setupTestContext, cleanupTestContext, getExtensionServiceWorker, type TestContext, js } from './test-utils.js'
|
||||
@@ -575,6 +575,43 @@ describe('Snapshot & Screenshot Tests', () => {
|
||||
await page.close()
|
||||
}, 60000)
|
||||
|
||||
it('should support getExistingCDPSession through the relay (reusing Playwright WS)', async () => {
|
||||
const browserContext = getBrowserContext()
|
||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||
|
||||
const page = await browserContext.newPage()
|
||||
await page.goto('https://example.com/')
|
||||
await page.bringToFront()
|
||||
|
||||
await serviceWorker.evaluate(async () => {
|
||||
await globalThis.toggleExtensionForActiveTab()
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
// Use the new getExistingCDPSessionForPage which reuses Playwright's internal WS
|
||||
const cdpClient = await getExistingCDPSessionForPage({ page: cdpPage! })
|
||||
|
||||
// Should be able to send CDP commands just like the regular getCDPSessionForPage
|
||||
const layoutMetrics = await cdpClient.send('Page.getLayoutMetrics')
|
||||
expect(layoutMetrics).toBeDefined()
|
||||
const metrics = layoutMetrics as { cssVisualViewport?: { clientWidth?: number } }
|
||||
expect(metrics.cssVisualViewport).toBeDefined()
|
||||
expect(metrics.cssVisualViewport!.clientWidth).toBeGreaterThan(0)
|
||||
|
||||
// Test DOM access
|
||||
const document = await cdpClient.send('DOM.getDocument')
|
||||
expect(document).toBeDefined()
|
||||
|
||||
await cdpClient.detach()
|
||||
await browser.close()
|
||||
await page.close()
|
||||
}, 60000)
|
||||
|
||||
it('should get aria ref for locator using getAriaSnapshot', async () => {
|
||||
const browserContext = getBrowserContext()
|
||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||
|
||||
Generated
+25
-10
@@ -251,6 +251,9 @@ importers:
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.6.1
|
||||
get-stream:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
https-proxy-agent:
|
||||
specifier: 7.0.6
|
||||
version: 7.0.6
|
||||
@@ -2449,6 +2452,9 @@ packages:
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||
|
||||
'@sentry-internal/browser-utils@9.47.1':
|
||||
resolution: {integrity: sha512-twv6YhrUlPkvKz4/iQDH4KHgcv9t4cMjmZPf4/dCSCXn4/GOjzjx2d74c1w+1KOdS7lcsQzI+MtbK6SeYLiGfQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4229,6 +4235,10 @@ packages:
|
||||
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
get-stream@9.0.1:
|
||||
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
get-symbol-description@1.1.0:
|
||||
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4567,6 +4577,10 @@ packages:
|
||||
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-stream@4.0.1:
|
||||
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
is-string@1.1.1:
|
||||
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -8902,6 +8916,8 @@ snapshots:
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
'@sentry-internal/browser-utils@9.47.1':
|
||||
dependencies:
|
||||
'@sentry/core': 9.47.1
|
||||
@@ -9514,14 +9530,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.6.0)
|
||||
|
||||
'@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.6.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.8
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.6.0)
|
||||
|
||||
'@vitest/mocker@4.0.8(vite@7.2.2(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.6.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.8
|
||||
@@ -9573,7 +9581,7 @@ snapshots:
|
||||
sirv: 3.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.6.1)(jsdom@27.2.0(bufferutil@4.0.9))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.6.0)
|
||||
vitest: 4.0.8(@types/node@25.2.0)(@vitest/ui@4.0.8)(jiti@2.6.1)(jsdom@27.2.0(bufferutil@4.0.9))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.6.0)
|
||||
|
||||
'@vitest/utils@4.0.16':
|
||||
dependencies:
|
||||
@@ -11180,6 +11188,11 @@ snapshots:
|
||||
dependencies:
|
||||
pump: 3.0.3
|
||||
|
||||
get-stream@9.0.1:
|
||||
dependencies:
|
||||
'@sec-ant/readable-stream': 0.4.1
|
||||
is-stream: 4.0.1
|
||||
|
||||
get-symbol-description@1.1.0:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -11569,6 +11582,8 @@ snapshots:
|
||||
|
||||
is-stream@1.1.0: {}
|
||||
|
||||
is-stream@4.0.1: {}
|
||||
|
||||
is-string@1.1.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -13655,7 +13670,7 @@ snapshots:
|
||||
vitest@4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.6.1)(jsdom@27.2.0(bufferutil@4.0.9))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.6.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.8
|
||||
'@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.6.0))
|
||||
'@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.6.0))
|
||||
'@vitest/pretty-format': 4.0.8
|
||||
'@vitest/runner': 4.0.8
|
||||
'@vitest/snapshot': 4.0.8
|
||||
|
||||
@@ -6,7 +6,7 @@ The getStylesForLocator function inspects CSS styles applied to an element, simi
|
||||
|
||||
```ts
|
||||
import type { ICDPSession } from './cdp-session.js';
|
||||
import type { Locator } from 'playwright-core';
|
||||
import type { Locator } from '@xmorse/playwright-core';
|
||||
export interface StyleSource {
|
||||
url: string;
|
||||
line: number;
|
||||
|
||||
Reference in New Issue
Block a user