Skip to content

Commit 46e2caf

Browse files
committed
test: add E2E tests for SessionManager multi-session lifecycle
17 tests covering session creation, retrieval, listing, closing, parallel session isolation, per-session mutex serialization, auto-purge on browser disconnect, and shutdown rejection.
1 parent bd57e47 commit 46e2caf

1 file changed

Lines changed: 295 additions & 0 deletions

File tree

tests/SessionManager.test.ts

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
import {describe, it, afterEach} from 'node:test';
9+
10+
import {SessionManager} from '../src/SessionManager.js';
11+
import type {SessionInfo} from '../src/SessionManager.js';
12+
13+
const contextOptions = {
14+
experimentalDevToolsDebugging: false,
15+
performanceCrux: false,
16+
};
17+
18+
describe('SessionManager', () => {
19+
const managers: SessionManager[] = [];
20+
21+
function createManager(): SessionManager {
22+
const m = new SessionManager(contextOptions);
23+
managers.push(m);
24+
return m;
25+
}
26+
27+
afterEach(async () => {
28+
for (const m of managers) {
29+
try {
30+
await m.closeAllSessions();
31+
} catch {
32+
// ignore
33+
}
34+
}
35+
managers.length = 0;
36+
});
37+
38+
describe('createSession', () => {
39+
it('creates a session and returns session info', async () => {
40+
const manager = createManager();
41+
const session = await manager.createSession({headless: true});
42+
43+
assert.ok(session.sessionId, 'sessionId should exist');
44+
assert.strictEqual(
45+
session.sessionId.length,
46+
8,
47+
'sessionId should be 8 chars',
48+
);
49+
assert.ok(session.browser, 'browser should exist');
50+
assert.ok(session.browser.connected, 'browser should be connected');
51+
assert.ok(session.context, 'context should exist');
52+
assert.ok(session.mutex, 'mutex should exist');
53+
assert.ok(session.createdAt instanceof Date, 'createdAt should be Date');
54+
assert.strictEqual(manager.sessionCount, 1);
55+
});
56+
57+
it('assigns label when provided', async () => {
58+
const manager = createManager();
59+
const session = await manager.createSession({
60+
headless: true,
61+
label: 'test-label',
62+
});
63+
64+
assert.strictEqual(session.label, 'test-label');
65+
});
66+
67+
it('generates unique session IDs', async () => {
68+
const manager = createManager();
69+
const session1 = await manager.createSession({headless: true});
70+
const session2 = await manager.createSession({headless: true});
71+
72+
assert.notStrictEqual(
73+
session1.sessionId,
74+
session2.sessionId,
75+
'session IDs must be unique',
76+
);
77+
assert.strictEqual(manager.sessionCount, 2);
78+
});
79+
80+
it('rejects creation when shutting down', async () => {
81+
const manager = createManager();
82+
await manager.closeAllSessions();
83+
84+
await assert.rejects(
85+
() => manager.createSession({headless: true}),
86+
{message: /shutting down/i},
87+
);
88+
});
89+
});
90+
91+
describe('getSession', () => {
92+
it('returns session by ID', async () => {
93+
const manager = createManager();
94+
const session = await manager.createSession({headless: true});
95+
const retrieved = manager.getSession(session.sessionId);
96+
97+
assert.strictEqual(retrieved.sessionId, session.sessionId);
98+
assert.strictEqual(retrieved.browser, session.browser);
99+
assert.strictEqual(retrieved.context, session.context);
100+
});
101+
102+
it('throws for unknown session ID', () => {
103+
const manager = createManager();
104+
105+
assert.throws(
106+
() => manager.getSession('deadbeef'),
107+
{message: /not found/i},
108+
);
109+
});
110+
111+
it('throws and purges for disconnected browser', async () => {
112+
const manager = createManager();
113+
const session = await manager.createSession({headless: true});
114+
115+
await session.browser.close();
116+
await new Promise(resolve => setTimeout(resolve, 100));
117+
118+
assert.throws(
119+
() => manager.getSession(session.sessionId),
120+
{message: /not found|disconnected/i},
121+
);
122+
assert.strictEqual(manager.sessionCount, 0);
123+
});
124+
});
125+
126+
describe('listSessions', () => {
127+
it('returns empty list when no sessions', () => {
128+
const manager = createManager();
129+
const list = manager.listSessions();
130+
assert.deepStrictEqual(list, []);
131+
});
132+
133+
it('returns all active sessions', async () => {
134+
const manager = createManager();
135+
await manager.createSession({headless: true, label: 'one'});
136+
await manager.createSession({headless: true, label: 'two'});
137+
138+
const list = manager.listSessions();
139+
assert.strictEqual(list.length, 2);
140+
141+
const labels = list.map(s => s.label).sort();
142+
assert.deepStrictEqual(labels, ['one', 'two']);
143+
for (const s of list) {
144+
assert.ok(s.sessionId);
145+
assert.ok(s.createdAt);
146+
assert.strictEqual(s.connected, true);
147+
}
148+
});
149+
});
150+
151+
describe('closeSession', () => {
152+
it('closes and removes session', async () => {
153+
const manager = createManager();
154+
const session = await manager.createSession({headless: true});
155+
156+
assert.strictEqual(manager.sessionCount, 1);
157+
await manager.closeSession(session.sessionId);
158+
assert.strictEqual(manager.sessionCount, 0);
159+
});
160+
161+
it('throws for unknown session ID', async () => {
162+
const manager = createManager();
163+
164+
await assert.rejects(
165+
() => manager.closeSession('deadbeef'),
166+
{message: /not found/i},
167+
);
168+
});
169+
170+
it('handles already-disconnected browser gracefully', async () => {
171+
const manager = createManager();
172+
const session = await manager.createSession({headless: true});
173+
const id = session.sessionId;
174+
175+
await session.browser.close();
176+
await new Promise(resolve => setTimeout(resolve, 100));
177+
178+
try {
179+
await manager.closeSession(id);
180+
} catch {
181+
// auto-purge may have already removed it — that's the expected behavior
182+
}
183+
assert.strictEqual(manager.sessionCount, 0);
184+
});
185+
});
186+
187+
describe('closeAllSessions', () => {
188+
it('closes all sessions', async () => {
189+
const manager = createManager();
190+
await manager.createSession({headless: true});
191+
await manager.createSession({headless: true});
192+
await manager.createSession({headless: true});
193+
194+
assert.strictEqual(manager.sessionCount, 3);
195+
await manager.closeAllSessions();
196+
assert.strictEqual(manager.sessionCount, 0);
197+
});
198+
});
199+
200+
describe('parallel sessions', () => {
201+
it('two sessions can navigate to different URLs independently', async () => {
202+
const manager = createManager();
203+
const session1 = await manager.createSession({headless: true});
204+
const session2 = await manager.createSession({headless: true});
205+
206+
const page1 = session1.context.getSelectedPage();
207+
const page2 = session2.context.getSelectedPage();
208+
209+
await Promise.all([
210+
page1.goto('data:text/html,<h1>Session One</h1>'),
211+
page2.goto('data:text/html,<h1>Session Two</h1>'),
212+
]);
213+
214+
const title1 = await page1.evaluate(
215+
() => document.querySelector('h1')?.textContent,
216+
);
217+
const title2 = await page2.evaluate(
218+
() => document.querySelector('h1')?.textContent,
219+
);
220+
221+
assert.strictEqual(title1, 'Session One');
222+
assert.strictEqual(title2, 'Session Two');
223+
});
224+
225+
it('closing one session does not affect another', async () => {
226+
const manager = createManager();
227+
const session1 = await manager.createSession({headless: true});
228+
const session2 = await manager.createSession({headless: true});
229+
230+
await manager.closeSession(session1.sessionId);
231+
232+
assert.strictEqual(manager.sessionCount, 1);
233+
assert.ok(session2.browser.connected, 'session2 browser should still be connected');
234+
235+
const page2 = session2.context.getSelectedPage();
236+
await page2.goto('data:text/html,<p>Still alive</p>');
237+
const text = await page2.evaluate(
238+
() => document.querySelector('p')?.textContent,
239+
);
240+
assert.strictEqual(text, 'Still alive');
241+
});
242+
243+
it('per-session mutex serializes within session but allows cross-session parallelism', async () => {
244+
const manager = createManager();
245+
const session1 = await manager.createSession({headless: true});
246+
const session2 = await manager.createSession({headless: true});
247+
248+
const order: string[] = [];
249+
250+
const guard1 = await session1.mutex.acquire();
251+
252+
const session1SecondAcquire = session1.mutex.acquire().then(g => {
253+
order.push('s1-second');
254+
g.dispose();
255+
});
256+
257+
const guard2 = await session2.mutex.acquire();
258+
order.push('s2-first');
259+
guard2.dispose();
260+
261+
guard1.dispose();
262+
await session1SecondAcquire;
263+
264+
assert.strictEqual(order[0], 's2-first', 'session2 should acquire before session1 second acquire');
265+
assert.strictEqual(order[1], 's1-second');
266+
});
267+
});
268+
269+
describe('auto-purge on disconnect', () => {
270+
it('removes session when browser disconnects unexpectedly', async () => {
271+
const manager = createManager();
272+
const session = await manager.createSession({headless: true});
273+
274+
assert.strictEqual(manager.sessionCount, 1);
275+
276+
const browserProcess = session.browser.process();
277+
if (browserProcess) {
278+
browserProcess.kill('SIGKILL');
279+
await new Promise<void>(resolve => {
280+
session.browser.on('disconnected', () => resolve());
281+
});
282+
} else {
283+
await session.browser.close();
284+
}
285+
286+
await new Promise(resolve => setTimeout(resolve, 100));
287+
288+
assert.strictEqual(
289+
manager.sessionCount,
290+
0,
291+
'session should be auto-purged after disconnect',
292+
);
293+
});
294+
});
295+
});

0 commit comments

Comments
 (0)