diff --git a/packages/playwright-core/src/tools/backend/devtools.ts b/packages/playwright-core/src/tools/backend/devtools.ts index 300793b84d903..56fe53258ef88 100644 --- a/packages/playwright-core/src/tools/backend/devtools.ts +++ b/packages/playwright-core/src/tools/backend/devtools.ts @@ -48,6 +48,10 @@ const resume = defineTool({ } }; browserContext.debugger.on('pausedstatechanged', listener); + browserContext.once('close', () => { + browserContext.debugger.off('pausedstatechanged', listener); + resolve(); + }); }); if (params.location) { diff --git a/tests/mcp/devtools.spec.ts b/tests/mcp/devtools.spec.ts index dc665f6d518fc..c7350f376f9f8 100644 --- a/tests/mcp/devtools.spec.ts +++ b/tests/mcp/devtools.spec.ts @@ -93,6 +93,29 @@ test('browser_hide_highlight', async ({ boundBrowser, startClient }) => { await expect(page.locator('x-pw-highlight')).toHaveCount(0); }); +test('browser_resume completes when context closes', async ({ boundBrowser, startClient }) => { + const page = await boundBrowser.newPage(); + await page.setContent(``); + const context = boundBrowser.contexts()[0]; + + // Pause the debugger before the next action. + await context.debugger.requestPause(); + const clickPromise = page.click('button'); + await new Promise(resolve => context.debugger.once('pausedstatechanged', resolve)); + expect(context.debugger.pausedDetails()).toBeTruthy(); + + // browser_resume without step or location resumes execution and + // waits for either the next pause or context close. Closing the + // context after resume simulates a test finishing with no further + // breakpoints. Before the fix, this would hang indefinitely. + const { client } = await startClient({ args: [`--endpoint=default`] }); + const resumePromise = client.callTool({ name: 'browser_resume' }); + + await clickPromise; + await context.close(); + await resumePromise; +}); + test('browser_hide_highlight all', async ({ boundBrowser, startClient }) => { const page = await boundBrowser.newPage(); await page.setContent(`Go`);