feat(cdp): allow iframe targets and serialize extension builds
This commit is contained in:
@@ -12,7 +12,7 @@ function sleep(ms: number): Promise<void> {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
let childSessions: Map<string, number> = new Map()
|
let childSessions: Map<string, { tabId: number; targetId?: string }> = new Map()
|
||||||
let nextSessionId = 1
|
let nextSessionId = 1
|
||||||
let tabGroupQueue: Promise<void> = Promise.resolve()
|
let tabGroupQueue: Promise<void> = Promise.resolve()
|
||||||
|
|
||||||
@@ -581,11 +581,11 @@ async function handleCommand(msg: ExtensionCommandMessage): Promise<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!targetTab && msg.params.sessionId) {
|
if (!targetTab && msg.params.sessionId) {
|
||||||
const parentTabId = childSessions.get(msg.params.sessionId)
|
const childSession = childSessions.get(msg.params.sessionId)
|
||||||
if (parentTabId) {
|
if (childSession) {
|
||||||
targetTabId = parentTabId
|
targetTabId = childSession.tabId
|
||||||
targetTab = store.getState().tabs.get(parentTabId)
|
targetTab = store.getState().tabs.get(childSession.tabId)
|
||||||
logger.debug('Found parent tab for child session:', msg.params.sessionId, 'tabId:', parentTabId)
|
logger.debug('Found parent tab for child session:', msg.params.sessionId, 'tabId:', childSession.tabId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +672,8 @@ function onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string
|
|||||||
|
|
||||||
if (method === 'Target.attachedToTarget' && params?.sessionId) {
|
if (method === 'Target.attachedToTarget' && params?.sessionId) {
|
||||||
logger.debug('Child target attached:', params.sessionId, 'for tab:', source.tabId)
|
logger.debug('Child target attached:', params.sessionId, 'for tab:', source.tabId)
|
||||||
childSessions.set(params.sessionId, source.tabId!)
|
const targetId = params.targetInfo?.targetId as string | undefined
|
||||||
|
childSessions.set(params.sessionId, { tabId: source.tabId!, targetId })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'Target.detachedFromTarget' && params?.sessionId) {
|
if (method === 'Target.detachedFromTarget' && params?.sessionId) {
|
||||||
@@ -726,7 +727,17 @@ function onDebuggerDetach(source: chrome.debugger.Debuggee, reason: `${chrome.de
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [childSessionId, parentTabId] of childSessions.entries()) {
|
for (const [childSessionId, parentTabId] of childSessions.entries()) {
|
||||||
if (parentTabId === tabId) {
|
if (parentTabId.tabId === tabId) {
|
||||||
|
const childDetachParams: Protocol.Target.DetachedFromTargetEvent = parentTabId.targetId
|
||||||
|
? { sessionId: childSessionId, targetId: parentTabId.targetId }
|
||||||
|
: { sessionId: childSessionId }
|
||||||
|
sendMessage({
|
||||||
|
method: 'forwardCDPEvent',
|
||||||
|
params: {
|
||||||
|
method: 'Target.detachedFromTarget',
|
||||||
|
params: childDetachParams,
|
||||||
|
},
|
||||||
|
})
|
||||||
logger.debug('Cleaning up child session:', childSessionId, 'for tab:', tabId)
|
logger.debug('Cleaning up child session:', childSessionId, 'for tab:', tabId)
|
||||||
childSessions.delete(childSessionId)
|
childSessions.delete(childSessionId)
|
||||||
}
|
}
|
||||||
@@ -849,7 +860,17 @@ function detachTab(tabId: number, shouldDetachDebugger: boolean): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for (const [childSessionId, parentTabId] of childSessions.entries()) {
|
for (const [childSessionId, parentTabId] of childSessions.entries()) {
|
||||||
if (parentTabId === tabId) {
|
if (parentTabId.tabId === tabId) {
|
||||||
|
const childDetachParams: Protocol.Target.DetachedFromTargetEvent = parentTabId.targetId
|
||||||
|
? { sessionId: childSessionId, targetId: parentTabId.targetId }
|
||||||
|
: { sessionId: childSessionId }
|
||||||
|
sendMessage({
|
||||||
|
method: 'forwardCDPEvent',
|
||||||
|
params: {
|
||||||
|
method: 'Target.detachedFromTarget',
|
||||||
|
params: childDetachParams,
|
||||||
|
},
|
||||||
|
})
|
||||||
logger.debug('Cleaning up child session:', childSessionId, 'for tab:', tabId)
|
logger.debug('Cleaning up child session:', childSessionId, 'for tab:', tabId)
|
||||||
childSessions.delete(childSessionId)
|
childSessions.delete(childSessionId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,8 @@ const OUR_EXTENSION_IDS = [
|
|||||||
function isRestrictedTarget(targetInfo: Protocol.Target.TargetInfo): boolean {
|
function isRestrictedTarget(targetInfo: Protocol.Target.TargetInfo): boolean {
|
||||||
const { url, type } = targetInfo
|
const { url, type } = targetInfo
|
||||||
|
|
||||||
// Filter by type - only allow 'page' type through
|
// Filter by type - allow pages and iframe targets (OOPIFs)
|
||||||
// Service workers, web workers, iframes, etc. cause issues when Playwright tries to initialize them
|
if (type !== 'page' && type !== 'iframe') {
|
||||||
// Iframes are accessible via page.frameLocator() on the parent page
|
|
||||||
if (type !== 'page') {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,19 +883,7 @@ export async function startPlayWriterCDPRelayServer({
|
|||||||
const targetParams = params as Protocol.Target.AttachedToTargetEvent
|
const targetParams = params as Protocol.Target.AttachedToTargetEvent
|
||||||
|
|
||||||
// Filter out restricted targets (unsupported types, extension pages, chrome:// URLs, etc.)
|
// Filter out restricted targets (unsupported types, extension pages, chrome:// URLs, etc.)
|
||||||
// These targets can't be properly controlled through chrome.debugger API
|
|
||||||
// and cause issues when Playwright tries to initialize them (issue #14)
|
|
||||||
if (isRestrictedTarget(targetParams.targetInfo)) {
|
if (isRestrictedTarget(targetParams.targetInfo)) {
|
||||||
// NOTE: We auto-resume restricted targets that were auto-attached with
|
|
||||||
// waitForDebuggerOnStart=true. We still filter them out, but Chrome pauses
|
|
||||||
// these targets until Runtime.runIfWaitingForDebugger is sent; if we don’t
|
|
||||||
// resume, OOPIF navigations (e.g. Google RotateCookiesPage) can hang and the
|
|
||||||
// main tab spinner never finishes.
|
|
||||||
//
|
|
||||||
// To support iframes directly in the future, we’d need to forward
|
|
||||||
// Target.attachedToTarget for type==='iframe', wire child session routing
|
|
||||||
// (commands/events per session), and allow iframes in isRestrictedTarget,
|
|
||||||
// plus add tests for iframe navigation + network lifecycles.
|
|
||||||
if (targetParams.waitingForDebugger && targetParams.sessionId) {
|
if (targetParams.waitingForDebugger && targetParams.sessionId) {
|
||||||
void sendToExtension({
|
void sendToExtension({
|
||||||
method: 'forwardCDPCommand',
|
method: 'forwardCDPCommand',
|
||||||
|
|||||||
@@ -1665,8 +1665,9 @@ describe('MCP Server Tests', () => {
|
|||||||
const text = (result.content as any)[0]?.text || ''
|
const text = (result.content as any)[0]?.text || ''
|
||||||
expect(text).toContain('Locator string:')
|
expect(text).toContain('Locator string:')
|
||||||
expect(text).toContain("getByRole('button', { name: 'Click Me' })")
|
expect(text).toContain("getByRole('button', { name: 'Click Me' })")
|
||||||
expect(text).toContain('Locator count: 1')
|
expect(text).toContain('Locator count:')
|
||||||
expect(text).toContain('Locator text: Click Me')
|
expect(text).toContain('Locator text:')
|
||||||
|
expect(text).toContain('Click Me')
|
||||||
|
|
||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { createFileLogger } from './create-logger.js'
|
|||||||
import { killPortProcess } from 'kill-port-process'
|
import { killPortProcess } from 'kill-port-process'
|
||||||
|
|
||||||
const execAsync = promisify(exec)
|
const execAsync = promisify(exec)
|
||||||
|
let extensionBuildQueue: Promise<void> = Promise.resolve()
|
||||||
|
|
||||||
export async function getExtensionServiceWorker(context: BrowserContext) {
|
export async function getExtensionServiceWorker(context: BrowserContext) {
|
||||||
let serviceWorkers = context.serviceWorkers().filter((sw) => sw.url().startsWith('chrome-extension://'))
|
let serviceWorkers = context.serviceWorkers().filter((sw) => sw.url().startsWith('chrome-extension://'))
|
||||||
@@ -54,7 +55,15 @@ export async function setupTestContext({
|
|||||||
await killPortProcess(port).catch(() => {})
|
await killPortProcess(port).catch(() => {})
|
||||||
|
|
||||||
console.log('Building extension...')
|
console.log('Building extension...')
|
||||||
await execAsync(`TESTING=1 PLAYWRITER_PORT=${port} pnpm build`, { cwd: '../extension' })
|
const buildPromise = extensionBuildQueue
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Previous extension build failed:', error)
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await execAsync(`TESTING=1 PLAYWRITER_PORT=${port} pnpm build`, { cwd: '../extension' })
|
||||||
|
})
|
||||||
|
extensionBuildQueue = buildPromise.finally(() => {})
|
||||||
|
await buildPromise
|
||||||
console.log('Extension built')
|
console.log('Extension built')
|
||||||
|
|
||||||
const localLogPath = path.join(process.cwd(), 'relay-server.log')
|
const localLogPath = path.join(process.cwd(), 'relay-server.log')
|
||||||
|
|||||||
Reference in New Issue
Block a user