Fix auto-enable to work without delay by skipping duplicate Target.attachedToTarget
Move auto-create logic from onOpen to Target.setAutoAttach handler so it runs synchronously in the CDP command flow. Add skipAttachedEvent option to extension's attachTab to prevent sending Target.attachedToTarget when the relay will send it via the Target.setAutoAttach response handler.
This commit is contained in:
+41
-16
@@ -273,7 +273,7 @@ async function handleCommand(msg: ExtensionCommandMessage): Promise<any> {
|
||||
if (!tab.id) throw new Error('Failed to create tab')
|
||||
logger.debug('Created tab:', tab.id, 'waiting for it to load...')
|
||||
await sleep(100)
|
||||
const targetInfo = await attachTab(tab.id)
|
||||
const { targetInfo } = await attachTab(tab.id)
|
||||
return { targetId: targetInfo.targetId } satisfies Protocol.Target.CreateTargetResponse
|
||||
}
|
||||
|
||||
@@ -378,7 +378,12 @@ function onDebuggerDetach(source: chrome.debugger.Debuggee, reason: `${chrome.de
|
||||
}
|
||||
}
|
||||
|
||||
async function attachTab(tabId: number): Promise<Protocol.Target.TargetInfo> {
|
||||
type AttachTabResult = {
|
||||
targetInfo: Protocol.Target.TargetInfo
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
async function attachTab(tabId: number, { skipAttachedEvent = false }: { skipAttachedEvent?: boolean } = {}): Promise<AttachTabResult> {
|
||||
const debuggee = { tabId }
|
||||
|
||||
logger.debug('Attaching debugger to tab:', tabId)
|
||||
@@ -415,20 +420,22 @@ async function attachTab(tabId: number): Promise<Protocol.Target.TargetInfo> {
|
||||
return { tabs: newTabs, connectionState: 'connected', errorText: undefined }
|
||||
})
|
||||
|
||||
sendMessage({
|
||||
method: 'forwardCDPEvent',
|
||||
params: {
|
||||
method: 'Target.attachedToTarget',
|
||||
if (!skipAttachedEvent) {
|
||||
sendMessage({
|
||||
method: 'forwardCDPEvent',
|
||||
params: {
|
||||
sessionId,
|
||||
targetInfo: { ...targetInfo, attached: true },
|
||||
waitingForDebugger: false,
|
||||
method: 'Target.attachedToTarget',
|
||||
params: {
|
||||
sessionId,
|
||||
targetInfo: { ...targetInfo, attached: true },
|
||||
waitingForDebugger: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
logger.debug('Tab attached successfully:', tabId, 'sessionId:', sessionId, 'targetId:', targetInfo.targetId)
|
||||
return targetInfo
|
||||
logger.debug('Tab attached successfully:', tabId, 'sessionId:', sessionId, 'targetId:', targetInfo.targetId, 'skipAttachedEvent:', skipAttachedEvent)
|
||||
return { targetInfo, sessionId }
|
||||
}
|
||||
|
||||
function detachTab(tabId: number, shouldDetachDebugger: boolean): void {
|
||||
@@ -630,14 +637,32 @@ async function ensureConnection(): Promise<void> {
|
||||
}
|
||||
|
||||
// Handle createInitialTab - create a new tab when Playwright connects and no tabs exist
|
||||
// We use skipAttachedEvent: true because the relay's Target.setAutoAttach handler will send
|
||||
// Target.attachedToTarget for all targets in connectedTargets. If we also sent it here,
|
||||
// Playwright would receive a duplicate.
|
||||
//
|
||||
// This differs from the normal flow (user clicks extension icon) where:
|
||||
// 1. Extension attaches and sends Target.attachedToTarget to existing Playwright clients
|
||||
// 2. New Playwright clients that connect later get targets via Target.setAutoAttach
|
||||
//
|
||||
// But with createInitialTab, the SAME client that triggered the create is waiting for
|
||||
// Target.setAutoAttach - so we'd send the event twice to the same client.
|
||||
if (message.method === 'createInitialTab') {
|
||||
try {
|
||||
logger.debug('Creating initial tab for Playwright client')
|
||||
const tab = await chrome.tabs.create({ url: 'about:blank', active: false })
|
||||
if (tab.id) {
|
||||
await connectTab(tab.id)
|
||||
logger.debug('Initial tab created and connected:', tab.id)
|
||||
sendMessage({ id: message.id, result: { success: true, tabId: tab.id } })
|
||||
const { targetInfo, sessionId } = await attachTab(tab.id, { skipAttachedEvent: true })
|
||||
logger.debug('Initial tab created and connected:', tab.id, 'sessionId:', sessionId)
|
||||
sendMessage({
|
||||
id: message.id,
|
||||
result: {
|
||||
success: true,
|
||||
tabId: tab.id,
|
||||
sessionId,
|
||||
targetInfo,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error('Failed to create tab - no tab ID returned')
|
||||
}
|
||||
|
||||
+38
-24
@@ -193,6 +193,40 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
||||
})
|
||||
}
|
||||
|
||||
// Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
|
||||
// This allows Playwright to connect and immediately have a page to work with.
|
||||
async function maybeAutoCreateInitialTab(): Promise<void> {
|
||||
if (!process.env.PLAYWRITER_AUTO_ENABLE) {
|
||||
return
|
||||
}
|
||||
if (!extensionWs) {
|
||||
return
|
||||
}
|
||||
if (connectedTargets.size > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
logger?.log(chalk.blue('Auto-creating initial tab for Playwright client'))
|
||||
const result = await sendToExtension({ method: 'createInitialTab', timeout: 10000 }) as {
|
||||
success: boolean
|
||||
tabId: number
|
||||
sessionId: string
|
||||
targetInfo: Protocol.Target.TargetInfo
|
||||
}
|
||||
if (result.success && result.sessionId && result.targetInfo) {
|
||||
connectedTargets.set(result.sessionId, {
|
||||
sessionId: result.sessionId,
|
||||
targetId: result.targetInfo.targetId,
|
||||
targetInfo: result.targetInfo
|
||||
})
|
||||
logger?.log(chalk.blue(`Auto-created tab, now have ${connectedTargets.size} targets`))
|
||||
}
|
||||
} catch (e) {
|
||||
logger?.error('Failed to auto-create initial tab:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function routeCdpCommand({ method, params, sessionId }: { method: string; params: any; sessionId?: string }) {
|
||||
switch (method) {
|
||||
case 'Browser.getVersion': {
|
||||
@@ -209,10 +243,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
||||
return {}
|
||||
}
|
||||
|
||||
// Target.setAutoAttach is a CDP command Playwright sends on first connection.
|
||||
// We use it as the hook to auto-create an initial tab. If Playwright changes
|
||||
// its initialization sequence in the future, this could be moved to a different command.
|
||||
case 'Target.setAutoAttach': {
|
||||
if (sessionId) {
|
||||
break
|
||||
}
|
||||
await maybeAutoCreateInitialTab()
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -371,30 +409,6 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
||||
// Add client first so it can receive Target.attachedToTarget events
|
||||
playwrightClients.set(clientId, { id: clientId, ws })
|
||||
logger?.log(chalk.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total) (extension? ${!!extensionWs}) (${connectedTargets.size} pages)`))
|
||||
|
||||
// Auto-create initial tab if enabled and no targets exist
|
||||
if (process.env.PLAYWRITER_AUTO_ENABLE && extensionWs && connectedTargets.size === 0) {
|
||||
try {
|
||||
logger?.log(chalk.blue('Auto-creating initial tab for Playwright client'))
|
||||
await sendToExtension({ method: 'createInitialTab', timeout: 10000 })
|
||||
// Wait for Target.attachedToTarget event to populate connectedTargets
|
||||
await new Promise((resolve) => {
|
||||
const checkTargets = () => {
|
||||
if (connectedTargets.size > 0) {
|
||||
resolve(undefined)
|
||||
} else {
|
||||
setTimeout(checkTargets, 50)
|
||||
}
|
||||
}
|
||||
checkTargets()
|
||||
// Timeout after 5 seconds
|
||||
setTimeout(() => resolve(undefined), 5000)
|
||||
})
|
||||
logger?.log(chalk.blue(`Auto-created tab, now have ${connectedTargets.size} targets`))
|
||||
} catch (e) {
|
||||
logger?.error('Failed to auto-create initial tab:', e)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async onMessage(event, ws) {
|
||||
|
||||
@@ -2924,12 +2924,10 @@ describe('Auto-enable Tests', () => {
|
||||
// Connect Playwright - this should trigger auto-create
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
|
||||
// Wait for auto-create to complete (async onOpen may not be fully awaited)
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
// Verify a page was auto-created
|
||||
const pages = browser.contexts()[0].pages()
|
||||
expect(pages.length).toBeGreaterThan(0)
|
||||
expect(pages.length).toBe(1)
|
||||
|
||||
const autoCreatedPage = pages[0]
|
||||
expect(autoCreatedPage.url()).toBe('about:blank')
|
||||
|
||||
Reference in New Issue
Block a user