adding support for multiple tabs

This commit is contained in:
Tommy D. Rossi
2025-11-14 15:23:47 +01:00
parent 50b453a64c
commit 6998031438
5 changed files with 421 additions and 182 deletions
+3 -1
View File
@@ -7,6 +7,8 @@ async function main() {
const contexts = browser.contexts();
console.log(`Found ${contexts.length} browser context(s)`);
// Sleep 200 ms
await new Promise(resolve => setTimeout(resolve, 200));
for (const context of contexts) {
const pages = context.pages();
console.log(`Context has ${pages.length} page(s):`);
@@ -26,6 +28,7 @@ async function main() {
console.log(`Browser log: [${msg.type()}] ${msg.text()}`);
});
console.log(`running eval`)
// Evaluate a sum in the browser and log something from inside the browser context
const sumResult = await page.evaluate(() => {
console.log('Logging from inside browser context!');
@@ -35,7 +38,6 @@ async function main() {
}
}
await browser.close();
}
main()
+100 -45
View File
@@ -40,6 +40,12 @@ import type { CDPCommand, CDPResponse, CDPEvent, Protocol } from '../cdp-types.j
const debugLogger = debug('pw:mcp:relay');
interface ConnectedTarget {
sessionId: string;
targetId: string;
targetInfo: Protocol.Target.TargetInfo;
}
export class CDPRelayServer {
private _wsHost: string;
@@ -48,12 +54,7 @@ export class CDPRelayServer {
private _wss: WebSocketServer;
private _playwrightConnection: WebSocket | null = null;
private _extensionConnection: ExtensionConnection | null = null;
private _connectedTabInfo: {
targetInfo: any;
// Page sessionId that should be used by this connection.
sessionId: string;
} | undefined;
private _nextSessionId: number = 1;
private _connectedTargets: Map<string, ConnectedTarget> = new Map();
private _extensionConnectionPromise!: ManualPromise<void>;
constructor(server: http.Server, ) {
@@ -119,8 +120,7 @@ export class CDPRelayServer {
if (this._playwrightConnection !== ws)
return;
this._playwrightConnection = null;
this._closeExtensionConnection('Playwright client disconnected');
debugLogger('Playwright WebSocket closed');
debugLogger('Playwright WebSocket closed - extension stays connected');
});
ws.on('error', error => {
debugLogger('Playwright WebSocket error:', error);
@@ -135,7 +135,7 @@ export class CDPRelayServer {
}
private _resetExtensionConnection() {
this._connectedTabInfo = undefined;
this._connectedTargets.clear();
this._extensionConnection = null;
this._extensionConnectionPromise = new ManualPromise();
void this._extensionConnectionPromise.catch(logUnhandledError);
@@ -167,23 +167,52 @@ export class CDPRelayServer {
private _handleExtensionMessage(message: ExtensionEventMessage) {
if (message.method === 'forwardCDPEvent') {
const sessionId = message.params.sessionId || this._connectedTabInfo?.sessionId;
this._sendToPlaywright({
sessionId,
method: message.params.method,
params: message.params.params
} as CDPEvent);
const { method, params, sessionId } = message.params;
if (method === 'Target.attachedToTarget') {
const targetParams = params as Protocol.Target.AttachedToTargetEvent;
this._connectedTargets.set(targetParams.sessionId, {
sessionId: targetParams.sessionId,
targetId: targetParams.targetInfo.targetId,
targetInfo: targetParams.targetInfo
});
debugLogger('\x1b[33m← Extension:\x1b[0m', `Target.attachedToTarget sessionId=${targetParams.sessionId}, targetId=${targetParams.targetInfo.targetId}`);
this._sendToPlaywright({
method: 'Target.attachedToTarget',
params: targetParams
} as CDPEvent);
} else if (method === 'Target.detachedFromTarget') {
const detachParams = params as Protocol.Target.DetachedFromTargetEvent;
this._connectedTargets.delete(detachParams.sessionId);
debugLogger('\x1b[33m← Extension:\x1b[0m', `Target.detachedFromTarget sessionId=${detachParams.sessionId}`);
this._sendToPlaywright({
method: 'Target.detachedFromTarget',
params: detachParams
} as CDPEvent);
} else {
this._sendToPlaywright({
sessionId,
method,
params
} as CDPEvent);
}
}
}
private async _handlePlaywrightMessage(message: CDPCommand): Promise<void> {
debugLogger('← Playwright:', `${message.method} (id=${message.id})`);
debugLogger('\x1b[36m← Playwright:\x1b[0m', `${message.method} (id=${message.id})`);
const { id, sessionId, method, params } = message;
try {
const result = await this._handleCDPCommand(method, params, sessionId);
this._sendToPlaywright({ id, sessionId, result });
} catch (e) {
debugLogger('Error in the extension:', e);
debugLogger('\x1b[31mError in the extension:\x1b[0m', e);
this._sendToPlaywright({
id,
sessionId,
@@ -207,33 +236,61 @@ export class CDPRelayServer {
return { };
}
case 'Target.setAutoAttach': {
// Forward child session handling.
if (sessionId)
if (sessionId) {
break;
// Simulate auto-attach behavior with real target info
if (!this._extensionConnection)
throw new Error('Extension not connected. Please ensure the Chrome extension is installed and connected to the extension endpoint before connecting Playwright.');
const { targetInfo } = await this._extensionConnection.send({ method: 'attachToTab' });
this._connectedTabInfo = {
targetInfo,
sessionId: `pw-tab-${this._nextSessionId++}`,
};
debugLogger('Simulating auto-attach');
this._sendToPlaywright({
method: 'Target.attachedToTarget',
params: {
sessionId: this._connectedTabInfo.sessionId,
targetInfo: {
...this._connectedTabInfo.targetInfo,
attached: true,
},
waitingForDebugger: false
}
} satisfies CDPEvent);
return { };
}
debugLogger('Target.setAutoAttach received (manual attach mode)');
debugLogger('Sending Target.attachedToTarget events for existing targets:', this._connectedTargets.size);
for (const target of this._connectedTargets.values()) {
debugLogger('Sending Target.attachedToTarget for sessionId:', target.sessionId, 'targetId:', target.targetId);
this._sendToPlaywright({
method: 'Target.attachedToTarget',
params: {
sessionId: target.sessionId,
targetInfo: {
...target.targetInfo,
attached: true
},
waitingForDebugger: false
}
} as CDPEvent);
}
return {};
}
case 'Target.getTargetInfo': {
return this._connectedTabInfo?.targetInfo;
const targetId = params?.targetId;
if (targetId) {
for (const target of this._connectedTargets.values()) {
if (target.targetId === targetId) {
return { targetInfo: target.targetInfo };
}
}
}
if (sessionId) {
const target = this._connectedTargets.get(sessionId);
if (target) {
return { targetInfo: target.targetInfo };
}
}
const firstTarget = this._connectedTargets.values().next().value;
return { targetInfo: firstTarget?.targetInfo };
}
case 'Target.getTargets': {
return {
targetInfos: Array.from(this._connectedTargets.values()).map(t => ({
...t.targetInfo,
attached: true,
}))
};
}
case 'Target.closeTarget': {
break;
}
}
return await this._forwardToExtension(method, params, sessionId);
@@ -242,9 +299,7 @@ export class CDPRelayServer {
private async _forwardToExtension(method: string, params: any, sessionId: string | undefined): Promise<any> {
if (!this._extensionConnection)
throw new Error('Extension not connected');
// Top level sessionId is only passed between the relay and the client.
if (this._connectedTabInfo?.sessionId === sessionId)
sessionId = undefined;
return await this._extensionConnection.send({
method: 'forwardCDPCommand',
params: { sessionId, method, params }
@@ -255,7 +310,7 @@ export class CDPRelayServer {
const logMessage = 'method' in message && message.method
? message.method
: `response(id=${'id' in message ? message.id : 'unknown'})`;
debugLogger('→ Playwright:', logMessage);
debugLogger('\x1b[32m→ Playwright:\x1b[0m', logMessage);
this._playwrightConnection?.send(JSON.stringify(message));
}
}