chore: make client-side instrumentation non-nullable (#22694)
This commit is contained in:
parent
b555d33e38
commit
e9373dfb6e
|
|
@ -86,11 +86,13 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
||||||
}
|
}
|
||||||
|
|
||||||
async newPage(options: BrowserContextOptions = {}): Promise<Page> {
|
async newPage(options: BrowserContextOptions = {}): Promise<Page> {
|
||||||
const context = await this.newContext(options);
|
return await this._wrapApiCall(async () => {
|
||||||
const page = await context.newPage();
|
const context = await this.newContext(options);
|
||||||
page._ownedContext = context;
|
const page = await context.newPage();
|
||||||
context._ownerPage = page;
|
page._ownedContext = context;
|
||||||
return page;
|
context._ownerPage = page;
|
||||||
|
return page;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isConnected(): boolean {
|
isConnected(): boolean {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ import { Tracing } from './tracing';
|
||||||
import type { BrowserType } from './browserType';
|
import type { BrowserType } from './browserType';
|
||||||
import { Artifact } from './artifact';
|
import { Artifact } from './artifact';
|
||||||
import { APIRequestContext } from './fetch';
|
import { APIRequestContext } from './fetch';
|
||||||
import { createInstrumentation } from './clientInstrumentation';
|
|
||||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||||
import { HarRouter } from './harRouter';
|
import { HarRouter } from './harRouter';
|
||||||
|
|
||||||
|
|
@ -69,7 +68,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) {
|
||||||
super(parent, type, guid, initializer, createInstrumentation());
|
super(parent, type, guid, initializer);
|
||||||
if (parent instanceof Browser)
|
if (parent instanceof Browser)
|
||||||
this._browser = parent;
|
this._browser = parent;
|
||||||
this._browser?._contexts.add(this);
|
this._browser?._contexts.add(this);
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
_defaultContextOptions?: BrowserContextOptions;
|
_defaultContextOptions?: BrowserContextOptions;
|
||||||
private _defaultLaunchOptions?: LaunchOptions;
|
private _defaultLaunchOptions?: LaunchOptions;
|
||||||
private _defaultConnectOptions?: ConnectOptions;
|
private _defaultConnectOptions?: ConnectOptions;
|
||||||
private _onDidCreateContext?: (context: BrowserContext) => Promise<void>;
|
|
||||||
private _onWillCloseContext?: (context: BrowserContext) => Promise<void>;
|
|
||||||
|
|
||||||
static from(browserType: channels.BrowserTypeChannel): BrowserType {
|
static from(browserType: channels.BrowserTypeChannel): BrowserType {
|
||||||
return (browserType as any)._object;
|
return (browserType as any)._object;
|
||||||
|
|
@ -254,11 +252,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
context._browserType = this;
|
context._browserType = this;
|
||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
context._setOptions(contextOptions, browserOptions);
|
context._setOptions(contextOptions, browserOptions);
|
||||||
await this._onDidCreateContext?.(context);
|
await this._instrumentation.onDidCreateBrowserContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _willCloseContext(context: BrowserContext) {
|
async _willCloseContext(context: BrowserContext) {
|
||||||
this._contexts.delete(context);
|
this._contexts.delete(context);
|
||||||
await this._onWillCloseContext?.(context);
|
await this._instrumentation.onWillCloseBrowserContext(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,17 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
readonly _channel: T;
|
readonly _channel: T;
|
||||||
readonly _initializer: channels.InitializerTraits<T>;
|
readonly _initializer: channels.InitializerTraits<T>;
|
||||||
_logger: Logger | undefined;
|
_logger: Logger | undefined;
|
||||||
_instrumentation: ClientInstrumentation | undefined;
|
readonly _instrumentation: ClientInstrumentation;
|
||||||
private _eventToSubscriptionMapping: Map<string, string> = new Map();
|
private _eventToSubscriptionMapping: Map<string, string> = new Map();
|
||||||
|
|
||||||
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>, instrumentation?: ClientInstrumentation) {
|
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>) {
|
||||||
super();
|
super();
|
||||||
this.setMaxListeners(0);
|
this.setMaxListeners(0);
|
||||||
this._connection = parent instanceof ChannelOwner ? parent._connection : parent;
|
this._connection = parent instanceof ChannelOwner ? parent._connection : parent;
|
||||||
this._type = type;
|
this._type = type;
|
||||||
this._guid = guid;
|
this._guid = guid;
|
||||||
this._parent = parent instanceof ChannelOwner ? parent : undefined;
|
this._parent = parent instanceof ChannelOwner ? parent : undefined;
|
||||||
this._instrumentation = instrumentation || this._parent?._instrumentation;
|
this._instrumentation = this._connection._instrumentation;
|
||||||
|
|
||||||
this._connection._objects.set(guid, this);
|
this._connection._objects.set(guid, this);
|
||||||
if (this._parent) {
|
if (this._parent) {
|
||||||
|
|
@ -179,7 +179,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
try {
|
try {
|
||||||
logApiCall(logger, `=> ${apiName} started`, isInternal);
|
logApiCall(logger, `=> ${apiName} started`, isInternal);
|
||||||
const apiZone = { stackTrace, isInternal, reported: false, csi, callCookie, wallTime };
|
const apiZone = { stackTrace, isInternal, reported: false, csi, callCookie, wallTime };
|
||||||
const result = await zones.run<ApiZone, R>('apiZone', apiZone, async () => {
|
const result = await zones.run<ApiZone, Promise<R>>('apiZone', apiZone, async () => {
|
||||||
return await func(apiZone);
|
return await func(apiZone);
|
||||||
});
|
});
|
||||||
csi?.onApiCallEnd(callCookie);
|
csi?.onApiCallEnd(callCookie);
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,30 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ParsedStackTrace } from '../utils/stackTrace';
|
import type { ParsedStackTrace } from '../utils/stackTrace';
|
||||||
|
import type { BrowserContext } from './browserContext';
|
||||||
|
import type { APIRequestContext } from './fetch';
|
||||||
|
|
||||||
export interface ClientInstrumentation {
|
export interface ClientInstrumentation {
|
||||||
addListener(listener: ClientInstrumentationListener): void;
|
addListener(listener: ClientInstrumentationListener): void;
|
||||||
removeListener(listener: ClientInstrumentationListener): void;
|
removeListener(listener: ClientInstrumentationListener): void;
|
||||||
removeAllListeners(): void;
|
removeAllListeners(): void;
|
||||||
onApiCallBegin(apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any): void;
|
onApiCallBegin(apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any): void;
|
||||||
onApiCallEnd(userData: any, error?: Error): any;
|
onApiCallEnd(userData: any, error?: Error): void;
|
||||||
|
onDidCreateBrowserContext(context: BrowserContext): Promise<void>;
|
||||||
|
onDidCreateRequestContext(context: APIRequestContext): Promise<void>;
|
||||||
|
onWillPause(): void;
|
||||||
|
onWillCloseBrowserContext(context: BrowserContext): Promise<void>;
|
||||||
|
onWillCloseRequestContext(context: APIRequestContext): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientInstrumentationListener {
|
export interface ClientInstrumentationListener {
|
||||||
onApiCallBegin?(apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any): any;
|
onApiCallBegin?(apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any): void;
|
||||||
onApiCallEnd?(userData: any, error?: Error): any;
|
onApiCallEnd?(userData: any, error?: Error): void;
|
||||||
|
onDidCreateBrowserContext?(context: BrowserContext): Promise<void>;
|
||||||
|
onDidCreateRequestContext?(context: APIRequestContext): Promise<void>;
|
||||||
|
onWillPause?(): void;
|
||||||
|
onWillCloseBrowserContext?(context: BrowserContext): Promise<void>;
|
||||||
|
onWillCloseRequestContext?(context: APIRequestContext): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInstrumentation(): ClientInstrumentation {
|
export function createInstrumentation(): ClientInstrumentation {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import { APIRequestContext } from './fetch';
|
||||||
import { LocalUtils } from './localUtils';
|
import { LocalUtils } from './localUtils';
|
||||||
import { Tracing } from './tracing';
|
import { Tracing } from './tracing';
|
||||||
import { findValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
|
import { findValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
|
||||||
|
import { createInstrumentation } from './clientInstrumentation';
|
||||||
|
|
||||||
class Root extends ChannelOwner<channels.RootChannel> {
|
class Root extends ChannelOwner<channels.RootChannel> {
|
||||||
constructor(connection: Connection) {
|
constructor(connection: Connection) {
|
||||||
|
|
@ -72,6 +73,7 @@ export class Connection extends EventEmitter {
|
||||||
// Some connections allow resolving in-process dispatchers.
|
// Some connections allow resolving in-process dispatchers.
|
||||||
toImpl: ((client: ChannelOwner) => any) | undefined;
|
toImpl: ((client: ChannelOwner) => any) | undefined;
|
||||||
private _tracingCount = 0;
|
private _tracingCount = 0;
|
||||||
|
readonly _instrumentation = createInstrumentation();
|
||||||
|
|
||||||
constructor(localUtils?: LocalUtils) {
|
constructor(localUtils?: LocalUtils) {
|
||||||
super();
|
super();
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ import { ChannelOwner } from './channelOwner';
|
||||||
import { RawHeaders } from './network';
|
import { RawHeaders } from './network';
|
||||||
import type { FilePayload, Headers, StorageState } from './types';
|
import type { FilePayload, Headers, StorageState } from './types';
|
||||||
import type { Playwright } from './playwright';
|
import type { Playwright } from './playwright';
|
||||||
import { createInstrumentation } from './clientInstrumentation';
|
|
||||||
import { Tracing } from './tracing';
|
import { Tracing } from './tracing';
|
||||||
|
|
||||||
export type FetchOptions = {
|
export type FetchOptions = {
|
||||||
|
|
@ -57,8 +56,6 @@ export class APIRequest implements api.APIRequest {
|
||||||
|
|
||||||
// Instrumentation.
|
// Instrumentation.
|
||||||
_defaultContextOptions?: NewContextOptions & { tracesDir?: string };
|
_defaultContextOptions?: NewContextOptions & { tracesDir?: string };
|
||||||
_onDidCreateContext?: (context: APIRequestContext) => Promise<void>;
|
|
||||||
_onWillCloseContext?: (context: APIRequestContext) => Promise<void>;
|
|
||||||
|
|
||||||
constructor(playwright: Playwright) {
|
constructor(playwright: Playwright) {
|
||||||
this._playwright = playwright;
|
this._playwright = playwright;
|
||||||
|
|
@ -80,7 +77,7 @@ export class APIRequest implements api.APIRequest {
|
||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
context._request = this;
|
context._request = this;
|
||||||
context._tracing._tracesDir = tracesDir;
|
context._tracing._tracesDir = tracesDir;
|
||||||
await this._onDidCreateContext?.(context);
|
await context._instrumentation.onDidCreateRequestContext(context);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,12 +91,12 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.APIRequestContextInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.APIRequestContextInitializer) {
|
||||||
super(parent, type, guid, initializer, createInstrumentation());
|
super(parent, type, guid, initializer);
|
||||||
this._tracing = Tracing.from(initializer.tracing);
|
this._tracing = Tracing.from(initializer.tracing);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispose(): Promise<void> {
|
async dispose(): Promise<void> {
|
||||||
await this._request?._onWillCloseContext?.(this);
|
await this._instrumentation.onWillCloseRequestContext(this);
|
||||||
await this._channel.dispose();
|
await this._channel.dispose();
|
||||||
this._request?._contexts.delete(this);
|
this._request?._contexts.delete(this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -691,6 +691,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
async pause() {
|
async pause() {
|
||||||
if (require('inspector').url())
|
if (require('inspector').url())
|
||||||
return;
|
return;
|
||||||
|
this._browserContext.setDefaultNavigationTimeout(0);
|
||||||
|
this._browserContext.setDefaultTimeout(0);
|
||||||
|
this._instrumentation?.onWillPause();
|
||||||
await this._closedOrCrashedRace.safeRace(this.context()._channel.pause());
|
await this._closedOrCrashedRace.safeRace(this.context()._channel.pause());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { RawStack } from './stackTrace';
|
import type { RawStack } from './stackTrace';
|
||||||
|
import { captureRawStack } from './stackTrace';
|
||||||
|
|
||||||
export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone';
|
export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone';
|
||||||
|
|
||||||
|
|
@ -22,15 +23,13 @@ class ZoneManager {
|
||||||
lastZoneId = 0;
|
lastZoneId = 0;
|
||||||
readonly _zones = new Map<number, Zone<any>>();
|
readonly _zones = new Map<number, Zone<any>>();
|
||||||
|
|
||||||
run<T, R>(type: ZoneType, data: T, func: (data: T) => R | Promise<R>): R | Promise<R> {
|
run<T, R>(type: ZoneType, data: T, func: (data: T) => R): R {
|
||||||
return new Zone<T>(this, ++this.lastZoneId, type, data).run(func);
|
return new Zone<T>(this, ++this.lastZoneId, type, data).run(func);
|
||||||
}
|
}
|
||||||
|
|
||||||
zoneData<T>(type: ZoneType, rawStack: RawStack): T | null {
|
zoneData<T>(type: ZoneType, rawStack: RawStack): T | null {
|
||||||
for (const line of rawStack) {
|
for (const line of rawStack) {
|
||||||
const index = line.indexOf('__PWZONE__[');
|
for (const zoneId of zoneIds(line)) {
|
||||||
if (index !== -1) {
|
|
||||||
const zoneId = + line.substring(index + '__PWZONE__['.length, line.indexOf(']', index));
|
|
||||||
const zone = this._zones.get(zoneId);
|
const zone = this._zones.get(zoneId);
|
||||||
if (zone && zone.type === type)
|
if (zone && zone.type === type)
|
||||||
return zone.data;
|
return zone.data;
|
||||||
|
|
@ -38,6 +37,22 @@ class ZoneManager {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preserve<T>(callback: () => Promise<T>): Promise<T> {
|
||||||
|
const rawStack = captureRawStack();
|
||||||
|
const refs: number[] = [];
|
||||||
|
for (const line of rawStack)
|
||||||
|
refs.push(...zoneIds(line));
|
||||||
|
Object.defineProperty(callback, 'name', { value: `__PWZONE__[${refs.join(',')}]-refs` });
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoneIds(line: string): number[] {
|
||||||
|
const index = line.indexOf('__PWZONE__[');
|
||||||
|
if (index === -1)
|
||||||
|
return [];
|
||||||
|
return line.substring(index + '__PWZONE__['.length, line.indexOf(']', index)).split(',').map(s => +s);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Zone<T> {
|
class Zone<T> {
|
||||||
|
|
@ -55,16 +70,16 @@ class Zone<T> {
|
||||||
this.wallTime = Date.now();
|
this.wallTime = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
run<R>(func: (data: T) => R | Promise<R>): R | Promise<R> {
|
run<R>(func: (data: T) => R): R {
|
||||||
this._manager._zones.set(this.id, this);
|
this._manager._zones.set(this.id, this);
|
||||||
Object.defineProperty(func, 'name', { value: `__PWZONE__[${this.id}]` });
|
Object.defineProperty(func, 'name', { value: `__PWZONE__[${this.id}]-${this.type}` });
|
||||||
return runWithFinally(() => func(this.data), () => {
|
return runWithFinally(() => func(this.data), () => {
|
||||||
this._manager._zones.delete(this.id);
|
this._manager._zones.delete(this.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runWithFinally<R>(func: () => R | Promise<R>, finallyFunc: Function): R | Promise<R> {
|
export function runWithFinally<R>(func: () => R, finallyFunc: Function): R {
|
||||||
try {
|
try {
|
||||||
const result = func();
|
const result = func();
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
|
|
@ -74,7 +89,7 @@ export function runWithFinally<R>(func: () => R | Promise<R>, finallyFunc: Funct
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
finallyFunc();
|
finallyFunc();
|
||||||
throw e;
|
throw e;
|
||||||
});
|
}) as any;
|
||||||
}
|
}
|
||||||
finallyFunc();
|
finallyFunc();
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import type { TestInfoImpl } from './worker/testInfo';
|
||||||
import { rootTestType } from './common/testType';
|
import { rootTestType } from './common/testType';
|
||||||
import { type ContextReuseMode } from './common/config';
|
import { type ContextReuseMode } from './common/config';
|
||||||
import { artifactsFolderName } from './isomorphic/folders';
|
import { artifactsFolderName } from './isomorphic/folders';
|
||||||
|
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
|
||||||
|
import type { ParsedStackTrace } from '../../playwright-core/src/utils/stackTrace';
|
||||||
export { expect } from './matchers/expect';
|
export { expect } from './matchers/expect';
|
||||||
export { store as _store } from './store';
|
export { store as _store } from './store';
|
||||||
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
||||||
|
|
@ -258,29 +260,51 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
const reusedContexts = new Set<BrowserContext>();
|
const reusedContexts = new Set<BrowserContext>();
|
||||||
let traceOrdinal = 0;
|
let traceOrdinal = 0;
|
||||||
|
|
||||||
const createInstrumentationListener = (context?: BrowserContext) => {
|
const csiListener: ClientInstrumentationListener = {
|
||||||
return {
|
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any) => {
|
||||||
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any) => {
|
if (apiCall.startsWith('expect.'))
|
||||||
if (apiCall.startsWith('expect.'))
|
return { userObject: null };
|
||||||
return { userObject: null };
|
const step = testInfoImpl._addStep({
|
||||||
if (apiCall === 'page.pause') {
|
location: stackTrace?.frames[0] as any,
|
||||||
testInfo.setTimeout(0);
|
category: 'pw:api',
|
||||||
context?.setDefaultNavigationTimeout(0);
|
title: apiCall,
|
||||||
context?.setDefaultTimeout(0);
|
wallTime,
|
||||||
}
|
});
|
||||||
const step = testInfoImpl._addStep({
|
userData.userObject = step;
|
||||||
location: stackTrace?.frames[0] as any,
|
},
|
||||||
category: 'pw:api',
|
onApiCallEnd: (userData: any, error?: Error) => {
|
||||||
title: apiCall,
|
const step = userData.userObject;
|
||||||
wallTime,
|
step?.complete({ error });
|
||||||
});
|
},
|
||||||
userData.userObject = step;
|
onWillPause: () => {
|
||||||
},
|
testInfo.setTimeout(0);
|
||||||
onApiCallEnd: (userData: any, error?: Error) => {
|
},
|
||||||
const step = userData.userObject;
|
onDidCreateBrowserContext: async (context: BrowserContext) => {
|
||||||
step?.complete({ error });
|
context.setDefaultTimeout(actionTimeout || 0);
|
||||||
},
|
context.setDefaultNavigationTimeout(navigationTimeout || 0);
|
||||||
};
|
await startTraceChunkOnContextCreation(context.tracing);
|
||||||
|
attachConnectedHeaderIfNeeded(testInfo, context.browser());
|
||||||
|
},
|
||||||
|
onDidCreateRequestContext: async (context: APIRequestContext) => {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await startTraceChunkOnContextCreation(tracing);
|
||||||
|
},
|
||||||
|
onWillCloseBrowserContext: async (context: BrowserContext) => {
|
||||||
|
// When reusing context, we get all previous contexts closed at the start of next test.
|
||||||
|
// Do not record empty traces and useless screenshots for them.
|
||||||
|
if (reusedContexts.has(context))
|
||||||
|
return;
|
||||||
|
await stopTracing(context.tracing, (context as any)[kStartedContextTearDown]);
|
||||||
|
if (screenshotMode === 'on' || screenshotMode === 'only-on-failure') {
|
||||||
|
// Capture screenshot for now. We'll know whether we have to preserve them
|
||||||
|
// after the test finishes.
|
||||||
|
await Promise.all(context.pages().map(screenshotPage));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onWillCloseRequestContext: async (context: APIRequestContext) => {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await stopTracing(tracing, (context as any)[kStartedContextTearDown]);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const startTraceChunkOnContextCreation = async (tracing: Tracing) => {
|
const startTraceChunkOnContextCreation = async (tracing: Tracing) => {
|
||||||
|
|
@ -304,21 +328,6 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDidCreateBrowserContext = async (context: BrowserContext) => {
|
|
||||||
context.setDefaultTimeout(actionTimeout || 0);
|
|
||||||
context.setDefaultNavigationTimeout(navigationTimeout || 0);
|
|
||||||
await startTraceChunkOnContextCreation(context.tracing);
|
|
||||||
const listener = createInstrumentationListener(context);
|
|
||||||
(context as any)._instrumentation.addListener(listener);
|
|
||||||
(context.request as any)._instrumentation.addListener(listener);
|
|
||||||
attachConnectedHeaderIfNeeded(testInfo, context.browser());
|
|
||||||
};
|
|
||||||
const onDidCreateRequestContext = async (context: APIRequestContext) => {
|
|
||||||
const tracing = (context as any)._tracing as Tracing;
|
|
||||||
await startTraceChunkOnContextCreation(tracing);
|
|
||||||
(context as any)._instrumentation.addListener(createInstrumentationListener());
|
|
||||||
};
|
|
||||||
|
|
||||||
const preserveTrace = () => {
|
const preserveTrace = () => {
|
||||||
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
||||||
return captureTrace && (traceMode === 'on' || (testFailed && traceMode === 'retain-on-failure') || (traceMode === 'on-first-retry' && testInfo.retry === 1) || (traceMode === 'on-all-retries' && testInfo.retry > 0));
|
return captureTrace && (traceMode === 'on' || (testFailed && traceMode === 'retain-on-failure') || (traceMode === 'on-first-retry' && testInfo.retry === 1) || (traceMode === 'on-all-retries' && testInfo.retry > 0));
|
||||||
|
|
@ -361,46 +370,26 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
await Promise.all(contexts.map(ctx => Promise.all(ctx.pages().map(screenshotPage))));
|
await Promise.all(contexts.map(ctx => Promise.all(ctx.pages().map(screenshotPage))));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onWillCloseContext = async (context: BrowserContext) => {
|
|
||||||
// When reusing context, we get all previous contexts closed at the start of next test.
|
|
||||||
// Do not record empty traces and useless screenshots for them.
|
|
||||||
if (reusedContexts.has(context))
|
|
||||||
return;
|
|
||||||
await stopTracing(context.tracing, (context as any)[kStartedContextTearDown]);
|
|
||||||
if (screenshotMode === 'on' || screenshotMode === 'only-on-failure') {
|
|
||||||
// Capture screenshot for now. We'll know whether we have to preserve them
|
|
||||||
// after the test finishes.
|
|
||||||
await Promise.all(context.pages().map(screenshotPage));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onWillCloseRequestContext = async (context: APIRequestContext) => {
|
|
||||||
const tracing = (context as any)._tracing as Tracing;
|
|
||||||
await stopTracing(tracing, (context as any)[kStartedContextTearDown]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1. Setup instrumentation and process existing contexts.
|
// 1. Setup instrumentation and process existing contexts.
|
||||||
|
const instrumentation = (playwright as any)._instrumentation as ClientInstrumentation;
|
||||||
|
instrumentation.addListener(csiListener);
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
||||||
(browserType as any)._onDidCreateContext = onDidCreateBrowserContext;
|
|
||||||
(browserType as any)._onWillCloseContext = onWillCloseContext;
|
|
||||||
(browserType as any)._defaultContextOptions = _combinedContextOptions;
|
(browserType as any)._defaultContextOptions = _combinedContextOptions;
|
||||||
const promises: Promise<void>[] = [];
|
const promises: (Promise<void> | undefined)[] = [];
|
||||||
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
||||||
for (const context of existingContexts) {
|
for (const context of existingContexts) {
|
||||||
if ((context as any)[kIsReusedContext])
|
if ((context as any)[kIsReusedContext])
|
||||||
reusedContexts.add(context);
|
reusedContexts.add(context);
|
||||||
else
|
else
|
||||||
promises.push(onDidCreateBrowserContext(context));
|
promises.push(csiListener.onDidCreateBrowserContext?.(context as any));
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
(playwright.request as any)._onDidCreateContext = onDidCreateRequestContext;
|
|
||||||
(playwright.request as any)._onWillCloseContext = onWillCloseRequestContext;
|
|
||||||
(playwright.request as any)._defaultContextOptions = { ..._combinedContextOptions };
|
(playwright.request as any)._defaultContextOptions = { ..._combinedContextOptions };
|
||||||
(playwright.request as any)._defaultContextOptions.tracesDir = path.join(_artifactsDir(), 'traces');
|
(playwright.request as any)._defaultContextOptions.tracesDir = path.join(_artifactsDir(), 'traces');
|
||||||
const existingApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
const existingApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
await Promise.all(existingApiRequests.map(onDidCreateRequestContext));
|
await Promise.all(existingApiRequests.map(c => csiListener.onDidCreateRequestContext?.(c as any)));
|
||||||
}
|
}
|
||||||
if (screenshotMode === 'on' || screenshotMode === 'only-on-failure')
|
if (screenshotMode === 'on' || screenshotMode === 'only-on-failure')
|
||||||
testInfoImpl._onTestFailureImmediateCallbacks.set(screenshotOnTestFailure, 'Screenshot on failure');
|
testInfoImpl._onTestFailureImmediateCallbacks.set(screenshotOnTestFailure, 'Screenshot on failure');
|
||||||
|
|
@ -421,19 +410,14 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Cleanup instrumentation.
|
// 4. Cleanup instrumentation.
|
||||||
|
instrumentation.removeListener(csiListener);
|
||||||
|
|
||||||
const leftoverContexts: BrowserContext[] = [];
|
const leftoverContexts: BrowserContext[] = [];
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
||||||
leftoverContexts.push(...(browserType as any)._contexts);
|
leftoverContexts.push(...(browserType as any)._contexts);
|
||||||
(browserType as any)._onDidCreateContext = undefined;
|
|
||||||
(browserType as any)._onWillCloseContext = undefined;
|
|
||||||
(browserType as any)._defaultContextOptions = undefined;
|
(browserType as any)._defaultContextOptions = undefined;
|
||||||
}
|
}
|
||||||
leftoverContexts.forEach(context => (context as any)._instrumentation.removeAllListeners());
|
|
||||||
for (const context of (playwright.request as any)._contexts)
|
|
||||||
context._instrumentation.removeAllListeners();
|
|
||||||
const leftoverApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
const leftoverApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
(playwright.request as any)._onDidCreateContext = undefined;
|
|
||||||
(playwright.request as any)._onWillCloseContext = undefined;
|
|
||||||
(playwright.request as any)._defaultContextOptions = undefined;
|
(playwright.request as any)._defaultContextOptions = undefined;
|
||||||
testInfoImpl._onTestFailureImmediateCallbacks.delete(screenshotOnTestFailure);
|
testInfoImpl._onTestFailureImmediateCallbacks.delete(screenshotOnTestFailure);
|
||||||
|
|
||||||
|
|
@ -607,12 +591,6 @@ type StackFrame = {
|
||||||
function?: string,
|
function?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
type ParsedStackTrace = {
|
|
||||||
frames: StackFrame[];
|
|
||||||
frameTexts: string[];
|
|
||||||
apiName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode } | undefined): VideoMode {
|
function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode } | undefined): VideoMode {
|
||||||
if (!video)
|
if (!video)
|
||||||
return 'off';
|
return 'off';
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { formatLocation, debugTest } from '../util';
|
import { formatLocation, debugTest } from '../util';
|
||||||
import { ManualPromise } from 'playwright-core/lib/utils';
|
import { ManualPromise, zones } from 'playwright-core/lib/utils';
|
||||||
import type { TestInfoImpl } from './testInfo';
|
import type { TestInfoImpl } from './testInfo';
|
||||||
import type { FixtureDescription, TimeoutManager } from './timeoutManager';
|
import type { FixtureDescription, TimeoutManager } from './timeoutManager';
|
||||||
import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures';
|
import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures';
|
||||||
|
|
@ -98,7 +98,10 @@ class Fixture {
|
||||||
throw e;
|
throw e;
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const result = this.registration.fn(params, useFunc, info);
|
const result = zones.preserve(async () => {
|
||||||
|
return await this.registration.fn(params, useFunc, info);
|
||||||
|
});
|
||||||
|
|
||||||
if (result instanceof Promise)
|
if (result instanceof Promise)
|
||||||
this._selfTeardownComplete = result.catch(handleError);
|
this._selfTeardownComplete = result.catch(handleError);
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,17 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
|
||||||
await page.setViewportSize({ width: 500, height: 600 });
|
await page.setViewportSize({ width: 500, height: 600 });
|
||||||
|
|
||||||
// Go through instrumentation to exercise reentrant stack traces.
|
// Go through instrumentation to exercise reentrant stack traces.
|
||||||
(browserType as any)._onWillCloseContext = async () => {
|
const csi = {
|
||||||
await page.hover('body');
|
onWillCloseBrowserContext: async () => {
|
||||||
await page.close();
|
await page.hover('body');
|
||||||
traceFile = path.join(workerInfo.project.outputDir, String(workerInfo.workerIndex), browserName, 'trace.zip');
|
await page.close();
|
||||||
await context.tracing.stop({ path: traceFile });
|
traceFile = path.join(workerInfo.project.outputDir, String(workerInfo.workerIndex), browserName, 'trace.zip');
|
||||||
|
await context.tracing.stop({ path: traceFile });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
(browserType as any)._instrumentation.addListener(csi);
|
||||||
await context.close();
|
await context.close();
|
||||||
(browserType as any)._onWillCloseContext = undefined;
|
(browserType as any)._instrumentation.removeListener(csi);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => {
|
test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => {
|
||||||
|
|
|
||||||
|
|
@ -276,9 +276,13 @@ test('should report expect steps', async ({ runInlineTest }) => {
|
||||||
`begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
`begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
||||||
`end {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
`end {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
||||||
`begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
`begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
||||||
|
`begin {\"title\":\"browserType.launch\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"browserType.launch\",\"category\":\"pw:api\"}`,
|
||||||
|
`begin {\"title\":\"browser.newContext\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"browser.newContext\",\"category\":\"pw:api\"}`,
|
||||||
`begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
`begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
||||||
`end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
`end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
||||||
`end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`,
|
`end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserType.launch\",\"category\":\"pw:api\"},{\"title\":\"browser.newContext\",\"category\":\"pw:api\"},{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`,
|
||||||
`begin {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`,
|
`begin {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`,
|
||||||
`end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`,
|
`end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`,
|
||||||
`begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
`begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
||||||
|
|
@ -333,9 +337,15 @@ test('should report api steps', async ({ runInlineTest }) => {
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.outputLines).toEqual([
|
expect(result.outputLines).toEqual([
|
||||||
`begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
`begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
||||||
|
`begin {\"title\":\"browserType.launch\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"browserType.launch\",\"category\":\"pw:api\"}`,
|
||||||
|
`begin {\"title\":\"browser.newContext\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"browser.newContext\",\"category\":\"pw:api\"}`,
|
||||||
`begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
`begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
||||||
`end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
`end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
||||||
`end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`,
|
`begin {\"title\":\"apiRequest.newContext\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"apiRequest.newContext\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserType.launch\",\"category\":\"pw:api\"},{\"title\":\"browser.newContext\",\"category\":\"pw:api\"},{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"},{\"title\":\"apiRequest.newContext\",\"category\":\"pw:api\"}]}`,
|
||||||
`begin {\"title\":\"page.waitForNavigation\",\"category\":\"pw:api\"}`,
|
`begin {\"title\":\"page.waitForNavigation\",\"category\":\"pw:api\"}`,
|
||||||
`begin {\"title\":\"page.goto(data:text/html,<button></button>)\",\"category\":\"pw:api\"}`,
|
`begin {\"title\":\"page.goto(data:text/html,<button></button>)\",\"category\":\"pw:api\"}`,
|
||||||
`end {\"title\":\"page.waitForNavigation\",\"category\":\"pw:api\"}`,
|
`end {\"title\":\"page.waitForNavigation\",\"category\":\"pw:api\"}`,
|
||||||
|
|
@ -400,9 +410,13 @@ test('should report api step failure', async ({ runInlineTest }) => {
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.outputLines).toEqual([
|
expect(result.outputLines).toEqual([
|
||||||
`begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
`begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
||||||
|
`begin {\"title\":\"browserType.launch\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"browserType.launch\",\"category\":\"pw:api\"}`,
|
||||||
|
`begin {\"title\":\"browser.newContext\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"browser.newContext\",\"category\":\"pw:api\"}`,
|
||||||
`begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
`begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
||||||
`end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
`end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
||||||
`end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`,
|
`end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserType.launch\",\"category\":\"pw:api\"},{\"title\":\"browser.newContext\",\"category\":\"pw:api\"},{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`,
|
||||||
`begin {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`,
|
`begin {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`,
|
||||||
`end {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`,
|
`end {\"title\":\"page.setContent\",\"category\":\"pw:api\"}`,
|
||||||
`begin {\"title\":\"page.click(input)\",\"category\":\"pw:api\"}`,
|
`begin {\"title\":\"page.click(input)\",\"category\":\"pw:api\"}`,
|
||||||
|
|
@ -460,9 +474,13 @@ test('should show nice stacks for locators', async ({ runInlineTest }) => {
|
||||||
expect(result.output).not.toContain('Internal error');
|
expect(result.output).not.toContain('Internal error');
|
||||||
expect(result.outputLines).toEqual([
|
expect(result.outputLines).toEqual([
|
||||||
`begin {"title":"Before Hooks","category":"hook"}`,
|
`begin {"title":"Before Hooks","category":"hook"}`,
|
||||||
|
`begin {\"title\":\"browserType.launch\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"browserType.launch\",\"category\":\"pw:api\"}`,
|
||||||
|
`begin {\"title\":\"browser.newContext\",\"category\":\"pw:api\"}`,
|
||||||
|
`end {\"title\":\"browser.newContext\",\"category\":\"pw:api\"}`,
|
||||||
`begin {"title":"browserContext.newPage","category":"pw:api"}`,
|
`begin {"title":"browserContext.newPage","category":"pw:api"}`,
|
||||||
`end {"title":"browserContext.newPage","category":"pw:api"}`,
|
`end {"title":"browserContext.newPage","category":"pw:api"}`,
|
||||||
`end {"title":"Before Hooks","category":"hook","steps":[{"title":"browserContext.newPage","category":"pw:api"}]}`,
|
`end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserType.launch\",\"category\":\"pw:api\"},{\"title\":\"browser.newContext\",\"category\":\"pw:api\"},{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`,
|
||||||
`begin {"title":"page.setContent","category":"pw:api"}`,
|
`begin {"title":"page.setContent","category":"pw:api"}`,
|
||||||
`end {"title":"page.setContent","category":"pw:api"}`,
|
`end {"title":"page.setContent","category":"pw:api"}`,
|
||||||
`begin {"title":"locator.evaluate(button)","category":"pw:api"}`,
|
`begin {"title":"locator.evaluate(button)","category":"pw:api"}`,
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,14 @@ test('should report api step hierarchy', async ({ runInlineTest }) => {
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: 'Before Hooks',
|
title: 'Before Hooks',
|
||||||
steps: [
|
steps: [
|
||||||
|
{
|
||||||
|
category: 'pw:api',
|
||||||
|
title: 'browserType.launch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'pw:api',
|
||||||
|
title: 'browser.newContext',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
category: 'pw:api',
|
category: 'pw:api',
|
||||||
title: 'browserContext.newPage',
|
title: 'browserContext.newPage',
|
||||||
|
|
@ -242,6 +250,14 @@ test('should not report nested after hooks', async ({ runInlineTest }) => {
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: 'Before Hooks',
|
title: 'Before Hooks',
|
||||||
steps: [
|
steps: [
|
||||||
|
{
|
||||||
|
category: 'pw:api',
|
||||||
|
title: 'browserType.launch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'pw:api',
|
||||||
|
title: 'browser.newContext',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
category: 'pw:api',
|
category: 'pw:api',
|
||||||
title: 'browserContext.newPage',
|
title: 'browserContext.newPage',
|
||||||
|
|
@ -349,6 +365,14 @@ test('should report expect step locations', async ({ runInlineTest }) => {
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: 'Before Hooks',
|
title: 'Before Hooks',
|
||||||
steps: [
|
steps: [
|
||||||
|
{
|
||||||
|
category: 'pw:api',
|
||||||
|
title: 'browserType.launch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'pw:api',
|
||||||
|
title: 'browser.newContext',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
category: 'pw:api',
|
category: 'pw:api',
|
||||||
title: 'browserContext.newPage',
|
title: 'browserContext.newPage',
|
||||||
|
|
@ -589,6 +613,14 @@ test('should nest steps based on zones', async ({ runInlineTest }) => {
|
||||||
],
|
],
|
||||||
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
location: { file: 'a.test.ts', line: 'number', column: 'number' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'browserType.launch',
|
||||||
|
category: 'pw:api'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'pw:api',
|
||||||
|
title: 'browser.newContext',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'browserContext.newPage',
|
title: 'browserContext.newPage',
|
||||||
category: 'pw:api'
|
category: 'pw:api'
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,8 @@ test('should report toHaveScreenshot step with expectation name in title', async
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.outputLines).toEqual([
|
expect(result.outputLines).toEqual([
|
||||||
|
`end browserType.launch`,
|
||||||
|
`end browser.newContext`,
|
||||||
`end browserContext.newPage`,
|
`end browserContext.newPage`,
|
||||||
`end Before Hooks`,
|
`end Before Hooks`,
|
||||||
`end expect.toHaveScreenshot(foo.png)`,
|
`end expect.toHaveScreenshot(foo.png)`,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue