diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 296a596977..07912bfbdd 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -76,7 +76,7 @@ export class Android extends ChannelOwner implements ap connection.on('close', closePipe); let device: AndroidDevice; - let closeError: Error | undefined; + let closeError: string | undefined; const onPipeClosed = () => { device?._didClose(); connection.close(closeError); @@ -88,7 +88,7 @@ export class Android extends ChannelOwner implements ap try { connection!.dispatch(message); } catch (e) { - closeError = e; + closeError = String(e); closePipe(); } }); diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 2e1e12a529..9932050d3e 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -143,8 +143,8 @@ export class BrowserType extends ChannelOwner imple connection.on('close', closePipe); let browser: Browser; - let closeError: Error | undefined; - const onPipeClosed = () => { + let closeError: string | undefined; + const onPipeClosed = (reason?: string) => { // Emulate all pages, contexts and the browser closing upon disconnect. for (const context of browser?.contexts() || []) { for (const page of context.pages()) @@ -152,16 +152,16 @@ export class BrowserType extends ChannelOwner imple context._onClose(); } browser?._didClose(); - connection.close(closeError); + connection.close(reason || closeError); }; - pipe.on('closed', onPipeClosed); - connection.onmessage = message => pipe.send({ message }).catch(onPipeClosed); + pipe.on('closed', params => onPipeClosed(params.reason)); + connection.onmessage = message => pipe.send({ message }).catch(() => onPipeClosed()); pipe.on('message', ({ message }) => { try { connection!.dispatch(message); } catch (e) { - closeError = e; + closeError = String(e); closePipe(); } }); diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index f468e3ff41..a5c8ef77eb 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -191,8 +191,8 @@ export class Connection extends EventEmitter { (object._channel as any).emit(method, validator(params, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._rawBuffers ? 'buffer' : 'fromBase64' })); } - close(cause?: Error) { - this._closedError = new TargetClosedError(cause?.toString()); + close(cause?: string) { + this._closedError = new TargetClosedError(cause); for (const callback of this._callbacks.values()) callback.reject(this._closedError); this._callbacks.clear(); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 2a2c9d72e5..2ec820711a 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2586,7 +2586,7 @@ scheme.JsonPipeMessageEvent = tObject({ message: tAny, }); scheme.JsonPipeClosedEvent = tObject({ - error: tOptional(tType('SerializedError')), + reason: tOptional(tString), }); scheme.JsonPipeSendParams = tObject({ message: tAny, diff --git a/packages/playwright-core/src/server/chromium/crConnection.ts b/packages/playwright-core/src/server/chromium/crConnection.ts index ccec4663d1..c257d0d7ec 100644 --- a/packages/playwright-core/src/server/chromium/crConnection.ts +++ b/packages/playwright-core/src/server/chromium/crConnection.ts @@ -75,11 +75,11 @@ export class CRConnection extends EventEmitter { session._onMessage(message); } - _onClose() { + _onClose(reason?: string) { this._closed = true; this._transport.onmessage = undefined; this._transport.onclose = undefined; - this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()); + this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs(), reason); this.rootSession.dispose(); Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); } diff --git a/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts b/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts index 952e52c66f..dead26993f 100644 --- a/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/jsonPipeDispatcher.ts @@ -17,7 +17,6 @@ import type * as channels from '@protocol/channels'; import { Dispatcher } from './dispatcher'; import { createGuid } from '../../utils'; -import { serializeError } from '../errors'; import type { LocalUtilsDispatcher } from './localUtilsDispatcher'; export class JsonPipeDispatcher extends Dispatcher<{ guid: string }, channels.JsonPipeChannel, LocalUtilsDispatcher> implements channels.JsonPipeChannel { @@ -43,10 +42,9 @@ export class JsonPipeDispatcher extends Dispatcher<{ guid: string }, channels.Js this._dispatchEvent('message', { message }); } - wasClosed(error?: Error): void { + wasClosed(reason?: string): void { if (!this._disposed) { - const params = error ? { error: serializeError(error) } : {}; - this._dispatchEvent('closed', params); + this._dispatchEvent('closed', { reason }); this._dispose(); } } diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index c90110a460..732e135e14 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -230,9 +230,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. pipe.on('message', message => { transport.send(message); }); - transport.onclose = () => { + transport.onclose = (reason?: string) => { socksInterceptor?.cleanup(); - pipe.wasClosed(); + pipe.wasClosed(reason); }; pipe.on('close', () => transport.close()); return { pipe, headers: transport.headers }; diff --git a/packages/playwright-core/src/server/firefox/ffConnection.ts b/packages/playwright-core/src/server/firefox/ffConnection.ts index f36dba4b23..1a24e1dbf0 100644 --- a/packages/playwright-core/src/server/firefox/ffConnection.ts +++ b/packages/playwright-core/src/server/firefox/ffConnection.ts @@ -77,11 +77,11 @@ export class FFConnection extends EventEmitter { session.dispatchMessage(message); } - _onClose() { + _onClose(reason?: string) { this._closed = true; this._transport.onmessage = undefined; this._transport.onclose = undefined; - this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()); + this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs(), reason); this.rootSession.dispose(); Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); } diff --git a/packages/playwright-core/src/server/helper.ts b/packages/playwright-core/src/server/helper.ts index 84fd0174f6..5b22dfd5e1 100644 --- a/packages/playwright-core/src/server/helper.ts +++ b/packages/playwright-core/src/server/helper.ts @@ -95,10 +95,10 @@ class Helper { }; } - static formatBrowserLogs(logs: string[]) { - if (!logs.length) + static formatBrowserLogs(logs: string[], disconnectReason?: string) { + if (!disconnectReason && !logs.length) return ''; - return '\n' + logs.join('\n'); + return '\n' + (disconnectReason ? disconnectReason + '\n' : '') + logs.join('\n'); } } diff --git a/packages/playwright-core/src/server/pipeTransport.ts b/packages/playwright-core/src/server/pipeTransport.ts index a84ca67023..b5755d098d 100644 --- a/packages/playwright-core/src/server/pipeTransport.ts +++ b/packages/playwright-core/src/server/pipeTransport.ts @@ -25,7 +25,7 @@ export class PipeTransport implements ConnectionTransport { private _pendingBuffers: Buffer[] = []; private _waitForNextTask = makeWaitForNextTask(); private _closed = false; - private _onclose?: () => void; + private _onclose?: (reason?: string) => void; onmessage?: (message: ProtocolResponse) => void; @@ -47,7 +47,7 @@ export class PipeTransport implements ConnectionTransport { return this._onclose; } - set onclose(onclose: undefined | (() => void)) { + set onclose(onclose: undefined | ((reason?: string) => void)) { this._onclose = onclose; if (onclose && !this._pipeRead.readable) onclose(); diff --git a/packages/playwright-core/src/server/transport.ts b/packages/playwright-core/src/server/transport.ts index 6cefa270f8..e213e030e7 100644 --- a/packages/playwright-core/src/server/transport.ts +++ b/packages/playwright-core/src/server/transport.ts @@ -55,7 +55,7 @@ export interface ConnectionTransport { send(s: ProtocolRequest): void; close(): void; // Note: calling close is expected to issue onclose at some point. onmessage?: (message: ProtocolResponse) => void, - onclose?: () => void, + onclose?: (reason?: string) => void, } export class WebSocketTransport implements ConnectionTransport { @@ -64,7 +64,7 @@ export class WebSocketTransport implements ConnectionTransport { private _logUrl: string; onmessage?: (message: ProtocolResponse) => void; - onclose?: () => void; + onclose?: (reason?: string) => void; readonly wsEndpoint: string; readonly headers: HeadersArray = []; @@ -175,7 +175,7 @@ export class WebSocketTransport implements ConnectionTransport { this._ws.addEventListener('close', event => { this._progress?.log(` ${logUrl} code=${event.code} reason=${event.reason}`); if (this.onclose) - this.onclose.call(null); + this.onclose.call(null, event.reason); }); // Prevent Error: read ECONNRESET. this._ws.addEventListener('error', error => this._progress?.log(` ${logUrl} ${error.type} ${error.message}`)); diff --git a/packages/playwright-core/src/server/webkit/wkConnection.ts b/packages/playwright-core/src/server/webkit/wkConnection.ts index 3eb777fd73..403b32bbd6 100644 --- a/packages/playwright-core/src/server/webkit/wkConnection.ts +++ b/packages/playwright-core/src/server/webkit/wkConnection.ts @@ -78,11 +78,11 @@ export class WKConnection { this.browserSession.dispatchMessage(message); } - _onClose() { + _onClose(reason?: string) { this._closed = true; this._transport.onmessage = undefined; this._transport.onclose = undefined; - this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()); + this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs(), reason); this.browserSession.dispose(); this._onDisconnect(); } diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 80f88980de..a35f12791a 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -4674,7 +4674,7 @@ export type JsonPipeMessageEvent = { message: any, }; export type JsonPipeClosedEvent = { - error?: SerializedError, + reason?: string, }; export type JsonPipeSendParams = { message: any, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 5ac91f587f..8c9e76baef 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -3548,4 +3548,4 @@ JsonPipe: closed: parameters: - error: SerializedError? + reason: string? diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 5dbd271264..a78814d326 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -129,6 +129,16 @@ for (const kind of ['launchServer', 'run-server'] as const) { expect(error.message).not.toContain('secret=MYSECRET'); }); + test('should print custom ws close error', async ({ connect, server }) => { + server.onceWebSocketConnection((ws, request) => { + ws.on('message', message => { + ws.close(4123, 'Oh my!'); + }); + }); + const error = await connect(`ws://localhost:${server.PORT}/ws`).catch(e => e); + expect(error.message).toContain('browserType.connect: Oh my!'); + }); + test('should be able to reconnect to a browser', async ({ connect, startRemoteServer, server }) => { const remoteServer = await startRemoteServer(kind); { @@ -325,7 +335,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { ]); expect(browser.isConnected()).toBe(false); const error = await page.evaluate('1 + 1').catch(e => e) as Error; - expect(error.message).toContain('has been closed'); + expect(error.message).toContain('closed'); }); test('should throw when calling waitForNavigation after disconnect', async ({ connect, startRemoteServer }) => { diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts index dd4e1c85fd..a473539e8e 100644 --- a/tests/library/chromium/connect-over-cdp.spec.ts +++ b/tests/library/chromium/connect-over-cdp.spec.ts @@ -527,3 +527,13 @@ test('setInputFiles should preserve lastModified timestamp', async ({ browserTyp await browserServer.close(); } }); + +test('should print custom ws close error', async ({ browserType, server }) => { + server.onceWebSocketConnection((ws, request) => { + ws.on('message', message => { + ws.close(4123, 'Oh my!'); + }); + }); + const error = await browserType.connectOverCDP(`ws://localhost:${server.PORT}/ws`).catch(e => e); + expect(error.message).toContain(`Browser logs:\n\nOh my!\n`); +});