chore: unify target closed errors (#27540)

This commit is contained in:
Pavel Feldman 2023-10-12 11:05:34 -07:00 committed by GitHub
parent 8595a6c25f
commit f212fd1a83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 171 additions and 146 deletions

View File

@ -10,7 +10,7 @@ export default defineConfig({
testDir: './tests',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
timeout: 15_000,
expect: {
@ -18,7 +18,7 @@ export default defineConfig({
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000
timeout: 5_000
},
/* Fail the build on CI if you accidentally left test.only in the source code. */

View File

@ -17,9 +17,8 @@ const TODO_ITEMS = [
test.describe('New Todo', () => {
test('should allow me to add todo items', async ({ page }) => {
test.setTimeout(5000);
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be completed?');
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create 1st todo.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');

View File

@ -34,6 +34,7 @@ import { spawn } from 'child_process';
import { wrapInASCIIBox, isLikelyNpxGlobal, assert, gracefullyProcessExitDoNotHang, getPackageManagerExecCommand } from '../utils';
import type { Executable } from '../server';
import { registry, writeDockerVersion } from '../server';
import { isTargetClosedError } from '../common/errors';
const packageJSON = require('../../package.json');
@ -530,7 +531,7 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi
else if (!url.startsWith('http') && !url.startsWith('file://') && !url.startsWith('about:') && !url.startsWith('data:'))
url = 'http://' + url;
await page.goto(url).catch(error => {
if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN && error.message.includes('Navigation failed because page was closed')) {
if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN && isTargetClosedError(error)) {
// Tests with PWTEST_CLI_AUTO_EXIT_WHEN might close page too fast, resulting
// in a stray navigation aborted error. We should ignore it.
} else {

View File

@ -27,7 +27,7 @@ import { TimeoutSettings } from '../common/timeoutSettings';
import { Waiter } from './waiter';
import { EventEmitter } from 'events';
import { Connection } from './connection';
import { isSafeCloseError, kBrowserClosedError } from '../common/errors';
import { isTargetClosedError, TargetClosedError } from '../common/errors';
import { raceAgainstDeadline } from '../utils/timeoutRunner';
import type { AndroidServerLauncherImpl } from '../androidServerImpl';
@ -76,10 +76,10 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
connection.on('close', closePipe);
let device: AndroidDevice;
let closeError: string | undefined;
let closeError: Error | undefined;
const onPipeClosed = () => {
device?._didClose();
connection.close(closeError || kBrowserClosedError);
connection.close(closeError);
};
pipe.on('closed', onPipeClosed);
connection.onmessage = message => pipe.send({ message }).catch(onPipeClosed);
@ -88,7 +88,7 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
try {
connection!.dispatch(message);
} catch (e) {
closeError = e.toString();
closeError = e;
closePipe();
}
});
@ -237,11 +237,11 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
async close() {
try {
if (this._shouldCloseConnectionOnClose)
this._connection.close(kBrowserClosedError);
this._connection.close();
else
await this._channel.close();
} catch (e) {
if (isSafeCloseError(e))
if (isTargetClosedError(e))
return;
throw e;
}
@ -281,7 +281,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.AndroidDevice.Close)
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed'));
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;

View File

@ -21,7 +21,7 @@ import type { Page } from './page';
import { ChannelOwner } from './channelOwner';
import { Events } from './events';
import type { LaunchOptions, BrowserContextOptions, HeadersArray } from './types';
import { isSafeCloseError, kBrowserClosedError } from '../common/errors';
import { isTargetClosedError } from '../common/errors';
import type * as api from '../../types/types';
import { CDPSession } from './cdpSession';
import type { BrowserType } from './browserType';
@ -133,12 +133,12 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
async close(): Promise<void> {
try {
if (this._shouldCloseConnectionOnClose)
this._connection.close(kBrowserClosedError);
this._connection.close();
else
await this._channel.close();
await this._closedPromise;
} catch (e) {
if (isSafeCloseError(e))
if (isTargetClosedError(e))
return;
throw e;
}

View File

@ -44,6 +44,7 @@ import { ConsoleMessage } from './consoleMessage';
import { Dialog } from './dialog';
import { WebError } from './webError';
import { parseError } from '../protocol/serializers';
import { TargetClosedError } from '../common/errors';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>();
@ -343,7 +344,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.BrowserContext.Close)
waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed'));
waiter.rejectOnEvent(this, Events.BrowserContext.Close, new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;

View File

@ -25,7 +25,6 @@ import type { ChildProcess } from 'child_process';
import { envObjectToArray } from './clientHelper';
import { assert, headersObjectToArray, monotonicTime } from '../utils';
import type * as api from '../../types/types';
import { kBrowserClosedError } from '../common/errors';
import { raceAgainstDeadline } from '../utils/timeoutRunner';
import type { Playwright } from './playwright';
@ -144,7 +143,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
connection.on('close', closePipe);
let browser: Browser;
let closeError: string | undefined;
let closeError: Error | undefined;
const onPipeClosed = () => {
// Emulate all pages, contexts and the browser closing upon disconnect.
for (const context of browser?.contexts() || []) {
@ -153,7 +152,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
context._onClose();
}
browser?._didClose();
connection.close(closeError || kBrowserClosedError);
connection.close(closeError);
};
pipe.on('closed', onPipeClosed);
connection.onmessage = message => pipe.send({ message }).catch(onPipeClosed);
@ -162,7 +161,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
try {
connection!.dispatch(message);
} catch (e) {
closeError = e.toString();
closeError = e;
closePipe();
}
});

View File

@ -35,7 +35,6 @@ import { WritableStream } from './writableStream';
import { debugLogger } from '../common/debugLogger';
import { SelectorsOwner } from './selectors';
import { Android, AndroidSocket, AndroidDevice } from './android';
import { captureLibraryStackText } from '../utils/stackTrace';
import { Artifact } from './artifact';
import { EventEmitter } from 'events';
import { JsonPipe } from './jsonPipe';
@ -45,6 +44,8 @@ import { Tracing } from './tracing';
import { findValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
import { createInstrumentation } from './clientInstrumentation';
import type { ClientInstrumentation } from './clientInstrumentation';
import { TargetClosedError } from '../common/errors';
import { formatCallLog, rewriteErrorMessage } from '../utils';
class Root extends ChannelOwner<channels.RootChannel> {
constructor(connection: Connection) {
@ -67,7 +68,7 @@ export class Connection extends EventEmitter {
private _lastId = 0;
private _callbacks = new Map<number, { resolve: (a: any) => void, reject: (a: Error) => void, apiName: string | undefined, type: string, method: string }>();
private _rootObject: Root;
private _closedErrorMessage: string | undefined;
private _closedError: Error | undefined;
private _isRemote = false;
private _localUtils?: LocalUtils;
// Some connections allow resolving in-process dispatchers.
@ -110,8 +111,8 @@ export class Connection extends EventEmitter {
}
async sendMessageToServer(object: ChannelOwner, method: string, params: any, apiName: string | undefined, frames: channels.StackFrame[], wallTime: number | undefined): Promise<any> {
if (this._closedErrorMessage)
throw new Error(this._closedErrorMessage);
if (this._closedError)
throw this._closedError;
if (object._wasCollected)
throw new Error('The object has been collected to prevent unbounded heap growth.');
@ -132,10 +133,10 @@ export class Connection extends EventEmitter {
}
dispatch(message: object) {
if (this._closedErrorMessage)
if (this._closedError)
return;
const { id, guid, method, params, result, error } = message as any;
const { id, guid, method, params, result, error, log } = message as any;
if (id) {
if (debugLogger.isEnabled('channel'))
debugLogger.log('channel', '<RECV ' + JSON.stringify(message));
@ -144,7 +145,9 @@ export class Connection extends EventEmitter {
throw new Error(`Cannot find command to respond: ${id}`);
this._callbacks.delete(id);
if (error && !result) {
callback.reject(parseError(error));
const parsedError = parseError(error);
rewriteErrorMessage(parsedError, parsedError.message + formatCallLog(log));
callback.reject(parsedError);
} else {
const validator = findValidator(callback.type, callback.method, 'Result');
callback.resolve(validator(result, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this.isRemote() ? 'fromBase64' : 'buffer' }));
@ -180,13 +183,12 @@ export class Connection extends EventEmitter {
(object._channel as any).emit(method, validator(params, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this.isRemote() ? 'fromBase64' : 'buffer' }));
}
close(errorMessage: string = 'Connection closed') {
const stack = captureLibraryStackText();
if (stack)
errorMessage += '\n ==== Closed by ====\n' + stack + '\n';
this._closedErrorMessage = errorMessage;
close(cause?: Error) {
this._closedError = cause || new TargetClosedError();
if (cause)
rewriteErrorMessage(this._closedError, this._closedError.message + '\nCaused by: ' + cause.toString());
for (const callback of this._callbacks.values())
callback.reject(new Error(errorMessage));
callback.reject(this._closedError);
this._callbacks.clear();
this.emit('close');
}

View File

@ -28,6 +28,7 @@ import { JSHandle, parseResult, serializeArgument } from './jsHandle';
import type { Page } from './page';
import type { Env, WaitForEventOptions, Headers, BrowserContextOptions } from './types';
import { Waiter } from './waiter';
import { TargetClosedError } from '../common/errors';
type ElectronOptions = Omit<channels.ElectronLaunchOptions, 'env'|'extraHTTPHeaders'|'recordHar'|'colorScheme'|'acceptDownloads'> & {
env?: Env,
@ -120,7 +121,7 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.ElectronApplication.Close)
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, new Error('Electron application closed'));
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;

View File

@ -21,7 +21,6 @@ import type { Serializable } from '../../types/structs';
import type * as api from '../../types/types';
import type { HeadersArray, NameValue } from '../common/types';
import type * as channels from '@protocol/channels';
import { kBrowserOrContextClosedError } from '../common/errors';
import { assert, headersObjectToArray, isString } from '../utils';
import { mkdirIfNeeded } from '../utils/fileUtils';
import { ChannelOwner } from './channelOwner';
@ -29,6 +28,7 @@ import { RawHeaders } from './network';
import type { FilePayload, Headers, StorageState } from './types';
import type { Playwright } from './playwright';
import { Tracing } from './tracing';
import { isTargetClosedError } from '../common/errors';
export type FetchOptions = {
params?: { [key: string]: string; },
@ -272,7 +272,7 @@ export class APIResponse implements api.APIResponse {
throw new Error('Response has been disposed');
return result.binary;
} catch (e) {
if (e.message.includes(kBrowserOrContextClosedError))
if (isTargetClosedError(e))
throw new Error('Response has been disposed');
throw e;
}

View File

@ -36,6 +36,7 @@ import { urlMatches } from '../utils/network';
import type * as api from '../../types/types';
import type * as structs from '../../types/structs';
import { debugLogger } from '../common/debugLogger';
import { TargetClosedError } from '../common/errors';
export type WaitForNavigationOptions = {
timeout?: number,
@ -104,8 +105,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
private _setupNavigationWaiter(options: { timeout?: number }): Waiter {
const waiter = new Waiter(this._page!, '');
if (this._page!.isClosed())
waiter.rejectImmediately(new Error('Navigation failed because page was closed!'));
waiter.rejectOnEvent(this._page!, Events.Page.Close, new Error('Navigation failed because page was closed!'));
waiter.rejectImmediately(new TargetClosedError());
waiter.rejectOnEvent(this._page!, Events.Page.Close, new TargetClosedError());
waiter.rejectOnEvent(this._page!, Events.Page.Crash, new Error('Navigation failed because page crashed!'));
waiter.rejectOnEvent<Frame>(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this);
const timeout = this._page!._timeoutSettings.navigationTimeout(options);

View File

@ -34,6 +34,7 @@ import { MultiMap } from '../utils/multimap';
import { APIResponse } from './fetch';
import type { Serializable } from '../../types/structs';
import type { BrowserContext } from './browserContext';
import { TargetClosedError } from '../common/errors';
export type NetworkCookie = {
name: string,
@ -610,7 +611,7 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));
if (event !== Events.WebSocket.Close)
waiter.rejectOnEvent(this, Events.WebSocket.Close, new Error('Socket closed'));
waiter.rejectOnEvent(this._page, Events.Page.Close, new Error('Page closed'));
waiter.rejectOnEvent(this._page, Events.Page.Close, new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;

View File

@ -19,7 +19,7 @@ import fs from 'fs';
import path from 'path';
import type * as structs from '../../types/structs';
import type * as api from '../../types/types';
import { isSafeCloseError, kBrowserOrContextClosedError } from '../common/errors';
import { isTargetClosedError, TargetClosedError, kTargetClosedErrorMessage } from '../common/errors';
import { urlMatches } from '../utils/network';
import { TimeoutSettings } from '../common/timeoutSettings';
import type * as channels from '@protocol/channels';
@ -140,8 +140,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this.coverage = new Coverage(this._channel);
this.once(Events.Page.Close, () => this._closedOrCrashedScope.close(kBrowserOrContextClosedError));
this.once(Events.Page.Crash, () => this._closedOrCrashedScope.close(kBrowserOrContextClosedError));
this.once(Events.Page.Close, () => this._closedOrCrashedScope.close(kTargetClosedErrorMessage));
this.once(Events.Page.Crash, () => this._closedOrCrashedScope.close(kTargetClosedErrorMessage));
this._setEventToSubscriptionMapping(new Map<string, channels.PageUpdateSubscriptionParams['event']>([
[Events.Page.Console, 'console'],
@ -398,7 +398,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
if (event !== Events.Page.Crash)
waiter.rejectOnEvent(this, Events.Page.Crash, new Error('Page crashed'));
if (event !== Events.Page.Close)
waiter.rejectOnEvent(this, Events.Page.Close, new Error('Page closed'));
waiter.rejectOnEvent(this, Events.Page.Close, new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose();
return result;
@ -520,7 +520,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
else
await this._channel.close(options);
} catch (e) {
if (isSafeCloseError(e) && !options.runBeforeUnload)
if (isTargetClosedError(e) && !options.runBeforeUnload)
return;
throw e;
}

View File

@ -23,7 +23,7 @@ import type { BrowserContext } from './browserContext';
import type * as api from '../../types/types';
import type * as structs from '../../types/structs';
import { LongStandingScope } from '../utils';
import { kBrowserOrContextClosedError } from '../common/errors';
import { kTargetClosedErrorMessage } from '../common/errors';
export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.Worker {
_page: Page | undefined; // Set for web workers.
@ -43,7 +43,7 @@ export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.
this._context._serviceWorkers.delete(this);
this.emit(Events.Worker.Close, this);
});
this.once(Events.Worker.Close, () => this._closedScope.close(kBrowserOrContextClosedError));
this.once(Events.Worker.Close, () => this._closedScope.close(kTargetClosedErrorMessage));
}
url(): string {

View File

@ -25,9 +25,15 @@ class CustomError extends Error {
export class TimeoutError extends CustomError {}
export const kBrowserClosedError = 'Browser has been closed';
export const kBrowserOrContextClosedError = 'Target page, context or browser has been closed';
export const kTargetClosedErrorMessage = 'Target page, context or browser has been closed';
export function isSafeCloseError(error: Error) {
return error.message.endsWith(kBrowserClosedError) || error.message.endsWith(kBrowserOrContextClosedError);
export class TargetClosedError extends Error {
constructor() {
super(kTargetClosedErrorMessage);
this.name = this.constructor.name;
}
}
export function isTargetClosedError(error: Error) {
return error instanceof TargetClosedError || error.message.includes(kTargetClosedErrorMessage);
}

View File

@ -25,6 +25,7 @@ import { debugLogger } from '../../common/debugLogger';
import type { ProtocolLogger } from '../types';
import { helper } from '../helper';
import { ProtocolError } from '../protocolError';
import { kTargetClosedErrorMessage } from '../../common/errors';
export const ConnectionEvents = {
Disconnected: Symbol('ConnectionEvents.Disconnected')
@ -140,12 +141,10 @@ export class CRSession extends EventEmitter {
private _closedErrorMessage() {
if (this._crashed)
return 'Target crashed';
if (this._connection._browserDisconnectedLogs !== undefined)
return `Browser closed.` + this._connection._browserDisconnectedLogs;
if (this._closed)
return `Target closed`;
if (this._connection._closed)
return 'Browser closed';
if (this._connection._browserDisconnectedLogs)
return kTargetClosedErrorMessage + '\nBrowser logs: ' + this._connection._browserDisconnectedLogs;
if (this._closed || this._connection._closed)
return kTargetClosedErrorMessage;
}
async send<T extends keyof Protocol.CommandParameters>(

View File

@ -45,6 +45,7 @@ import { platformToFontFamilies } from './defaultFontFamilies';
import type { Protocol } from './protocol';
import { VideoRecorder } from './videoRecorder';
import { BrowserContext } from '../browserContext';
import { TargetClosedError } from '../../common/errors';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -574,7 +575,7 @@ class FrameSession {
}
dispose() {
this._firstNonInitialNavigationCommittedReject(new Error('Page closed'));
this._firstNonInitialNavigationCommittedReject(new TargetClosedError());
for (const childSession of this._childSessions)
childSession.dispose();
if (this._parentSession)

View File

@ -19,10 +19,9 @@ import type * as channels from '@protocol/channels';
import { serializeError } from '../../protocol/serializers';
import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator';
import { assert, isUnderTest, monotonicTime } from '../../utils';
import { kBrowserOrContextClosedError } from '../../common/errors';
import { TargetClosedError } from '../../common/errors';
import type { CallMetadata } from '../instrumentation';
import { SdkObject } from '../instrumentation';
import { rewriteErrorMessage } from '../../utils/stackTrace';
import type { PlaywrightDispatcher } from './playwrightDispatcher';
import { eventsHelper } from '../..//utils/eventsHelper';
import type { RegisteredListener } from '../..//utils/eventsHelper';
@ -262,7 +261,7 @@ export class DispatcherConnection {
const { id, guid, method, params, metadata } = message as any;
const dispatcher = this._dispatchers.get(guid);
if (!dispatcher) {
this.onmessage({ id, error: serializeError(new Error(kBrowserOrContextClosedError)) });
this.onmessage({ id, error: serializeError(new TargetClosedError()) });
return;
}
@ -324,20 +323,13 @@ export class DispatcherConnection {
}
}
let error: any;
await sdkObject?.instrumentation.onBeforeCall(sdkObject, callMetadata);
try {
const result = await (dispatcher as any)[method](validParams, callMetadata);
const validator = findValidator(dispatcher._type, method, 'Result');
callMetadata.result = validator(result, '', { tChannelImpl: this._tChannelImplToWire.bind(this), binary: this._isLocal ? 'buffer' : 'toBase64' });
} catch (e) {
// Dispatching error
// We want original, unmodified error in metadata.
callMetadata.error = serializeError(e);
if (callMetadata.log.length)
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log));
error = serializeError(e);
} finally {
callMetadata.endTime = monotonicTime();
await sdkObject?.instrumentation.onAfterCall(sdkObject, callMetadata);
@ -346,18 +338,10 @@ export class DispatcherConnection {
const response: any = { id };
if (callMetadata.result)
response.result = callMetadata.result;
if (error)
response.error = error;
if (callMetadata.error) {
response.error = callMetadata.error;
response.log = callMetadata.log;
}
this.onmessage(response);
}
}
function formatLogRecording(log: string[]): string {
if (!log.length)
return '';
const header = ` logs `;
const headerLength = 60;
const leftLength = (headerLength - header.length) / 2;
const rightLength = headerLength - header.length - leftLength;
return `\n${'='.repeat(leftLength)}${header}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`;
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { kBrowserClosedError } from '../../common/errors';
import { kTargetClosedErrorMessage } from '../../common/errors';
import { assert } from '../../utils';
import type { BrowserOptions } from '../browser';
import { Browser } from '../browser';
@ -159,7 +159,7 @@ export class FFBrowser extends Browser {
_onDisconnect() {
for (const video of this._idToVideo.values())
video.artifact.reportFinished(kBrowserClosedError);
video.artifact.reportFinished(kTargetClosedErrorMessage);
this._idToVideo.clear();
for (const ffPage of this._ffPages.values())
ffPage.didClose();

View File

@ -24,6 +24,7 @@ import { debugLogger } from '../../common/debugLogger';
import type { ProtocolLogger } from '../types';
import { helper } from '../helper';
import { ProtocolError } from '../protocolError';
import { kTargetClosedErrorMessage } from '../../common/errors';
export const ConnectionEvents = {
Disconnected: Symbol('Disconnected'),
@ -134,12 +135,10 @@ export class FFSession extends EventEmitter {
private _closedErrorMessage() {
if (this._crashed)
return 'Target crashed';
if (this._connection._browserDisconnectedLogs !== undefined)
return `Browser closed.` + this._connection._browserDisconnectedLogs;
if (this._disposed)
return `Target closed`;
if (this._connection._closed)
return 'Browser closed';
if (this._connection._browserDisconnectedLogs)
return kTargetClosedErrorMessage + '\nBrowser logs: ' + this._connection._browserDisconnectedLogs;
if (this._disposed || this._connection._closed)
return kTargetClosedErrorMessage;
}
async send<T extends keyof Protocol.CommandParameters>(

View File

@ -35,6 +35,7 @@ import { splitErrorMessage } from '../../utils/stackTrace';
import { debugLogger } from '../../common/debugLogger';
import { ManualPromise } from '../../utils/manualPromise';
import { BrowserContext } from '../browserContext';
import { TargetClosedError } from '../../common/errors';
export const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -342,7 +343,7 @@ export class FFPage implements PageDelegate {
}
didClose() {
this._markAsError(new Error('Page closed'));
this._markAsError(new TargetClosedError());
this._session.dispose();
eventsHelper.removeEventListeners(this._eventListeners);
this._networkManager.dispose();

View File

@ -66,7 +66,7 @@ export class FrameSelectors {
const resolved = await this.resolveInjectedForSelector(selector, { mainWorld: true }, scope);
// Be careful, |this.frame| can be different from |resolved.frame|.
if (!resolved)
throw new Error(`Error: failed to find frame for selector "${selector}"`);
throw new Error(`Failed to find frame for selector "${selector}"`);
return await resolved.injected.evaluateHandle((injected, { info, scope }) => {
return injected.querySelectorAll(info.parsed, scope || document);
}, { info: resolved.info, scope: resolved.scope });
@ -76,7 +76,7 @@ export class FrameSelectors {
const resolved = await this.resolveInjectedForSelector(selector);
// Be careful, |this.frame| can be different from |resolved.frame|.
if (!resolved)
throw new Error(`Error: failed to find frame for selector "${selector}"`);
throw new Error(`Failed to find frame for selector "${selector}"`);
return await resolved.injected.evaluate((injected, { info }) => {
return injected.querySelectorAll(info.parsed, document).length;
}, { info: resolved.info });

View File

@ -834,7 +834,7 @@ export class Frame extends SdkObject {
async evalOnSelector(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle): Promise<any> {
const handle = await this.selectors.query(selector, { strict }, scope);
if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
throw new Error(`Failed to find element matching selector "${selector}"`);
const result = await handle.evaluateExpression(expression, { isFunction }, arg);
handle.dispose();
return result;

View File

@ -98,7 +98,7 @@ class Helper {
static formatBrowserLogs(logs: string[]) {
if (!logs.length)
return '';
return '\n' + '='.repeat(20) + ' Browser output: ' + '='.repeat(20) + '\n' + logs.join('\n');
return '\n' + logs.join('\n');
}
}

View File

@ -43,6 +43,7 @@ import type { TimeoutOptions } from '../common/types';
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers';
import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
import { kTargetClosedErrorMessage } from '../common/errors';
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
@ -275,7 +276,7 @@ export class Page extends SdkObject {
this.emit(Page.Events.Close);
this._closedPromise.resolve();
this.instrumentation.onPageClose(this);
this.openScope.close('Page closed');
this.openScope.close(kTargetClosedErrorMessage);
}
_didCrash() {

View File

@ -30,7 +30,7 @@ import type { Protocol } from './protocol';
import type { PageProxyMessageReceivedPayload } from './wkConnection';
import { kPageProxyMessageReceived, WKConnection, WKSession } from './wkConnection';
import { WKPage } from './wkPage';
import { kBrowserClosedError } from '../../common/errors';
import { kTargetClosedErrorMessage } from '../../common/errors';
import type { SdkObject } from '../instrumentation';
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15';
@ -81,7 +81,7 @@ export class WKBrowser extends Browser {
wkPage.didClose();
this._wkPages.clear();
for (const video of this._idToVideo.values())
video.artifact.reportFinished(kBrowserClosedError);
video.artifact.reportFinished(kTargetClosedErrorMessage);
this._idToVideo.clear();
this._didClose();
}
@ -165,7 +165,7 @@ export class WKBrowser extends Browser {
context = this._defaultContext as WKBrowserContext;
if (!context)
return;
const pageProxySession = new WKSession(this._connection, pageProxyId, `Target closed`, (message: any) => {
const pageProxySession = new WKSession(this._connection, pageProxyId, kTargetClosedErrorMessage, (message: any) => {
this._connection.rawSend({ ...message, pageProxyId });
});
const opener = event.openerId ? this._wkPages.get(event.openerId) : undefined;

View File

@ -24,7 +24,7 @@ import type { RecentLogsCollector } from '../../common/debugLogger';
import { debugLogger } from '../../common/debugLogger';
import type { ProtocolLogger } from '../types';
import { helper } from '../helper';
import { kBrowserClosedError } from '../../common/errors';
import { kTargetClosedErrorMessage } from '../../common/errors';
import { ProtocolError } from '../protocolError';
// WKPlaywright uses this special id to issue Browser.close command which we
@ -51,7 +51,7 @@ export class WKConnection {
this._onDisconnect = onDisconnect;
this._protocolLogger = protocolLogger;
this._browserLogsCollector = browserLogsCollector;
this.browserSession = new WKSession(this, '', kBrowserClosedError, (message: any) => {
this.browserSession = new WKSession(this, '', kTargetClosedErrorMessage, (message: any) => {
this.rawSend(message);
});
this._transport.onmessage = this._dispatchMessage.bind(this);
@ -137,7 +137,7 @@ export class WKSession extends EventEmitter {
if (this._crashed)
throw new ProtocolError(true, 'Target crashed');
if (this._disposed)
throw new ProtocolError(true, `Target closed`);
throw new ProtocolError(true, kTargetClosedErrorMessage);
const id = this.connection.nextMessageId();
const messageObj = { id, method, params };
this._rawSend(messageObj);
@ -160,7 +160,7 @@ export class WKSession extends EventEmitter {
dispose() {
if (this.connection._browserDisconnectedLogs)
this.errorText = 'Browser closed.' + this.connection._browserDisconnectedLogs;
this.errorText = kTargetClosedErrorMessage + '\nBrowser logs: ' + this.connection._browserDisconnectedLogs;
for (const callback of this._callbacks.values()) {
callback.error.sessionClosed = true;
callback.reject(rewriteErrorMessage(callback.error, this.errorText));

View File

@ -45,6 +45,7 @@ import { WKWorkers } from './wkWorkers';
import { debugLogger } from '../../common/debugLogger';
import { ManualPromise } from '../../utils/manualPromise';
import { BrowserContext } from '../browserContext';
import { TargetClosedError, kTargetClosedErrorMessage } from '../../common/errors';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -271,7 +272,7 @@ export class WKPage implements PageDelegate {
this._provisionalPage.dispose();
this._provisionalPage = null;
}
this._firstNonInitialNavigationCommittedReject(new Error('Page closed'));
this._firstNonInitialNavigationCommittedReject(new TargetClosedError());
this._page._didClose();
}
@ -303,7 +304,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, `Target closed`, (message: any) => {
const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, kTargetClosedErrorMessage, (message: any) => {
this._pageProxySession.send('Target.sendMessageToTarget', {
message: JSON.stringify(message), targetId: targetInfo.targetId
}).catch(e => {
@ -524,7 +525,7 @@ export class WKPage implements PageDelegate {
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
if (this._pageProxySession.isDisposed())
throw new Error('Target closed');
throw new TargetClosedError();
const pageProxyId = this._pageProxySession.sessionId;
const result = await this._pageProxySession.connection.browserSession.send('Playwright.navigate', { url, pageProxyId, frameId: frame._id, referrer });
return { newDocumentId: result.loaderId };

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { rewriteErrorMessage } from './stackTrace';
import { captureRawStack } from './stackTrace';
export class ManualPromise<T = void> extends Promise<T> {
private _resolve!: (t: T) => void;
@ -59,7 +59,7 @@ export class ManualPromise<T = void> extends Promise<T> {
export class LongStandingScope {
private _terminateError: Error | undefined;
private _terminateErrorMessage: string | undefined;
private _terminatePromises = new Map<ManualPromise<Error>, Error>();
private _terminatePromises = new Map<ManualPromise<Error>, string[]>();
private _isClosed = false;
reject(error: Error) {
@ -72,9 +72,10 @@ export class LongStandingScope {
close(errorMessage: string) {
this._isClosed = true;
this._terminateErrorMessage = errorMessage;
for (const [p, e] of this._terminatePromises) {
rewriteErrorMessage(e, errorMessage);
p.resolve(e);
for (const [p, frames] of this._terminatePromises) {
const error = new Error(errorMessage);
error.stack = [error.name + ':' + errorMessage, ...frames].join('\n');
p.resolve(error);
}
}
@ -100,7 +101,7 @@ export class LongStandingScope {
terminatePromise.resolve(this._terminateError);
if (this._terminateErrorMessage)
terminatePromise.resolve(new Error(this._terminateErrorMessage));
this._terminatePromises.set(terminatePromise, new Error(''));
this._terminatePromises.set(terminatePromise, captureRawStack());
try {
return await Promise.race([
terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)),

View File

@ -18,6 +18,7 @@ import path from 'path';
import { parseStackTraceLine } from '../utilsBundle';
import { isUnderTest } from './';
import type { StackFrame } from '@protocol/channels';
import { colors } from '../utilsBundle';
export function rewriteErrorMessage<E extends Error>(e: E, newMessage: string): E {
const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at '));
@ -131,6 +132,15 @@ export function splitErrorMessage(message: string): { name: string, message: str
};
}
export function formatCallLog(log: string[] | undefined): string {
if (!log || !log.some(l => !!l))
return '';
return `
Call log:
${colors.dim('- ' + (log || []).join('\n - '))}
`;
}
export type ExpectZone = {
title: string;
wallTime: number;

View File

@ -20,7 +20,8 @@ import type { StackFrame } from '@protocol/channels';
import util from 'util';
import path from 'path';
import url from 'url';
import { colors, debug, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
import { debug, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
import { formatCallLog } from 'playwright-core/lib/utils';
import type { TestInfoError } from './../types/test';
import type { Location } from './../types/testReporter';
import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
@ -213,14 +214,7 @@ export function getContainedPath(parentPath: string, subPath: string = ''): stri
export const debugTest = debug('pw:test');
export function callLogText(log: string[] | undefined): string {
if (!log)
return '';
return `
Call log:
${colors.dim('- ' + (log || []).join('\n - '))}
`;
}
export const callLogText = formatCallLog;
const folderToPackageJsonPath = new Map<string, string>();

17
tests/config/errors.ts Normal file
View File

@ -0,0 +1,17 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* 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 const kTargetClosedErrorMessage = 'Target page, context or browser has been closed';

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { kTargetClosedErrorMessage } from '../config/errors';
import { browserTest as test, expect } from '../config/browserTest';
test('should return browserType', function({ browser, browserType }) {
@ -59,5 +60,5 @@ test('should dispatch page.on(close) upon browser.close and reject evaluate', as
await browser.close();
expect(closed).toBe(true);
const error = await promise;
expect(error.message).toMatch(/(Target|Browser) closed/);
expect(error.message).toContain(kTargetClosedErrorMessage);
});

View File

@ -15,6 +15,7 @@
* limitations under the License.
*/
import { kTargetClosedErrorMessage } from '../config/errors';
import { browserTest as it, expect } from '../config/browserTest';
import { attachFrame, verifyViewport } from '../config/utils';
@ -129,7 +130,7 @@ it('close() should abort waitForEvent', async ({ browser }) => {
const promise = context.waitForEvent('page').catch(e => e);
await context.close();
const error = await promise;
expect(error.message).toContain('Context closed');
expect(error.message).toContain(kTargetClosedErrorMessage);
});
it('close() should be callable twice', async ({ browser }) => {

View File

@ -27,6 +27,7 @@ import { parseTrace, suppressCertificateWarning } from '../config/utils';
import formidable from 'formidable';
import type { Browser, ConnectOptions } from 'playwright-core';
import { createHttpServer } from '../../packages/playwright-core/lib/utils/network';
import { kTargetClosedErrorMessage } from '../config/errors';
type ExtraFixtures = {
connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise<Browser>,
@ -336,7 +337,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
]);
expect(browser.isConnected()).toBe(false);
const error = await page.waitForNavigation().catch(e => e);
expect(error.message).toContain('Navigation failed because page was closed');
expect(error.message).toContain(kTargetClosedErrorMessage);
});
test('should reject navigation when browser closes', async ({ connect, startRemoteServer, server }) => {
@ -391,7 +392,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
]);
for (let i = 0; i < 2; i++) {
const message = results[i].message;
expect(message).toContain('Page closed');
expect(message).toContain(kTargetClosedErrorMessage);
expect(message).not.toContain('Timeout');
}
});
@ -549,13 +550,11 @@ for (const kind of ['launchServer', 'run-server'] as const) {
await disconnectedPromise;
expect(browser.isConnected()).toBe(false);
const navMessage = (await navigationPromise).message;
expect(navMessage).toContain('Connection closed');
expect(navMessage).toContain('Closed by');
expect(navMessage).toContain(__filename);
expect((await waitForNavigationPromise).message).toContain('Navigation failed because page was closed');
const navError = await navigationPromise;
expect(navError.message).toContain(kTargetClosedErrorMessage);
expect((await waitForNavigationPromise).message).toContain(kTargetClosedErrorMessage);
expect((await page.goto(server.EMPTY_PAGE).catch(e => e)).message).toContain('has been closed');
expect((await page.waitForNavigation().catch(e => e)).message).toContain('Navigation failed because page was closed');
expect((await page.waitForNavigation().catch(e => e)).message).toContain(kTargetClosedErrorMessage);
});
test('should be able to connect when the wsEndpoint is passed as an option', async ({ browserType, startRemoteServer }) => {
@ -894,9 +893,9 @@ test.describe('launchServer only', () => {
expect(browser.isConnected()).toBe(false);
expect((await navigationPromise).message).toContain('has been closed');
expect((await waitForNavigationPromise).message).toContain('Navigation failed because page was closed');
expect((await waitForNavigationPromise).message).toContain(kTargetClosedErrorMessage);
expect((await page.goto(server.EMPTY_PAGE).catch(e => e)).message).toContain('has been closed');
expect((await page.waitForNavigation().catch(e => e)).message).toContain('Navigation failed because page was closed');
expect((await page.waitForNavigation().catch(e => e)).message).toContain(kTargetClosedErrorMessage);
});
test('should be able to reconnect to a browser 12 times without warnings', async ({ connect, startRemoteServer, server }) => {

View File

@ -64,7 +64,7 @@ it('should reject if launched browser fails immediately', async ({ mode, browser
let waitError = null;
await browserType.launch({ executablePath: asset('dummy_bad_browser_executable.js') }).catch(e => waitError = e);
expect(waitError.message).toContain('== logs ==');
expect(waitError.message).toContain('Browser logs:');
});
it('should reject if executable path is invalid', async ({ browserType, mode }) => {

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { kTargetClosedErrorMessage } from '../../config/errors';
import { contextTest as it, expect } from '../../config/browserTest';
import { browserTest } from '../../config/browserTest';
@ -139,7 +140,7 @@ browserTest('should reject protocol calls when page closes', async function({ br
const promise = session.send('Runtime.evaluate', { expression: 'new Promise(() => {})', awaitPromise: true }).catch(e => e);
await page.close();
const error1 = await promise;
expect(error1.message).toContain('Target closed');
expect(error1.message).toContain(kTargetClosedErrorMessage);
const error2 = await session.send('Runtime.evaluate', { expression: 'new Promise(() => {})', awaitPromise: true }).catch(e => e);
expect(error2.message).toContain('Target page, context or browser has been closed');
await context.close();

View File

@ -15,6 +15,7 @@
* limitations under the License.
*/
import { kTargetClosedErrorMessage } from '../config/errors';
import { contextTest as it, expect } from '../config/browserTest';
import { Server as WebSocketServer } from 'ws';
@ -196,7 +197,7 @@ it('should reject waitForEvent on page close', async ({ page, server }) => {
]);
const error = ws.waitForEvent('framesent').catch(e => e);
await page.close();
expect((await error).message).toContain('Page closed');
expect((await error).message).toContain(kTargetClosedErrorMessage);
});
it('should turn off when offline', async ({ page }) => {

View File

@ -37,7 +37,7 @@ it('should throw in case of missing selector', async ({ page, server }) => {
await page.setContent(htmlContent);
const elementHandle = await page.$('#myId');
const errorMessage = await elementHandle.$eval('.a', node => (node as HTMLElement).innerText).catch(error => error.message);
expect(errorMessage).toContain(`Error: failed to find element matching selector ".a"`);
expect(errorMessage).toContain(`elementHandle.$eval: Failed to find element matching selector ".a"`);
});
it('should work for all', async ({ page, server }) => {

View File

@ -99,7 +99,7 @@ it('should accept ElementHandles as arguments', async ({ page, server }) => {
it('should throw error if no element is found', async ({ page, server }) => {
let error = null;
await page.$eval('section', e => e.id).catch(e => error = e);
expect(error.message).toContain('failed to find element matching selector "section"');
expect(error.message).toContain('Failed to find element matching selector "section"');
});
it('should support >> syntax', async ({ page, server }) => {

View File

@ -15,6 +15,7 @@
* limitations under the License.
*/
import { kTargetClosedErrorMessage } from '../config/errors';
import { test as it, expect } from './pageTest';
it('should reject all promises when page is closed', async ({ page, isWebView2, isAndroid }) => {
@ -26,7 +27,7 @@ it('should reject all promises when page is closed', async ({ page, isWebView2,
page.evaluate(() => new Promise(r => {})).catch(e => error = e),
page.close(),
]);
expect(error.message).toContain('Target closed');
expect(error.message).toContain(kTargetClosedErrorMessage);
});
it('should set the page close state', async ({ page, isWebView2 }) => {
@ -59,7 +60,7 @@ it('should terminate network waiters', async ({ page, server, isAndroid, isWebVi
]);
for (let i = 0; i < 2; i++) {
const message = results[i].message;
expect(message).toContain('Page closed');
expect(message).toContain(kTargetClosedErrorMessage);
expect(message).not.toContain('Timeout');
}
});
@ -142,7 +143,7 @@ it('should fail with error upon disconnect', async ({ page, isAndroid, isWebView
const waitForPromise = page.waitForEvent('download').catch(e => error = e);
await page.close();
await waitForPromise;
expect(error.message).toContain('Page closed');
expect(error.message).toContain(kTargetClosedErrorMessage);
});
it('page.url should work', async ({ page, server }) => {

View File

@ -198,7 +198,7 @@ it('should throw nice error without injected script stack when element is not an
let error = null;
await page.goto(server.PREFIX + '/input/textarea.html');
await page.fill('body', '').catch(e => error = e);
expect(error.message).toContain('page.fill: Error: Element is not an <input>, <textarea> or [contenteditable] element\n=========================== logs');
expect(error.message).toContain('page.fill: Error: Element is not an <input>, <textarea> or [contenteditable] element\nCall log:');
});
it('should throw if passed a non-string value', async ({ page, server }) => {

View File

@ -113,12 +113,12 @@ it('$eval should throw for missing frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
{
const error = await page.$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find element matching selector');
expect(error.message).toContain('page.$eval: Failed to find element matching selector');
}
{
const body = await page.$('body');
const error = await body.$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find element matching selector');
expect(error.message).toContain('elementHandle.$eval: Failed to find element matching selector');
}
});
@ -126,12 +126,12 @@ it('$$eval should throw for missing frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
{
const error = await page.$$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find frame for selector');
expect(error.message).toContain('page.$$eval: Failed to find frame for selector');
}
{
const body = await page.$('body');
const error = await body.$$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find frame for selector');
expect(error.message).toContain('Failed to find frame for selector');
}
});

View File

@ -20,6 +20,7 @@ import type { Worker as PwWorker } from '@playwright/test';
import { attachFrame } from '../config/utils';
import type { ConsoleMessage } from 'playwright-core';
import fs from 'fs';
import { kTargetClosedErrorMessage } from '../config/errors';
it('Page.workers @smoke', async function({ page, server }) {
await Promise.all([
@ -43,7 +44,8 @@ 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).toMatch(/jsHandle.getProperty: (Worker was closed|Target closed)/);
expect(error.message).toContain('jsHandle.getProperty');
expect(error.message).toContain(kTargetClosedErrorMessage);
});
it('should report console logs', async function({ page }) {

View File

@ -339,7 +339,7 @@ test('should report error and pending operations on timeout', async ({ runInline
expect(result.exitCode).toBe(1);
expect(result.passed).toBe(0);
expect(result.failed).toBe(1);
expect(result.output).toContain('Error: locator.textContent: Page closed');
expect(result.output).toContain('Error: locator.textContent: Target page, context or browser has been closed');
expect(result.output).toContain('a.test.ts:7:42');
});

View File

@ -454,7 +454,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
`begin {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`,
`end {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`,
`begin {\"title\":\"page.click(input)\",\"category\":\"pw:api\"}`,
`end {\"title\":\"page.click(input)\",\"category\":\"pw:api\",\"error\":{\"message\":\"page.click: Timeout 1ms exceeded.\\n=========================== logs ===========================\\nwaiting for locator('input')\\n============================================================\",\"stack\":\"<stack>\",\"location\":\"<location>\",\"snippet\":\"<snippet>\"}}`,
`end {\"title\":\"page.click(input)\",\"category\":\"pw:api\",\"error\":{\"message\":\"page.click: Timeout 1ms exceeded.\\nCall log:\\n \\u001b[2m- waiting for locator('input')\\u001b[22m\\n\",\"stack\":\"<stack>\",\"location\":\"<location>\",\"snippet\":\"<snippet>\"}}`,
`begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
`begin {\"title\":\"fixture: page\",\"category\":\"fixture\"}`,
`end {\"title\":\"fixture: page\",\"category\":\"fixture\"}`,