blob: 2975abd89ac745fb46bc6c2d6603ca51cc7c8433 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import sinon from 'sinon';
import type * as Puppeteer from '../../third_party/puppeteer/puppeteer.js';
import * as ProtocolClient from './protocol_client.js';
// eslint-disable-next-line @devtools/es-modules-import
import {PuppeteerDevToolsConnection} from './PuppeteerDevToolsConnection.js';
class MockPuppeteerSession {
#id: string;
#connection: MockPuppeteerConnection;
#listeners = new Map<string, Set<(...args: unknown[]) => void>>();
constructor(id: string, connection: MockPuppeteerConnection) {
this.#id = id;
this.#connection = connection;
}
id(): string {
return this.#id;
}
connection(): MockPuppeteerConnection {
return this.#connection;
}
on(event: string, handler: (...args: unknown[]) => void): void {
let handlers = this.#listeners.get(event);
if (!handlers) {
handlers = new Set();
this.#listeners.set(event, handlers);
}
handlers.add(handler);
}
off(event: string, handler: (...args: unknown[]) => void): void {
const handlers = this.#listeners.get(event);
if (handlers) {
handlers.delete(handler);
}
}
emit(event: string, ...args: unknown[]): void {
const handlers = this.#listeners.get(event);
if (handlers) {
for (const handler of handlers) {
handler(...args);
}
}
if (event !== '*') {
const wildcardHandlers = this.#listeners.get('*');
if (wildcardHandlers) {
for (const handler of wildcardHandlers) {
handler(event, ...args);
}
}
}
}
getListenerCount(event: string): number {
return this.#listeners.get(event)?.size ?? 0;
}
send = sinon.stub();
}
class MockPuppeteerConnection {
#sessions = new Map<string, MockPuppeteerSession>();
addSession(session: MockPuppeteerSession): void {
this.#sessions.set(session.id(), session);
}
session(sessionId: string): MockPuppeteerSession|undefined {
return this.#sessions.get(sessionId);
}
}
function createMockObserver(): ProtocolClient.CDPConnection.CDPConnectionObserver&{
onEvent: sinon.SinonStub, onDisconnect: sinon.SinonStub,
}
{
return {
onEvent: sinon.stub(),
onDisconnect: sinon.stub(),
};
}
describe('PuppeteerDevToolsConnection', () => {
let connection: MockPuppeteerConnection;
let rootSession: MockPuppeteerSession;
let cdpConnection: PuppeteerDevToolsConnection;
beforeEach(() => {
connection = new MockPuppeteerConnection();
rootSession = new MockPuppeteerSession('root', connection);
cdpConnection = new PuppeteerDevToolsConnection(rootSession as unknown as Puppeteer.CDPSession);
});
it('forwards CDP events from child sessions to observers', () => {
const observer = createMockObserver();
cdpConnection.observe(observer);
const child = new MockPuppeteerSession('child-1', connection);
connection.addSession(child);
rootSession.emit('sessionattached', child);
assert.strictEqual(child.getListenerCount('*'), 1);
child.emit('Page.loadEventFired', {timestamp: 123});
sinon.assert.calledOnceWithExactly(observer.onEvent, {
method: 'Page.loadEventFired',
sessionId: 'child-1',
params: {timestamp: 123},
});
});
it('forwards CDP events from root session to observers', () => {
const observer = createMockObserver();
cdpConnection.observe(observer);
rootSession.emit('Page.loadEventFired', {timestamp: 456});
sinon.assert.calledOnceWithExactly(observer.onEvent, {
method: 'Page.loadEventFired',
sessionId: 'root',
params: {timestamp: 456},
});
});
it('does not forward sessionattached or sessiondetached as CDP events to observers', () => {
const observer = createMockObserver();
cdpConnection.observe(observer);
const child = new MockPuppeteerSession('child-1', connection);
connection.addSession(child);
rootSession.emit('sessionattached', child);
rootSession.emit('sessiondetached', child);
sinon.assert.notCalled(observer.onEvent);
});
it('stops forwarding events when child session detaches', () => {
const child = new MockPuppeteerSession('child-1', connection);
connection.addSession(child);
rootSession.emit('sessionattached', child);
assert.strictEqual(child.getListenerCount('*'), 1);
rootSession.emit('sessiondetached', child);
assert.strictEqual(child.getListenerCount('*'), 0);
});
it('sends command to a known child session', async () => {
const child = new MockPuppeteerSession('child-1', connection);
connection.addSession(child);
rootSession.emit('sessionattached', child);
const mockVersionResult = {
protocolVersion: '1.3',
product: 'chrome',
revision: '123',
userAgent: 'test',
jsVersion: '10',
};
child.send.resolves(mockVersionResult);
const response = await cdpConnection.send(
'Browser.getVersion',
undefined,
'child-1',
);
sinon.assert.calledOnceWithExactly(child.send, 'Browser.getVersion', undefined);
assert.deepEqual(response, {result: mockVersionResult});
});
it('returns error response when command rejects', async () => {
const child = new MockPuppeteerSession('child-1', connection);
connection.addSession(child);
rootSession.emit('sessionattached', child);
child.send.rejects(new Error('Something went wrong'));
const response = await cdpConnection.send(
'Browser.getVersion',
undefined,
'child-1',
);
assert.isTrue('error' in response);
if ('error' in response) {
assert.strictEqual(response.error.code, ProtocolClient.CDPConnection.CDPErrorStatus.SERVER_ERROR);
assert.strictEqual(response.error.message, 'Something went wrong');
}
});
it('returns SESSION_NOT_FOUND error when sending to unknown session ID', async () => {
const response = await cdpConnection.send(
'Browser.getVersion',
undefined,
'unknown-session',
);
assert.isTrue('error' in response);
if ('error' in response) {
assert.strictEqual(response.error.code, ProtocolClient.CDPConnection.CDPErrorStatus.SESSION_NOT_FOUND);
assert.strictEqual(response.error.message, 'Unknown session unknown-session');
}
});
it('throws synchronously when attempting to send on root session', () => {
assert.throws(() => {
void cdpConnection.send(
'Browser.getVersion',
undefined,
undefined,
);
}, 'Attempting to send on the root session. This must not happen');
});
it('removes event listeners and notifies observers on dispose', () => {
const observer = createMockObserver();
cdpConnection.observe(observer);
const child1 = new MockPuppeteerSession('child-1', connection);
const child2 = new MockPuppeteerSession('child-2', connection);
connection.addSession(child1);
connection.addSession(child2);
rootSession.emit('sessionattached', child1);
rootSession.emit('sessionattached', child2);
assert.strictEqual(rootSession.getListenerCount('sessionattached'), 1);
assert.strictEqual(rootSession.getListenerCount('sessiondetached'), 1);
assert.strictEqual(rootSession.getListenerCount('*'), 1);
assert.strictEqual(child1.getListenerCount('*'), 1);
assert.strictEqual(child2.getListenerCount('*'), 1);
cdpConnection.dispose('Test teardown');
assert.strictEqual(rootSession.getListenerCount('sessionattached'), 0);
assert.strictEqual(rootSession.getListenerCount('sessiondetached'), 0);
assert.strictEqual(rootSession.getListenerCount('*'), 0);
assert.strictEqual(child1.getListenerCount('*'), 0);
assert.strictEqual(child2.getListenerCount('*'), 0);
sinon.assert.calledOnceWithExactly(observer.onDisconnect, 'Test teardown');
});
});