chore: make client-side instrumentation non-nullable (#22694)

This commit is contained in:
Pavel Feldman 2023-04-28 08:57:43 -07:00 committed by GitHub
parent b555d33e38
commit e9373dfb6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 185 additions and 120 deletions

View File

@ -86,11 +86,13 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
}
async newPage(options: BrowserContextOptions = {}): Promise<Page> {
const context = await this.newContext(options);
const page = await context.newPage();
page._ownedContext = context;
context._ownerPage = page;
return page;
return await this._wrapApiCall(async () => {
const context = await this.newContext(options);
const page = await context.newPage();
page._ownedContext = context;
context._ownerPage = page;
return page;
});
}
isConnected(): boolean {

View File

@ -37,7 +37,6 @@ import { Tracing } from './tracing';
import type { BrowserType } from './browserType';
import { Artifact } from './artifact';
import { APIRequestContext } from './fetch';
import { createInstrumentation } from './clientInstrumentation';
import { rewriteErrorMessage } from '../utils/stackTrace';
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) {
super(parent, type, guid, initializer, createInstrumentation());
super(parent, type, guid, initializer);
if (parent instanceof Browser)
this._browser = parent;
this._browser?._contexts.add(this);

View File

@ -51,8 +51,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
_defaultContextOptions?: BrowserContextOptions;
private _defaultLaunchOptions?: LaunchOptions;
private _defaultConnectOptions?: ConnectOptions;
private _onDidCreateContext?: (context: BrowserContext) => Promise<void>;
private _onWillCloseContext?: (context: BrowserContext) => Promise<void>;
static from(browserType: channels.BrowserTypeChannel): BrowserType {
return (browserType as any)._object;
@ -254,11 +252,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
context._browserType = this;
this._contexts.add(context);
context._setOptions(contextOptions, browserOptions);
await this._onDidCreateContext?.(context);
await this._instrumentation.onDidCreateBrowserContext(context);
}
async _willCloseContext(context: BrowserContext) {
this._contexts.delete(context);
await this._onWillCloseContext?.(context);
await this._instrumentation.onWillCloseBrowserContext(context);
}
}

View File

@ -39,17 +39,17 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
readonly _channel: T;
readonly _initializer: channels.InitializerTraits<T>;
_logger: Logger | undefined;
_instrumentation: ClientInstrumentation | undefined;
readonly _instrumentation: ClientInstrumentation;
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();
this.setMaxListeners(0);
this._connection = parent instanceof ChannelOwner ? parent._connection : parent;
this._type = type;
this._guid = guid;
this._parent = parent instanceof ChannelOwner ? parent : undefined;
this._instrumentation = instrumentation || this._parent?._instrumentation;
this._instrumentation = this._connection._instrumentation;
this._connection._objects.set(guid, this);
if (this._parent) {
@ -179,7 +179,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
try {
logApiCall(logger, `=> ${apiName} started`, isInternal);
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);
});
csi?.onApiCallEnd(callCookie);

View File

