diff --git a/packages/playwright-cloudflare/src/cloudflare/webSocketTransport.ts b/packages/playwright-cloudflare/src/cloudflare/webSocketTransport.ts index eee13c144..22d1b0401 100644 --- a/packages/playwright-cloudflare/src/cloudflare/webSocketTransport.ts +++ b/packages/playwright-cloudflare/src/cloudflare/webSocketTransport.ts @@ -11,6 +11,7 @@ export class WebSocketTransport implements ConnectionTransport { private _ws: WebSocket; private _pingInterval: NodeJS.Timer; private _chunks: Uint8Array[] = []; + private _disableChunking: boolean; onmessage?: (message: ProtocolResponse) => void; onclose?: () => void; readonly sessionId: string; @@ -23,13 +24,20 @@ export class WebSocketTransport implements ConnectionTransport { return transport; } - constructor(ws: WebSocket, sessionId: string) { + constructor(ws: WebSocket, sessionId: string, { disableChunking = false } = {}) { this._pingInterval = setInterval(() => { return this._ws.send('ping'); }, 1000); // TODO more investigation this._ws = ws; this.sessionId = sessionId; + this._disableChunking = disableChunking; this._ws.addEventListener('message', event => { + if (this._disableChunking) { + if (event.data && this.onmessage) + this.onmessage!(JSON.parse(event.data as string) as ProtocolResponse); + return; + } + this._chunks.push(new Uint8Array(event.data as ArrayBuffer)); const message = chunksToMessage(this._chunks, sessionId); if (message && this.onmessage) @@ -48,6 +56,10 @@ export class WebSocketTransport implements ConnectionTransport { } send(message: ProtocolRequest): void { + if (this._disableChunking) { + this._ws.send(JSON.stringify(message)); + return; + } for (const chunk of messageToChunks(JSON.stringify(message))) this._ws.send(chunk); } diff --git a/packages/playwright-cloudflare/src/index.ts b/packages/playwright-cloudflare/src/index.ts index 51330adfe..b07e22161 100644 --- a/packages/playwright-cloudflare/src/index.ts +++ b/packages/playwright-cloudflare/src/index.ts @@ -36,6 +36,11 @@ const originalConnectOverCDP = playwright.chromium.connectOverCDP; const wsEndpoint = typeof endpointURLOrOptions === 'string' ? endpointURLOrOptions : endpointURLOrOptions.wsEndpoint ?? endpointURLOrOptions.endpointURL; if (!wsEndpoint) throw new Error('No wsEndpoint provided'); + + if (isExternalWebSocketEndpoint(wsEndpoint)) { + return connectToExternalWebSocket(wsEndpoint); + } + const wsUrl = new URL(wsEndpoint); // by default, playwright.chromium.connectOverCDP enforces persistent to true (the default behavior upstream) if (!wsUrl.searchParams.has('persistent')) @@ -45,6 +50,35 @@ const originalConnectOverCDP = playwright.chromium.connectOverCDP; : launch(wsUrl.toString()); }; +function extractSessionIdFromUrl(wsEndpoint: string): string | undefined { + const url = new URL(wsEndpoint); + const sessionId = url.searchParams.get('browser_session') ?? undefined; + return sessionId; +} + +async function connectToExternalWebSocket(wsEndpoint: string): Promise { + resetMonotonicTime(); + const webSocket = new WebSocket(wsEndpoint); + await new Promise((resolve, reject) => { + webSocket.addEventListener('open', () => { + resolve(webSocket); + }); + webSocket.addEventListener('error', (error) => { + reject(error); + }); + }); + + const sessionId = extractSessionIdFromUrl(wsEndpoint); + + const transport = new WebSocketTransport(webSocket, sessionId ?? '', { disableChunking: true }); + + return await createBrowser(transport, { persistent: true }); +} + +function isExternalWebSocketEndpoint(endpoint: string): boolean { + return endpoint.startsWith('ws://') || endpoint.startsWith('wss://'); +} + async function connectDevtools(endpoint: BrowserEndpoint, options: { sessionId: string, persistent?: boolean }): Promise { resetMonotonicTime(); const url = new URL(`${HTTP_FAKE_HOST}/v1/connectDevtools`);