Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 17 additions & 100 deletions tests/mcp/http.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,27 +526,23 @@ async function keepBusy(client: Client, ms: number) {
}
}

test('http transport shared context: idle client keeps its state while another client is active', async ({ serverEndpoint, server }) => {
test('http transport shared context: one idle timer across clients', async ({ serverEndpoint, server }) => {
const { url, stderr } = await serverEndpoint({ args: ['--shared-browser-context', '--timeout-idle=500'] });
const client1 = await connectClient(url, 'test1');
await client1.client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});

// The second client keeps working well past the idle timeout.
// While the second client keeps working past the timeout, the idle client keeps its state.
const client2 = await connectClient(url, 'test2');
await keepBusy(client2.client, 1200);

// The idle client was neither disconnected nor closed.
expect(formatLog(stderr())).toEqual({
'create browser (persistent)': 1,
'connect to shared browser': 2,
'create http session': 2,
'create context': 2,
});

// It resumes with its page and without a notice.
const response = await client1.client.callTool({
name: 'browser_snapshot',
arguments: {},
Expand All @@ -556,121 +552,42 @@ test('http transport shared context: idle client keeps its state while another c
});
expect(response.content[0].text).not.toContain('inactivity');

await client1.close();
await client2.close();
await expect.poll(() => formatLog(stderr())).toEqual({
'create browser (persistent)': 1,
'connect to shared browser': 2,
'disconnect from shared browser': 1,
'create http session': 2,
'delete http session': 2,
'create context': 2,
'close browser': 1,
});
});

test('http transport shared context: shared browser closes when the last active client leaves', async ({ serverEndpoint, server }) => {
const { url, stderr } = await serverEndpoint({ args: ['--shared-browser-context', '--timeout-idle=500'] });
const client1 = await connectClient(url, 'test1');
await client1.client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});

// The second client keeps the shared browser alive past the idle timeout, then leaves.
const client2 = await connectClient(url, 'test2');
await keepBusy(client2.client, 1200);
await client2.close();

// Nobody has been active for the timeout, so the browser closes under the remaining client.
// Once every client has been idle for the timeout, the shared browser closes.
await expect.poll(() => formatLog(stderr())).toEqual({
'create browser (persistent)': 1,
'connect to shared browser': 2,
'disconnect from shared browser': 1,
'create http session': 2,
'delete http session': 1,
'create context': 2,
'close browser': 1,
});

// The idle client relaunches the browser on its next call and is told once.
const response = await client1.client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(response.content[0].text).toContain('browser was closed after 500ms of inactivity');
expect(response).toHaveResponse({
snapshot: expect.stringContaining(`Hello, world!`),
});

await client1.close();
await expect.poll(() => formatLog(stderr())).toEqual({
'create browser (persistent)': 2,
'connect to shared browser': 3,
'disconnect from shared browser': 1,
'create http session': 2,
'delete http session': 2,
'create context': 3,
'close browser': 2,
});
});

test('http transport shared context: shared browser closes once every client is idle', async ({ serverEndpoint, server }) => {
const { url, stderr } = await serverEndpoint({ args: ['--shared-browser-context', '--timeout-idle=500'] });
const client1 = await connectClient(url, 'test1');
const client2 = await connectClient(url, 'test2');
// Each client relaunches on its next call and is told once.
for (const { client } of [client1, client2]) {
await client.callTool({
const response = await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(response.content[0].text).toContain('browser was closed after 500ms of inactivity');
expect(response).toHaveResponse({
snapshot: expect.stringContaining(`Hello, world!`),
});
}

// The shared browser closes once both clients have been idle for the timeout, dropping both backends.
await expect.poll(() => formatLog(stderr())).toEqual({
'create browser (persistent)': 1,
'connect to shared browser': 2,
'disconnect from shared browser': 1,
'create http session': 2,
'create context': 2,
'close browser': 1,
});

// The next call relaunches the shared browser and says so.
const response1 = await client1.client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(response1.content[0].text).toContain('browser was closed after 500ms of inactivity');
expect(response1).toHaveResponse({
snapshot: expect.stringContaining(`Hello, world!`),
});
expect(formatLog(stderr())).toEqual({
'create browser (persistent)': 2,
'connect to shared browser': 3,
'disconnect from shared browser': 1,
'create http session': 2,
'create context': 3,
'close browser': 1,
});

// The other client is told once as well.
const response2 = await client2.client.callTool({
name: 'browser_snapshot',
arguments: {},
});
expect(response2.content[0].text).toContain('browser was closed after 500ms of inactivity');
expect(response2).toHaveResponse({
inlineSnapshot: expect.stringContaining(`Hello, world!`),
});

for (const { client } of [client1, client2]) {
const response = await client.callTool({
name: 'browser_snapshot',
arguments: {},
});
expect(response.content[0].text).not.toContain('inactivity');
}
expect(formatLog(stderr())).toEqual({
'create browser (persistent)': 2,
'connect to shared browser': 4,
'disconnect from shared browser': 1,
'create http session': 2,
'create context': 4,
'close browser': 1,
});

await client1.close();
await client2.close();
Expand Down
62 changes: 0 additions & 62 deletions tests/mcp/idle-timeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,68 +61,6 @@ test('closes the browser after the idle timeout and relaunches it on the next ca
});
});

test('does not close the browser while a tool call is running', async ({ startClient, server }) => {
const { client, stderr } = await startClient({
args: ['--timeout-idle=500'],
env: { DEBUG: 'pw:mcp:test' },
});

await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});

// The wait outlasts the idle timeout, which only starts once the call completes.
expect(await client.callTool({
name: 'browser_wait_for',
arguments: { time: 1 },
})).toHaveResponse({
code: `await new Promise(f => setTimeout(f, 1 * 1000));`,
});

expect(formatLog(stderr())).toEqual({
'create browser (persistent)': 1,
'create context': 1,
});
});

test('isolated context loses in-memory state on idle close', async ({ startClient, server }) => {
server.setContent('/', `
<body>
</body>
<script>
document.body.textContent = localStorage.getItem('test') ? 'Storage: YES' : 'Storage: NO';
localStorage.setItem('test', 'test');
</script>
`, 'text/html');

const { client, stderr } = await startClient({
args: ['--isolated', '--timeout-idle=500'],
env: { DEBUG: 'pw:mcp:test' },
});

expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
snapshot: expect.stringContaining(`Storage: NO`),
});

await expect.poll(() => formatLog(stderr())).toEqual({
'create browser (isolated)': 1,
'connect to shared browser': 1,
'create context': 1,
'close browser': 1,
});

expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
snapshot: expect.stringContaining(`Storage: NO`),
});
});

test('cdp endpoint only disconnects on idle and reconnects to the same pages', async ({ cdpServer, startClient, server }) => {
const browserContext = await cdpServer.start();
const { client, stderr } = await startClient({
Expand Down