@ -15,17 +15,30 @@
*/
import type { ParsedStackTrace } from '../utils/stackTrace';
import type { BrowserContext } from './browserContext';
import type { APIRequestContext } from './fetch';
export interface ClientInstrumentation {
addListener(listener: ClientInstrumentationListener): void;
removeListener(listener: ClientInstrumentationListener): void;
removeAllListeners(): 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 {
onApiCallBegin?(apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any): any;
onApiCallEnd?(userData: any, error?: Error): any;
onApiCallBegin?(apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any): void;
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 {

View File

@ -44,6 +44,7 @@ import { APIRequestContext } from './fetch';
import { LocalUtils } from './localUtils';
import { Tracing } from './tracing';
import { findValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
import { createInstrumentation } from './clientInstrumentation';
class Root extends ChannelOwner<channels.RootChannel> {
constructor(connection: Connection) {
@ -72,6 +73,7 @@ export class Connection extends EventEmitter {
// Some connections allow resolving in-process dispatchers.
toImpl: ((client: ChannelOwner) => any) | undefined;
private _tracingCount = 0;
readonly _instrumentation = createInstrumentation();
constructor(localUtils?: LocalUtils) {
super();

View File

@ -28,7 +28,6 @@ import { ChannelOwner } from './channelOwner';
import { RawHeaders } from './network';
import type { FilePayload, Headers, StorageState } from './types';
import type { Playwright } from './playwright';
import { createInstrumentation } from './clientInstrumentation';
import { Tracing } from './tracing';
export type FetchOptions = {
@ -57,8 +56,6 @@ export class APIRequest implements api.APIRequest {
// Instrumentation.
_defaultContextOptions?: NewContextOptions & { tracesDir?: string };
_onDidCreateContext?: (context: APIRequestContext) => Promise<void>;
_onWillCloseContext?: (context: APIRequestContext) => Promise<void>;
constructor(playwright: Playwright) {
this._playwright = playwright;
@ -80,7 +77,7 @@ export class APIRequest implements api.APIRequest {
this._contexts.add(context);
context._request = this;
context._tracing._tracesDir = tracesDir;
await this._onDidCreateContext?.(context);
await context._instrumentation.onDidCreateRequestContext(context);
return context;
}
}
@ -94,12 +91,12 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
}
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);
}
async dispose(): Promise<void> {
await this._request?._onWillCloseContext?.(this);
await this._instrumentation.onWillCloseRequestContext(this);
await this._channel.dispose();
this._request?._contexts.delete(this);
}

View File

@ -691,6 +691,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
async pause() {
if (require('inspector').url())
return;
this._browserContext.setDefaultNavigationTimeout(0);
this._browserContext.setDefaultTimeout(0);
this._instrumentation?.onWillPause();
await this._closedOrCrashedRace.safeRace(this.context()._channel.pause());
}

View File

@ -15,6 +15,7 @@
*/
import type { RawStack } from './stackTrace';
import { captureRawStack } from './stackTrace';
export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone';
@ -22,15 +23,13 @@ class ZoneManager {
lastZoneId = 0;
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);
}
zoneData<T>(type: ZoneType, rawStack: RawStack): T | null {
for (const line of rawStack) {
const index = line.indexOf('__PWZONE__[');
if (index !== -1) {
const zoneId = + line.substring(index + '__PWZONE__['.length, line.indexOf(']', index));
for (const zoneId of zoneIds(line)) {
const zone = this._zones.get(zoneId);
if (zone && zone.type === type)
return zone.data;
@ -38,6 +37,22 @@ class ZoneManager {
}
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> {
@ -55,16 +70,16 @@ class Zone<T> {
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);
Object.defineProperty(func, 'name', { value: `__PWZONE__[${this.id}]` });
Object.defineProperty(func, 'name', { value: `__PWZONE__[${this.id}]-${this.type}` });
return runWithFinally(() => func(this.data), () => {
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 {
const result = func();
if (result instanceof Promise) {
@ -74,7 +89,7 @@ export function runWithFinally<R>(func: () => R | Promise<R>, finallyFunc: Funct
}).catch(e => {
finallyFunc();
throw e;
});
}) as any;
}
finallyFunc();
return result;

View File

@ -24,6 +24,8 @@ import type { TestInfoImpl } from './worker/testInfo';
import { rootTestType } from './common/testType';
import { type ContextReuseMode } from './common/config';
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 { store as _store } from './store';
export const _baseTest: TestType<{}, {}> = rootTestType.test;
@ -258,29 +260,51 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
const reusedContexts = new Set<BrowserContext>();
let traceOrdinal = 0;
const createInstrumentationListener = (context?: BrowserContext) => {
return {
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any) => {
if (apiCall.startsWith('expect.'))
return { userObject: null };
if (apiCall === 'page.pause') {
testInfo.setTimeout(0);
context?.setDefaultNavigationTimeout(0);
context?.setDefaultTimeout(0);
}
const step = testInfoImpl._addStep({
location: stackTrace?.frames[0] as any,
category: 'pw:api',
title: apiCall,
wallTime,
});
userData.userObject = step;
},
onApiCallEnd: (userData: any, error?: Error) => {
const step = userData.userObject;
step?.complete({ error });
},
};
const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any) => {
if (apiCall.startsWith('expect.'))
return { userObject: null };
const step = testInfoImpl._addStep({
location: stackTrace?.frames[0] as any,
category: 'pw:api',
title: apiCall,
wallTime,
});
userData.userObject = step;
},
onApiCallEnd: (userData: any, error?: Error) => {
const step = userData.userObject;
step?.complete({ error });
},
onWillPause: () => {
testInfo.setTimeout(0);
},
onDidCreateBrowserContext: async (context: BrowserContext) => {
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) => {
@ -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 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));
@ -361,46 +370,26 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
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.
const instrumentation = (playwright as any)._instrumentation as ClientInstrumentation;
instrumentation.addListener(csiListener);
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;
const promises: Promise<void>[] = [];
const promises: (Promise<void> | undefined)[] = [];
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
for (const context of existingContexts) {
if ((context as any)[kIsReusedContext])
reusedContexts.add(context);
else
promises.push(onDidCreateBrowserContext(context));
promises.push(csiListener.onDidCreateBrowserContext?.(context as any));
}
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.tracesDir = path.join(_artifactsDir(), 'traces');
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')
testInfoImpl._onTestFailureImmediateCallbacks.set(screenshotOnTestFailure, 'Screenshot on failure');
@ -421,19 +410,14 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
};
// 4. Cleanup instrumentation.
instrumentation.removeListener(csiListener);
const leftoverContexts: BrowserContext[] = [];
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
leftoverContexts.push(...(browserType as any)._contexts);
(browserType as any)._onDidCreateContext = undefined;
(browserType as any)._onWillCloseContext = 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>);
(playwright.request as any)._onDidCreateContext = undefined;
(playwright.request as any)._onWillCloseContext = undefined;
(playwright.request as any)._defaultContextOptions = undefined;
testInfoImpl._onTestFailureImmediateCallbacks.delete(screenshotOnTestFailure);
@ -607,12 +591,6 @@ type StackFrame = {
function?: string,
};
type ParsedStackTrace = {
frames: StackFrame[];
frameTexts: string[];
apiName: string;
};
function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode } | undefined): VideoMode {
if (!video)
return 'off';

View File

@ -15,7 +15,7 @@
*/
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 { FixtureDescription, TimeoutManager } from './timeoutManager';
import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures';
@ -98,7 +98,10 @@ class Fixture {
throw e;
};
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)
this._selfTeardownComplete = result.catch(handleError);
else

View File

@ -63,14 +63,17 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
await page.setViewportSize({ width: 500, height: 600 });
// Go through instrumentation to exercise reentrant stack traces.
(browserType as any)._onWillCloseContext = async () => {
await page.hover('body');
await page.close();
traceFile = path.join(workerInfo.project.outputDir, String(workerInfo.workerIndex), browserName, 'trace.zip');
await context.tracing.stop({ path: traceFile });
const csi = {
onWillCloseBrowserContext: async () => {
await page.hover('body');
await page.close();
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();
(browserType as any)._onWillCloseContext = undefined;
(browserType as any)._instrumentation.removeListener(csi);
});
test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => {

View File

@ -276,9 +276,13 @@ test('should report expect steps', async ({ runInlineTest }) => {
`begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
`end {\"title\":\"After 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\"}`,
`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\"}`,
`end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`,
`begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
@ -333,9 +337,15 @@ test('should report api steps', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0);
expect(result.outputLines).toEqual([
`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\"}`,
`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.goto(data:text/html,<button></button>)\",\"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.outputLines).toEqual([
`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\"}`,
`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\"}`,
`end {\"title\":\"page.setContent\",\"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.outputLines).toEqual([
`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"}`,
`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"}`,
`end {"title":"page.setContent","category":"pw:api"}`,
`begin {"title":"locator.evaluate(button)","category":"pw:api"}`,

View File

@ -95,6 +95,14 @@ test('should report api step hierarchy', async ({ runInlineTest }) => {
category: 'hook',
title: 'Before Hooks',
steps: [
{
category: 'pw:api',
title: 'browserType.launch',
},
{
category: 'pw:api',
title: 'browser.newContext',
},
{
category: 'pw:api',
title: 'browserContext.newPage',
@ -242,6 +250,14 @@ test('should not report nested after hooks', async ({ runInlineTest }) => {
category: 'hook',
title: 'Before Hooks',
steps: [
{
category: 'pw:api',
title: 'browserType.launch',
},
{
category: 'pw:api',
title: 'browser.newContext',
},
{
category: 'pw:api',
title: 'browserContext.newPage',
@ -349,6 +365,14 @@ test('should report expect step locations', async ({ runInlineTest }) => {
category: 'hook',
title: 'Before Hooks',
steps: [
{
category: 'pw:api',
title: 'browserType.launch',
},
{
category: 'pw:api',
title: 'browser.newContext',
},
{
category: 'pw:api',
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' }
},
{
title: 'browserType.launch',
category: 'pw:api'
},
{
category: 'pw:api',
title: 'browser.newContext',
},
{
title: 'browserContext.newPage',
category: 'pw:api'

View File

@ -206,6 +206,8 @@ test('should report toHaveScreenshot step with expectation name in title', async
expect(result.exitCode).toBe(0);
expect(result.outputLines).toEqual([
`end browserType.launch`,
`end browser.newContext`,
`end browserContext.newPage`,
`end Before Hooks`,
`end expect.toHaveScreenshot(foo.png)`,