diff --git a/src/server/chromium/crConnection.ts b/src/server/chromium/crConnection.ts index 21f0ef17ee..da508ac536 100644 --- a/src/server/chromium/crConnection.ts +++ b/src/server/chromium/crConnection.ts @@ -23,6 +23,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace'; import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger'; import { ProtocolLogger } from '../types'; import { helper } from '../helper'; +import { ProtocolError } from '../common/protocolError'; export const ConnectionEvents = { Disconnected: Symbol('ConnectionEvents.Disconnected') @@ -126,7 +127,7 @@ export const CRSessionEvents = { export class CRSession extends EventEmitter { _connection: CRConnection | null; _eventListener?: (method: string, params?: Object) => void; - private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); + private readonly _callbacks = new Map void, reject: (e: ProtocolError) => void, error: ProtocolError, method: string}>(); private readonly _targetType: string; private readonly _sessionId: string; private readonly _rootSessionId: string; @@ -164,19 +165,19 @@ export class CRSession extends EventEmitter { params?: Protocol.CommandParameters[T] ): Promise { if (this._crashed) - throw new Error('Target crashed'); + throw new ProtocolError(true, 'Target crashed'); if (this._browserDisconnectedLogs !== undefined) - throw new Error(`Protocol error (${method}): Browser closed.` + this._browserDisconnectedLogs); + throw new ProtocolError(true, `Browser closed.` + this._browserDisconnectedLogs); if (!this._connection) - throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`); + throw new ProtocolError(true, `Target closed`); const id = this._connection._rawSend(this._sessionId, method, params); return new Promise((resolve, reject) => { - this._callbacks.set(id, {resolve, reject, error: new Error(), method}); + this._callbacks.set(id, {resolve, reject, error: new ProtocolError(false), method}); }); } _sendMayFail(method: T, params?: Protocol.CommandParameters[T]): Promise { - return this.send(method, params).catch((error: Error) => debugLogger.log('error', error)); + return this.send(method, params).catch((error: ProtocolError) => debugLogger.log('error', error)); } _onMessage(object: ProtocolResponse) { @@ -208,16 +209,18 @@ export class CRSession extends EventEmitter { _onClosed(browserDisconnectedLogs: string | undefined) { this._browserDisconnectedLogs = browserDisconnectedLogs; - const errorMessage = browserDisconnectedLogs !== undefined ? 'Browser closed.' + browserDisconnectedLogs : 'Target closed.'; - for (const callback of this._callbacks.values()) - callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): ` + errorMessage)); + const errorMessage = browserDisconnectedLogs !== undefined ? 'Browser closed.' + browserDisconnectedLogs : 'Target closed'; + for (const callback of this._callbacks.values()) { + callback.error.sessionClosed = true; + callback.reject(rewriteErrorMessage(callback.error, errorMessage)); + } this._callbacks.clear(); this._connection = null; Promise.resolve().then(() => this.emit(CRSessionEvents.Disconnected)); } } -function createProtocolError(error: Error, method: string, protocolError: { message: string; data: any; }): Error { +function createProtocolError(error: ProtocolError, method: string, protocolError: { message: string; data: any; }): ProtocolError { let message = `Protocol error (${method}): ${protocolError.message}`; if ('data' in protocolError) message += ` ${protocolError.data}`; diff --git a/src/server/chromium/crExecutionContext.ts b/src/server/chromium/crExecutionContext.ts index e2d16f93d5..51d68e8d3f 100644 --- a/src/server/chromium/crExecutionContext.ts +++ b/src/server/chromium/crExecutionContext.ts @@ -21,6 +21,7 @@ import { Protocol } from './protocol'; import * as js from '../javascript'; import { rewriteErrorMessage } from '../../utils/stackTrace'; import { parseEvaluationResultValue } from '../common/utilityScriptSerializers'; +import { isSessionClosedError } from '../common/protocolError'; export class CRExecutionContext implements js.ExecutionContextDelegate { _client: CRSession; @@ -38,7 +39,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate { returnByValue: true, }).catch(rewriteError); if (exceptionDetails) - throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails)); + throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails)); return remoteObject.value; } @@ -48,7 +49,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate { contextId: this._contextId, }).catch(rewriteError); if (exceptionDetails) - throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails)); + throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails)); return remoteObject.objectId!; } @@ -76,7 +77,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate { userGesture: true }).catch(rewriteError); if (exceptionDetails) - throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails)); + throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails)); return returnByValue ? parseEvaluationResultValue(remoteObject.value) : utilityScript._context.createHandle(remoteObject); } @@ -109,10 +110,10 @@ function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue { if (error.message.includes('Object couldn\'t be returned by value')) return {result: {type: 'undefined'}}; - if (js.isContextDestroyedError(error) || error.message.endsWith('Inspected target navigated or closed')) - throw new Error('Execution context was destroyed, most likely because of a navigation.'); if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON')) rewriteErrorMessage(error, error.message + ' Are you passing a nested JSHandle?'); + if (!js.isJavaScriptErrorInEvaluate(error) && !isSessionClosedError(error)) + throw new Error('Execution context was destroyed, most likely because of a navigation.'); throw error; } diff --git a/src/server/common/protocolError.ts b/src/server/common/protocolError.ts new file mode 100644 index 0000000000..5d38429d00 --- /dev/null +++ b/src/server/common/protocolError.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class ProtocolError extends Error { + sessionClosed: boolean; + + constructor(sessionClosed: boolean, message?: string) { + super(message); + this.sessionClosed = sessionClosed || false; + } +} + +export function isSessionClosedError(e: Error): boolean { + return e instanceof ProtocolError && e.sessionClosed; +} diff --git a/src/server/firefox/ffBrowser.ts b/src/server/firefox/ffBrowser.ts index 219ee295f3..da3ebc0743 100644 --- a/src/server/firefox/ffBrowser.ts +++ b/src/server/firefox/ffBrowser.ts @@ -104,7 +104,7 @@ export class FFBrowser extends Browser { assert(type === 'page'); const context = browserContextId ? this._contexts.get(browserContextId)! : this._defaultContext as FFBrowserContext; assert(context, `Unknown context id:${browserContextId}, _defaultContext: ${this._defaultContext}`); - const session = this._connection.createSession(payload.sessionId, type); + const session = this._connection.createSession(payload.sessionId); const opener = openerId ? this._ffPages.get(openerId)! : null; const ffPage = new FFPage(session, context, opener); this._ffPages.set(targetId, ffPage); diff --git a/src/server/firefox/ffConnection.ts b/src/server/firefox/ffConnection.ts index 8d37452efe..d6de82fb52 100644 --- a/src/server/firefox/ffConnection.ts +++ b/src/server/firefox/ffConnection.ts @@ -23,6 +23,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace'; import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger'; import { ProtocolLogger } from '../types'; import { helper } from '../helper'; +import { ProtocolError } from '../common/protocolError'; export const ConnectionEvents = { Disconnected: Symbol('Disconnected'), @@ -34,7 +35,7 @@ export const kBrowserCloseMessageId = -9999; export class FFConnection extends EventEmitter { private _lastId: number; - private _callbacks: Map; + private _callbacks: Map void, reject: (e: ProtocolError) => void, error: ProtocolError, method: string}>; private _transport: ConnectionTransport; private readonly _protocolLogger: ProtocolLogger; private readonly _browserLogsCollector: RecentLogsCollector; @@ -76,7 +77,7 @@ export class FFConnection extends EventEmitter { const id = this.nextMessageId(); this._rawSend({id, method, params}); return new Promise((resolve, reject) => { - this._callbacks.set(id, {resolve, reject, error: new Error(), method}); + this._callbacks.set(id, {resolve, reject, error: new ProtocolError(false), method}); }); } @@ -86,7 +87,7 @@ export class FFConnection extends EventEmitter { _checkClosed(method: string) { if (this._closed) - throw new Error(`Protocol error (${method}): Browser closed.` + helper.formatBrowserLogs(this._browserLogsCollector.recentLogs())); + throw new ProtocolError(true, `${method}): Browser closed.` + helper.formatBrowserLogs(this._browserLogsCollector.recentLogs())); } _rawSend(message: ProtocolRequest) { @@ -123,10 +124,13 @@ export class FFConnection extends EventEmitter { this._transport.onclose = undefined; const formattedBrowserLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()); for (const session of this._sessions.values()) - session.dispose(formattedBrowserLogs); + session.dispose(); this._sessions.clear(); - for (const callback of this._callbacks.values()) - callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Browser closed.` + formattedBrowserLogs)); + for (const callback of this._callbacks.values()) { + const error = rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Browser closed.` + formattedBrowserLogs); + error.sessionClosed = true; + callback.reject(error); + } this._callbacks.clear(); Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); } @@ -136,8 +140,8 @@ export class FFConnection extends EventEmitter { this._transport.close(); } - createSession(sessionId: string, type: string): FFSession { - const session = new FFSession(this, type, sessionId, message => this._rawSend({...message, sessionId})); + createSession(sessionId: string): FFSession { + const session = new FFSession(this, sessionId, message => this._rawSend({...message, sessionId})); this._sessions.set(sessionId, session); return session; } @@ -150,8 +154,7 @@ export const FFSessionEvents = { export class FFSession extends EventEmitter { _connection: FFConnection; _disposed = false; - private _callbacks: Map; - private _targetType: string; + private _callbacks: Map; private _sessionId: string; private _rawSend: (message: any) => void; private _crashed: boolean = false; @@ -161,12 +164,11 @@ export class FFSession extends EventEmitter { override removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; override once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - constructor(connection: FFConnection, targetType: string, sessionId: string, rawSend: (message: any) => void) { + constructor(connection: FFConnection, sessionId: string, rawSend: (message: any) => void) { super(); this.setMaxListeners(0); this._callbacks = new Map(); this._connection = connection; - this._targetType = targetType; this._sessionId = sessionId; this._rawSend = rawSend; @@ -186,14 +188,14 @@ export class FFSession extends EventEmitter { params?: Protocol.CommandParameters[T] ): Promise { if (this._crashed) - throw new Error('Page crashed'); + throw new ProtocolError(true, 'Target crashed'); this._connection._checkClosed(method); if (this._disposed) - throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`); + throw new ProtocolError(true, 'Target closed'); const id = this._connection.nextMessageId(); this._rawSend({method, params, id}); return new Promise((resolve, reject) => { - this._callbacks.set(id, {resolve, reject, error: new Error(), method}); + this._callbacks.set(id, {resolve, reject, error: new ProtocolError(false), method}); }); } @@ -215,9 +217,11 @@ export class FFSession extends EventEmitter { } } - dispose(formattedBrowserLogs?: string) { - for (const callback of this._callbacks.values()) - callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.` + formattedBrowserLogs)); + dispose() { + for (const callback of this._callbacks.values()) { + callback.error.sessionClosed = true; + callback.reject(rewriteErrorMessage(callback.error, 'Target closed')); + } this._callbacks.clear(); this._disposed = true; this._connection._sessions.delete(this._sessionId); @@ -225,7 +229,7 @@ export class FFSession extends EventEmitter { } } -function createProtocolError(error: Error, method: string, protocolError: { message: string; data: any; }): Error { +function createProtocolError(error: ProtocolError, method: string, protocolError: { message: string; data: any; }): ProtocolError { let message = `Protocol error (${method}): ${protocolError.message}`; if ('data' in protocolError) message += ` ${protocolError.data}`; diff --git a/src/server/firefox/ffExecutionContext.ts b/src/server/firefox/ffExecutionContext.ts index 6f055f5c73..8b29ab27c9 100644 --- a/src/server/firefox/ffExecutionContext.ts +++ b/src/server/firefox/ffExecutionContext.ts @@ -20,6 +20,7 @@ import { FFSession } from './ffConnection'; import { Protocol } from './protocol'; import { rewriteErrorMessage } from '../../utils/stackTrace'; import { parseEvaluationResultValue } from '../common/utilityScriptSerializers'; +import { isSessionClosedError } from '../common/protocolError'; export class FFExecutionContext implements js.ExecutionContextDelegate { _session: FFSession; @@ -103,18 +104,18 @@ function checkException(exceptionDetails?: Protocol.Runtime.ExceptionDetails) { if (!exceptionDetails) return; if (exceptionDetails.value) - throw new Error('Evaluation failed: ' + JSON.stringify(exceptionDetails.value)); + throw new js.JavaScriptErrorInEvaluate(JSON.stringify(exceptionDetails.value)); else - throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack); + throw new js.JavaScriptErrorInEvaluate(exceptionDetails.text + '\n' + exceptionDetails.stack); } function rewriteError(error: Error): (Protocol.Runtime.evaluateReturnValue | Protocol.Runtime.callFunctionReturnValue) { if (error.message.includes('cyclic object value') || error.message.includes('Object is not serializable')) return {result: {type: 'undefined', value: undefined}}; - if (js.isContextDestroyedError(error)) - throw new Error('Execution context was destroyed, most likely because of a navigation.'); if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON')) rewriteErrorMessage(error, error.message + ' Are you passing a nested JSHandle?'); + if (!js.isJavaScriptErrorInEvaluate(error) && !isSessionClosedError(error)) + throw new Error('Execution context was destroyed, most likely because of a navigation.'); throw error; } diff --git a/src/server/firefox/ffPage.ts b/src/server/firefox/ffPage.ts index 825f498ab9..f4d53a7aa4 100644 --- a/src/server/firefox/ffPage.ts +++ b/src/server/firefox/ffPage.ts @@ -269,7 +269,7 @@ export class FFPage implements PageDelegate { async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) { const workerId = event.workerId; const worker = new Worker(this._page, event.url); - const workerSession = new FFSession(this._session._connection, 'worker', workerId, (message: any) => { + const workerSession = new FFSession(this._session._connection, workerId, (message: any) => { this._session.send('Page.sendMessageToWorker', { frameId: event.frameId, workerId: workerId, diff --git a/src/server/frames.ts b/src/server/frames.ts index 42f676874a..7ce7001c7e 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -30,6 +30,7 @@ import { assert, constructURLBasedOnBaseURL, makeWaitForNextTask } from '../util import { debugLogger } from '../utils/debugLogger'; import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation'; import { ElementStateWithoutStable } from './injected/injectedScript'; +import { isSessionClosedError } from './common/protocolError'; type ContextData = { contextPromise: Promise; @@ -728,7 +729,7 @@ export class Frame extends SdkObject { return adopted; } catch (e) { // Navigated while trying to adopt the node. - if (!js.isContextDestroyedError(e) && !e.message.includes(dom.kUnableToAdoptErrorMessage)) + if (js.isJavaScriptErrorInEvaluate(e)) throw e; result.dispose(); } @@ -1350,11 +1351,12 @@ class RerunnableTask { this._contextData.rerunnableTasks.delete(this); this._resolve(result); } catch (e) { + if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e)) { + this._contextData.rerunnableTasks.delete(this); + this._reject(e); + } + // We will try again in the new execution context. - if (js.isContextDestroyedError(e)) - return; - this._contextData.rerunnableTasks.delete(this); - this._reject(e); } } } diff --git a/src/server/javascript.ts b/src/server/javascript.ts index ca4d5b476d..042dd2b80c 100644 --- a/src/server/javascript.ts +++ b/src/server/javascript.ts @@ -280,26 +280,10 @@ export function normalizeEvaluationExpression(expression: string, isFunction: bo return expression; } -export const kSwappedOutErrorMessage = 'Target was swapped out.'; - -export function isContextDestroyedError(e: any) { - if (!e || typeof e !== 'object' || typeof e.message !== 'string') - return false; - - // Evaluating in a context which was already destroyed. - if (e.message.includes('Cannot find context with specified id') - || e.message.includes('Failed to find execution context with id') - || e.message.includes('Missing injected script for given') - || e.message.includes('Cannot find object with id')) - return true; - - // Evaluation promise is rejected when context is gone. - if (e.message.includes('Execution context was destroyed')) - return true; - - // WebKit target swap. - if (e.message.includes(kSwappedOutErrorMessage)) - return true; - - return false; +// Error inside the expression evaluation as opposed to a protocol error. +export class JavaScriptErrorInEvaluate extends Error { +} + +export function isJavaScriptErrorInEvaluate(error: Error) { + return error instanceof JavaScriptErrorInEvaluate; } diff --git a/src/server/page.ts b/src/server/page.ts index b49e4a8e7e..0698a2f89e 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -435,7 +435,7 @@ export class Page extends SdkObject { const runBeforeUnload = !!options && !!options.runBeforeUnload; if (this._closedState !== 'closing') { this._closedState = 'closing'; - assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.'); + assert(!this._disconnected, 'Target closed'); // This might throw if the browser context containing the page closes // while we are trying to close the page. await this._delegate.closePage(runBeforeUnload).catch(e => debugLogger.log('error', e)); diff --git a/src/server/webkit/wkBrowser.ts b/src/server/webkit/wkBrowser.ts index 443beb1408..82917f678b 100644 --- a/src/server/webkit/wkBrowser.ts +++ b/src/server/webkit/wkBrowser.ts @@ -153,7 +153,7 @@ export class WKBrowser extends Browser { context = this._defaultContext as WKBrowserContext; if (!context) return; - const pageProxySession = new WKSession(this._connection, pageProxyId, `The page has been closed.`, (message: any) => { + const pageProxySession = new WKSession(this._connection, pageProxyId, `Target closed`, (message: any) => { this._connection.rawSend({ ...message, pageProxyId }); }); const opener = event.openerId ? this._wkPages.get(event.openerId) : undefined; diff --git a/src/server/webkit/wkConnection.ts b/src/server/webkit/wkConnection.ts index 08682c1a96..3f5659a1a4 100644 --- a/src/server/webkit/wkConnection.ts +++ b/src/server/webkit/wkConnection.ts @@ -24,6 +24,7 @@ import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger'; import { ProtocolLogger } from '../types'; import { helper } from '../helper'; import { kBrowserClosedError } from '../../utils/errors'; +import { ProtocolError } from '../common/protocolError'; // WKPlaywright uses this special id to issue Browser.close command which we // should ignore. @@ -101,7 +102,7 @@ export class WKSession extends EventEmitter { private _disposed = false; private readonly _rawSend: (message: any) => void; - private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); + private readonly _callbacks = new Map void, reject: (e: ProtocolError) => void, error: ProtocolError, method: string}>(); private _crashed: boolean = false; override on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; @@ -130,14 +131,14 @@ export class WKSession extends EventEmitter { params?: Protocol.CommandParameters[T] ): Promise { if (this._crashed) - throw new Error('Target crashed'); + throw new ProtocolError(true, 'Target crashed'); if (this._disposed) - throw new Error(`Protocol error (${method}): ${this.errorText}`); + throw new ProtocolError(true, `Target closed`); const id = this.connection.nextMessageId(); const messageObj = { id, method, params }; this._rawSend(messageObj); return new Promise((resolve, reject) => { - this._callbacks.set(id, {resolve, reject, error: new Error(), method}); + this._callbacks.set(id, {resolve, reject, error: new ProtocolError(false), method}); }); } @@ -156,8 +157,10 @@ export class WKSession extends EventEmitter { dispose(disconnected: boolean) { if (disconnected) this.errorText = 'Browser closed.' + helper.formatBrowserLogs(this.connection._browserLogsCollector.recentLogs()); - for (const callback of this._callbacks.values()) - callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): ${this.errorText}`)); + for (const callback of this._callbacks.values()) { + callback.error.sessionClosed = true; + callback.reject(rewriteErrorMessage(callback.error, this.errorText)); + } this._callbacks.clear(); this._disposed = true; } @@ -179,7 +182,7 @@ export class WKSession extends EventEmitter { } } -export function createProtocolError(error: Error, method: string, protocolError: { message: string; data: any; }): Error { +export function createProtocolError(error: ProtocolError, method: string, protocolError: { message: string; data: any; }): ProtocolError { let message = `Protocol error (${method}): ${protocolError.message}`; if ('data' in protocolError) message += ` ${JSON.stringify(protocolError.data)}`; diff --git a/src/server/webkit/wkExecutionContext.ts b/src/server/webkit/wkExecutionContext.ts index aac2685116..99d948009c 100644 --- a/src/server/webkit/wkExecutionContext.ts +++ b/src/server/webkit/wkExecutionContext.ts @@ -19,6 +19,7 @@ import { WKSession } from './wkConnection'; import { Protocol } from './protocol'; import * as js from '../javascript'; import { parseEvaluationResultValue } from '../common/utilityScriptSerializers'; +import { isSessionClosedError } from '../common/protocolError'; export class WKExecutionContext implements js.ExecutionContextDelegate { private readonly _session: WKSession; @@ -46,7 +47,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { returnByValue: true }); if (response.wasThrown) - throw new Error('Evaluation failed: ' + response.result.description); + throw new js.JavaScriptErrorInEvaluate(response.result.description); return response.result.value; } catch (error) { throw rewriteError(error); @@ -61,7 +62,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { returnByValue: false }); if (response.wasThrown) - throw new Error('Evaluation failed: ' + response.result.description); + throw new js.JavaScriptErrorInEvaluate(response.result.description); return response.result.objectId!; } catch (error) { throw rewriteError(error); @@ -81,7 +82,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], objectIds: string[]): Promise { try { const response = await Promise.race([ - this._executionContextDestroyedPromise.then(() => contextDestroyedResult), + this._executionContextDestroyedPromise.then(() => { throw new Error(contextDestroyedError); }), this._session.send('Runtime.callFunctionOn', { functionDeclaration: expression, objectId: utilityScript._objectId!, @@ -96,7 +97,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { }) ]); if (response.wasThrown) - throw new Error('Evaluation failed: ' + response.result.description); + throw new js.JavaScriptErrorInEvaluate(response.result.description); if (returnByValue) return parseEvaluationResultValue(response.result.value); return utilityScript._context.createHandle(response.result); @@ -129,12 +130,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { } } -const contextDestroyedResult = { - wasThrown: true, - result: { - description: 'Protocol error: Execution context was destroyed, most likely because of a navigation.' - } as Protocol.Runtime.RemoteObject -}; +const contextDestroyedError = 'Execution context was destroyed.'; function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObject): any { const value = remoteObject.value; @@ -143,7 +139,7 @@ function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObj } function rewriteError(error: Error): Error { - if (js.isContextDestroyedError(error)) + if (!js.isJavaScriptErrorInEvaluate(error) && !isSessionClosedError(error)) return new Error('Execution context was destroyed, most likely because of a navigation.'); return error; } diff --git a/src/server/webkit/wkPage.ts b/src/server/webkit/wkPage.ts index a13e0310a6..44d96ff61f 100644 --- a/src/server/webkit/wkPage.ts +++ b/src/server/webkit/wkPage.ts @@ -26,7 +26,7 @@ import * as dom from '../dom'; import * as frames from '../frames'; import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper'; import { helper } from '../helper'; -import { JSHandle, kSwappedOutErrorMessage } from '../javascript'; +import { JSHandle } from '../javascript'; import * as network from '../network'; import { Page, PageBinding, PageDelegate } from '../page'; import { Progress } from '../progress'; @@ -223,7 +223,6 @@ export class WKPage implements PageDelegate { assert(this._provisionalPage); assert(this._provisionalPage._session.sessionId === newTargetId, 'Unknown new target: ' + newTargetId); assert(this._session.sessionId === oldTargetId, 'Unknown old target: ' + oldTargetId); - this._session.errorText = kSwappedOutErrorMessage; const newSession = this._provisionalPage._session; this._provisionalPage.commit(); this._provisionalPage.dispose(); @@ -294,7 +293,7 @@ export class WKPage implements PageDelegate { private async _onTargetCreated(event: Protocol.Target.targetCreatedPayload) { const { targetInfo } = event; - const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, `The ${targetInfo.type} has been closed.`, (message: any) => { + const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, `Target closed`, (message: any) => { this._pageProxySession.send('Target.sendMessageToTarget', { message: JSON.stringify(message), targetId: targetInfo.targetId }).catch(e => { diff --git a/src/utils/stackTrace.ts b/src/utils/stackTrace.ts index 917da9461d..6e0c71f51b 100644 --- a/src/utils/stackTrace.ts +++ b/src/utils/stackTrace.ts @@ -21,13 +21,12 @@ import { isUnderTest } from './utils'; const stackUtils = new StackUtils(); -export function rewriteErrorMessage(e: Error, newMessage: string): Error { - if (e.stack) { - const index = e.stack.indexOf(e.message); - if (index !== -1) - e.stack = e.stack.substring(0, index) + newMessage + e.stack.substring(index + e.message.length); - } +export function rewriteErrorMessage(e: E, newMessage: string): E { + const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at ')); e.message = newMessage; + const errorTitle = `${e.name}: ${e.message}`; + if (lines.length) + e.stack = `${errorTitle}\n${lines.join('\n')}`; return e; } diff --git a/tests/browsertype-launch.spec.ts b/tests/browsertype-launch.spec.ts index ab7fbedf9b..b55f1fcd02 100644 --- a/tests/browsertype-launch.spec.ts +++ b/tests/browsertype-launch.spec.ts @@ -25,7 +25,8 @@ it('should reject all promises when browser is closed', async ({browserType, bro await page.evaluate(() => new Promise(f => setTimeout(f, 0))); await browser.close(); await neverResolves; - expect(error.message).toContain('Protocol error'); + // WebKit under task-set -c 1 is giving browser, rest are giving target. + expect(error.message).toContain(' closed'); }); it('should throw if userDataDir option is passed', async ({browserType, browserOptions}) => { diff --git a/tests/page/page-basic.spec.ts b/tests/page/page-basic.spec.ts index 9a45f05170..43c1cb68f4 100644 --- a/tests/page/page-basic.spec.ts +++ b/tests/page/page-basic.spec.ts @@ -23,7 +23,7 @@ it('should reject all promises when page is closed', async ({page}) => { page.evaluate(() => new Promise(r => {})).catch(e => error = e), page.close(), ]); - expect(error.message).toContain('Protocol error'); + expect(error.message).toContain('Target closed'); }); it('should set the page close state', async ({page}) => { diff --git a/tests/page/page-event-crash.spec.ts b/tests/page/page-event-crash.spec.ts index 115da312dc..5b64adb525 100644 --- a/tests/page/page-event-crash.spec.ts +++ b/tests/page/page-event-crash.spec.ts @@ -45,7 +45,7 @@ it.describe('', () => { await page.waitForEvent('crash'); const err = await page.evaluate(() => {}).then(() => null, e => e); expect(err).toBeTruthy(); - expect(err.message).toContain('crash'); + expect(err.message).toContain('Target crashed'); }); it('should cancel waitForEvent when page crashes', async ({ page, toImpl, browserName, platform, mode }) => { diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index 9ae3a84a18..fa6edba6bc 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -506,7 +506,7 @@ it('should not fulfill with redirect status', async ({page, server, browserName} } }); -it('should support cors with GET', async ({page, server}) => { +it('should support cors with GET', async ({page, server, browserName}) => { await page.goto(server.EMPTY_PAGE); await page.route('**/cars*', async (route, request) => { const headers = request.url().endsWith('allow') ? { 'access-control-allow-origin': '*' } : {}; @@ -531,7 +531,12 @@ it('should support cors with GET', async ({page, server}) => { const response = await fetch('https://example.com/cars?reject', { mode: 'cors' }); return response.json(); }).catch(e => e); - expect(error.message).toContain('failed'); + if (browserName === 'chromium') + expect(error.message).toContain('Failed'); + if (browserName === 'webkit') + expect(error.message).toContain('TypeError'); + if (browserName === 'firefox') + expect(error.message).toContain('NetworkError'); } }); diff --git a/tests/page/workers.spec.ts b/tests/page/workers.spec.ts index 002d677494..830461fd7e 100644 --- a/tests/page/workers.spec.ts +++ b/tests/page/workers.spec.ts @@ -41,7 +41,7 @@ it('should emit created and destroyed events', async function({page}) { await page.evaluate(workerObj => workerObj.terminate(), workerObj); expect(await workerDestroyedPromise).toBe(worker); const error = await workerThisObj.getProperty('self').catch(error => error); - expect(error.message).toContain('Most likely the worker has been closed.'); + expect(error.message).toContain('Target closed'); }); it('should report console logs', async function({page}) {