send runtime.disable then enable
This commit is contained in:
@@ -72,11 +72,6 @@ interface AttachedTab {
|
||||
targetId: string;
|
||||
sessionId: string;
|
||||
targetInfo: Protocol.Target.TargetInfo;
|
||||
// Cache execution contexts for this tab. When Playwright reconnects and calls Runtime.enable,
|
||||
// Chrome's debugger does NOT re-send Runtime.executionContextCreated events for contexts that
|
||||
// already exist. We must manually replay them so Playwright knows what contexts are available.
|
||||
// Without this, page.evaluate() hangs because Playwright has no valid execution context IDs.
|
||||
executionContexts: Map<number, Protocol.Runtime.ExecutionContextCreatedEvent>;
|
||||
}
|
||||
|
||||
export class RelayConnection {
|
||||
@@ -180,7 +175,6 @@ export class RelayConnection {
|
||||
targetId: targetInfo.targetId,
|
||||
sessionId,
|
||||
targetInfo,
|
||||
executionContexts: new Map()
|
||||
});
|
||||
|
||||
logger.debug('Sending Target.attachedToTarget event, WebSocket state:', this._ws.readyState);
|
||||
@@ -291,18 +285,6 @@ export class RelayConnection {
|
||||
// Track execution contexts so we can replay them when Playwright reconnects.
|
||||
// Chrome's debugger only sends Runtime.executionContextCreated events once per context,
|
||||
// not on every Runtime.enable call. We cache them here and replay on reconnection.
|
||||
if (method === 'Runtime.executionContextCreated') {
|
||||
const contextEvent = params as Protocol.Runtime.ExecutionContextCreatedEvent;
|
||||
tab.executionContexts.set(contextEvent.context.id, contextEvent);
|
||||
logger.debug('Cached execution context:', contextEvent.context.id, 'for tab:', source.tabId, 'total contexts:', tab.executionContexts.size);
|
||||
} else if (method === 'Runtime.executionContextDestroyed') {
|
||||
const destroyedEvent = params as Protocol.Runtime.ExecutionContextDestroyedEvent;
|
||||
tab.executionContexts.delete(destroyedEvent.executionContextId);
|
||||
logger.debug('Removed execution context:', destroyedEvent.executionContextId, 'from tab:', source.tabId, 'remaining:', tab.executionContexts.size);
|
||||
} else if (method === 'Runtime.executionContextsCleared') {
|
||||
tab.executionContexts.clear();
|
||||
logger.debug('Cleared all execution contexts for tab:', source.tabId);
|
||||
}
|
||||
|
||||
logger.debug('Forwarding CDP event:', method, 'from tab:', source.tabId);
|
||||
|
||||
@@ -401,34 +383,25 @@ export class RelayConnection {
|
||||
sessionId: msg.params.sessionId !== targetTab.sessionId ? msg.params.sessionId : undefined,
|
||||
};
|
||||
|
||||
if (msg.params.method === 'Runtime.enable') {
|
||||
logger.debug('Runtime.enable called, disabling first to force context refresh for tab:', targetTab.debuggee.tabId);
|
||||
try {
|
||||
await chrome.debugger.sendCommand(debuggerSession, 'Runtime.disable');
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
} catch (e) {
|
||||
logger.debug('Error disabling Runtime (ignoring):', e);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await chrome.debugger.sendCommand(
|
||||
debuggerSession,
|
||||
msg.params.method,
|
||||
msg.params.params
|
||||
);
|
||||
|
||||
// When Playwright reconnects and calls Runtime.enable, Chrome does NOT automatically
|
||||
// re-send Runtime.executionContextCreated events for contexts that already exist.
|
||||
// This causes page.evaluate() to hang because Playwright has no execution context IDs.
|
||||
// Solution: manually replay all cached contexts so Playwright gets fresh, valid IDs.
|
||||
if (msg.params.method === 'Runtime.enable' && targetTab.executionContexts.size > 0) {
|
||||
logger.debug('Runtime.enable called, replaying', targetTab.executionContexts.size, 'cached execution contexts for tab:', targetTab.debuggee.tabId);
|
||||
|
||||
for (const contextEvent of targetTab.executionContexts.values()) {
|
||||
logger.debug('Replaying execution context:', contextEvent.context.id);
|
||||
this._sendMessage({
|
||||
method: 'forwardCDPEvent',
|
||||
params: {
|
||||
sessionId: msg.params.sessionId,
|
||||
method: 'Runtime.executionContextCreated',
|
||||
params: contextEvent,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private _sendMessage(message: any): void {
|
||||
@@ -437,10 +410,12 @@ export class RelayConnection {
|
||||
this._ws.send(JSON.stringify(message));
|
||||
// logger.debug('Message sent successfully, type:', message.method || 'response');
|
||||
} catch (error: any) {
|
||||
logger.debug('ERROR sending message:', error, 'message type:', message.method || 'response');
|
||||
// Use console directly to avoid infinite recursion if logger tries to send log over this same connection
|
||||
console.debug('ERROR sending message:', error, 'message type:', message.method || 'response');
|
||||
}
|
||||
} else {
|
||||
logger.debug('Cannot send message, WebSocket not open. State:', this._ws.readyState, 'message type:', message.method || 'response');
|
||||
// Use console directly to avoid infinite recursion
|
||||
console.debug('Cannot send message, WebSocket not open. State:', this._ws.readyState, 'message type:', message.method || 'response');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +311,11 @@ describe('MCP Server Tests', () => {
|
||||
expect(foundPage).toBeDefined()
|
||||
expect(foundPage?.url()).toBe(testUrl)
|
||||
|
||||
// Verify execution works
|
||||
const sum1 = await foundPage?.evaluate(() => 1 + 1)
|
||||
expect(sum1).toBe(2)
|
||||
|
||||
await directBrowser.close()
|
||||
|
||||
|
||||
// 4. Disable extension on this tab
|
||||
@@ -330,6 +335,7 @@ describe('MCP Server Tests', () => {
|
||||
foundPage = pages.find(p => p.url() === testUrl)
|
||||
expect(foundPage).toBeUndefined()
|
||||
|
||||
await directBrowser.close()
|
||||
|
||||
|
||||
// 6. Re-enable extension
|
||||
@@ -355,7 +361,11 @@ describe('MCP Server Tests', () => {
|
||||
expect(foundPage).toBeDefined()
|
||||
expect(foundPage?.url()).toBe(testUrl)
|
||||
|
||||
// Verify execution works again
|
||||
const sum2 = await foundPage?.evaluate(() => 2 + 2)
|
||||
expect(sum2).toBe(4)
|
||||
|
||||
await directBrowser.close()
|
||||
await page.close()
|
||||
})
|
||||
|
||||
@@ -391,6 +401,10 @@ describe('MCP Server Tests', () => {
|
||||
expect(foundPage).toBeDefined()
|
||||
expect(foundPage?.url()).toBe(testUrl)
|
||||
|
||||
// Verify execution works
|
||||
const sum1 = await foundPage?.evaluate(() => 10 + 20)
|
||||
expect(sum1).toBe(30)
|
||||
|
||||
// 4. Disable extension
|
||||
await serviceWorker.evaluate(async () => {
|
||||
await globalThis.toggleExtensionForActiveTab()
|
||||
@@ -420,6 +434,10 @@ describe('MCP Server Tests', () => {
|
||||
expect(foundPage).toBeDefined()
|
||||
expect(foundPage?.url()).toBe(testUrl)
|
||||
|
||||
// Verify execution works again
|
||||
const sum2 = await foundPage?.evaluate(() => 30 + 40)
|
||||
expect(sum2).toBe(70)
|
||||
|
||||
await page.close()
|
||||
await directBrowser.close()
|
||||
})
|
||||
@@ -444,12 +462,18 @@ describe('MCP Server Tests', () => {
|
||||
const connectedPage = directBrowser.contexts()[0].pages().find(p => p.url() === initialUrl)
|
||||
expect(connectedPage).toBeDefined()
|
||||
|
||||
// Verify execution
|
||||
expect(await connectedPage?.evaluate(() => 1 + 1)).toBe(2)
|
||||
|
||||
// 4. Reload
|
||||
// We use a loop to check if it's still connected because reload might cause temporary disconnect/reconnect events
|
||||
// that Playwright handles natively if the session ID stays valid.
|
||||
await connectedPage?.reload()
|
||||
await connectedPage?.waitForLoadState('networkidle')
|
||||
expect(await connectedPage?.title()).toBe('Example Domain')
|
||||
|
||||
// Verify execution after reload
|
||||
expect(await connectedPage?.evaluate(() => 2 + 2)).toBe(4)
|
||||
|
||||
// 5. Navigate to new URL
|
||||
const newUrl = 'https://news.ycombinator.com/'
|
||||
@@ -459,7 +483,10 @@ describe('MCP Server Tests', () => {
|
||||
expect(connectedPage?.url()).toBe(newUrl)
|
||||
expect(await connectedPage?.title()).toContain('Hacker News')
|
||||
|
||||
// Verify execution after navigation
|
||||
expect(await connectedPage?.evaluate(() => 3 + 3)).toBe(6)
|
||||
|
||||
await directBrowser.close()
|
||||
await page.close()
|
||||
})
|
||||
|
||||
@@ -537,6 +564,100 @@ describe('MCP Server Tests', () => {
|
||||
]
|
||||
`)
|
||||
|
||||
// Verify execution on both pages
|
||||
const pageA_CDP = pages.find(p => p.url().includes('tab-a'))
|
||||
const pageB_CDP = pages.find(p => p.url().includes('tab-b'))
|
||||
|
||||
expect(await pageA_CDP?.evaluate(() => 10 + 10)).toBe(20)
|
||||
expect(await pageB_CDP?.evaluate(() => 20 + 20)).toBe(40)
|
||||
|
||||
await browser.close()
|
||||
await pageA.close()
|
||||
await pageB.close()
|
||||
})
|
||||
|
||||
it('should support multiple concurrent tabs', async () => {
|
||||
if (!browserContext) throw new Error('Browser not initialized')
|
||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// Tab A
|
||||
const pageA = await browserContext.newPage()
|
||||
await pageA.goto('https://example.com/tab-a')
|
||||
await pageA.bringToFront()
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
await serviceWorker.evaluate(async () => {
|
||||
await globalThis.toggleExtensionForActiveTab()
|
||||
})
|
||||
|
||||
// Tab B
|
||||
const pageB = await browserContext.newPage()
|
||||
await pageB.goto('https://example.com/tab-b')
|
||||
await pageB.bringToFront()
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
await serviceWorker.evaluate(async () => {
|
||||
await globalThis.toggleExtensionForActiveTab()
|
||||
})
|
||||
|
||||
// Get target IDs for both
|
||||
const targetIds = await serviceWorker.evaluate(async () => {
|
||||
const state = globalThis.getExtensionState()
|
||||
const chrome = globalThis.chrome
|
||||
const tabs = await chrome.tabs.query({})
|
||||
const tabA = tabs.find((t: any) => t.url?.includes('tab-a'))
|
||||
const tabB = tabs.find((t: any) => t.url?.includes('tab-b'))
|
||||
return {
|
||||
idA: state.connectedTabs.get(tabA?.id)?.targetId,
|
||||
idB: state.connectedTabs.get(tabB?.id)?.targetId
|
||||
}
|
||||
})
|
||||
|
||||
expect(targetIds).toMatchInlineSnapshot({
|
||||
idA: expect.any(String),
|
||||
idB: expect.any(String)
|
||||
}, `
|
||||
{
|
||||
"idA": Any<String>,
|
||||
"idB": Any<String>,
|
||||
}
|
||||
`)
|
||||
expect(targetIds.idA).not.toBe(targetIds.idB)
|
||||
|
||||
// Verify independent connections
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
|
||||
const pages = browser.contexts()[0].pages()
|
||||
|
||||
const results = await Promise.all(pages.map(async (p) => ({
|
||||
url: p.url(),
|
||||
title: await p.title()
|
||||
})))
|
||||
|
||||
expect(results).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"title": "",
|
||||
"url": "about:blank",
|
||||
},
|
||||
{
|
||||
"title": "Example Domain",
|
||||
"url": "https://example.com/tab-a",
|
||||
},
|
||||
{
|
||||
"title": "Example Domain",
|
||||
"url": "https://example.com/tab-b",
|
||||
},
|
||||
]
|
||||
`)
|
||||
|
||||
// Verify execution on both pages
|
||||
const pageA_CDP = pages.find(p => p.url().includes('tab-a'))
|
||||
const pageB_CDP = pages.find(p => p.url().includes('tab-b'))
|
||||
|
||||
expect(await pageA_CDP?.evaluate(() => 10 + 10)).toBe(20)
|
||||
expect(await pageB_CDP?.evaluate(() => 20 + 20)).toBe(40)
|
||||
|
||||
await browser.close()
|
||||
await pageA.close()
|
||||
await pageB.close()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user