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`);