mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: remove isContextDestroyedError heuristic (#8456)
This commit is contained in:
parent
32fbf22add
commit
4ed976f2e9
@ -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<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
|
||||
private readonly _callbacks = new Map<number, {resolve: (o: any) => 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<Protocol.CommandReturnValues[T]> {
|
||||
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<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
|
||||
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}`;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
28
src/server/common/protocolError.ts
Normal file
28
src/server/common/protocolError.ts
Normal file
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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<number, {resolve: Function, reject: Function, error: Error, method: string}>;
|
||||
private _callbacks: Map<number, {resolve: (o: any) => 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<number, {resolve: Function, reject: Function, error: Error, method: string}>;
|
||||
private _targetType: string;
|
||||
private _callbacks: Map<number, {resolve: Function, reject: Function, error: ProtocolError, method: string}>;
|
||||
private _sessionId: string;
|
||||
private _rawSend: (message: any) => void;
|
||||
private _crashed: boolean = false;
|
||||
@ -161,12 +164,11 @@ export class FFSession extends EventEmitter {
|
||||
override removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
override once: <T extends keyof Protocol.Events | symbol>(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<Protocol.CommandReturnValues[T]> {
|
||||
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}`;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<dom.FrameExecutionContext>;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
|
||||
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: ProtocolError) => void, error: ProtocolError, method: string}>();
|
||||
private _crashed: boolean = false;
|
||||
|
||||
override on: <T extends keyof Protocol.Events | symbol>(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<Protocol.CommandReturnValues[T]> {
|
||||
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<Protocol.CommandReturnValues[T]>((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)}`;
|
||||
|
||||
@ -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<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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 extends Error>(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;
|
||||
}
|
||||
|
||||
|
||||
@ -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}) => {
|
||||
|
||||
@ -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}) => {
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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}) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user