chrome: don't nest pw:api steps (#8932)

This commit is contained in:
Pavel Feldman 2021-09-15 11:34:23 -07:00 committed by GitHub
parent 2ec82b9a5c
commit 14bc663742
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 85 additions and 57 deletions

View file

@ -72,15 +72,22 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
}; };
} }
private _createChannel(base: Object, stackTrace: ParsedStackTrace | null): T { private _createChannel(base: Object, stackTrace: ParsedStackTrace | null, csi?: ClientSideInstrumentation, callCookie?: { userObject: any }): T {
const channel = new Proxy(base, { const channel = new Proxy(base, {
get: (obj: any, prop) => { get: (obj: any, prop) => {
if (prop === 'debugScopeState') if (prop === 'debugScopeState')
return (params: any) => this._connection.sendMessageToServer(this, prop, params, stackTrace); return (params: any) => this._connection.sendMessageToServer(this, prop, params, stackTrace);
if (typeof prop === 'string') { if (typeof prop === 'string') {
const validator = scheme[paramsName(this._type, prop)]; const validator = scheme[paramsName(this._type, prop)];
if (validator) if (validator) {
return (params: any) => this._connection.sendMessageToServer(this, prop, validator(params, ''), stackTrace); return (params: any) => {
if (callCookie && csi) {
callCookie.userObject = csi.onApiCallBegin(renderCallWithParams(stackTrace!.apiName, params)).userObject;
csi = undefined;
}
return this._connection.sendMessageToServer(this, prop, validator(params, ''), stackTrace);
};
}
} }
return obj[prop]; return obj[prop];
}, },
@ -97,22 +104,25 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
let ancestorWithCSI: ChannelOwner<any> = this; let ancestorWithCSI: ChannelOwner<any> = this;
while (!ancestorWithCSI._csi && ancestorWithCSI._parent) while (!ancestorWithCSI._csi && ancestorWithCSI._parent)
ancestorWithCSI = ancestorWithCSI._parent; ancestorWithCSI = ancestorWithCSI._parent;
let csiCallback: ((e?: Error) => void) | undefined;
// Do not report nested async calls to _wrapApiCall.
const isNested = stackTrace.allFrames.filter(f => f.function?.includes('_wrapApiCall')).length > 1;
const csi = isNested ? undefined : ancestorWithCSI._csi;
const callCookie: { userObject: any } = { userObject: null };
try { try {
logApiCall(logger, `=> ${apiName} started`); logApiCall(logger, `=> ${apiName} started`, isNested);
csiCallback = ancestorWithCSI._csi?.onApiCall(stackTrace); const channel = this._createChannel({}, stackTrace, csi, callCookie);
const channel = this._createChannel({}, stackTrace);
const result = await func(channel as any, stackTrace); const result = await func(channel as any, stackTrace);
csiCallback?.(); csi?.onApiCallEnd(callCookie);
logApiCall(logger, `<= ${apiName} succeeded`); logApiCall(logger, `<= ${apiName} succeeded`, isNested);
return result; return result;
} catch (e) { } catch (e) {
const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : ''; const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
e.message = apiName + ': ' + e.message; e.message = apiName + ': ' + e.message;
e.stack = e.message + '\n' + frameTexts.join('\n') + innerError; e.stack = e.message + '\n' + frameTexts.join('\n') + innerError;
csiCallback?.(e); csi?.onApiCallEnd(callCookie, e);
logApiCall(logger, `<= ${apiName} failed`); logApiCall(logger, `<= ${apiName} failed`, isNested);
throw e; throw e;
} }
} }
@ -129,7 +139,9 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
} }
} }
function logApiCall(logger: Logger | undefined, message: string) { function logApiCall(logger: Logger | undefined, message: string, isNested: boolean) {
if (isNested)
return;
if (logger && logger.isEnabled('api', 'info')) if (logger && logger.isEnabled('api', 'info'))
logger.log('api', 'info', message, [], { color: 'cyan' }); logger.log('api', 'info', message, [], { color: 'cyan' });
debugLogger.log('api', message); debugLogger.log('api', message);
@ -139,6 +151,19 @@ function paramsName(type: string, method: string) {
return type + method[0].toUpperCase() + method.substring(1) + 'Params'; return type + method[0].toUpperCase() + method.substring(1) + 'Params';
} }
const paramsToRender = ['url', 'selector', 'text', 'key'];
export function renderCallWithParams(apiName: string, params: any) {
const paramsArray = [];
if (params) {
for (const name of paramsToRender) {
if (params[name])
paramsArray.push(params[name]);
}
}
const paramsText = paramsArray.length ? '(' + paramsArray.join(', ') + ')' : '';
return apiName + paramsText;
}
const tChannel = (name: string): Validator => { const tChannel = (name: string): Validator => {
return (arg: any, path: string) => { return (arg: any, path: string) => {
if (arg._object instanceof ChannelOwner && (name === '*' || arg._object._type === name)) if (arg._object instanceof ChannelOwner && (name === '*' || arg._object._type === name))

View file

@ -83,7 +83,7 @@ export class Connection extends EventEmitter {
async sendMessageToServer(object: ChannelOwner, method: string, params: any, maybeStackTrace: ParsedStackTrace | null): Promise<any> { async sendMessageToServer(object: ChannelOwner, method: string, params: any, maybeStackTrace: ParsedStackTrace | null): Promise<any> {
const guid = object._guid; const guid = object._guid;
const stackTrace = maybeStackTrace || { frameTexts: [], frames: [], apiName: '' }; const stackTrace: ParsedStackTrace = maybeStackTrace || { frameTexts: [], frames: [], apiName: '', allFrames: [] };
const { frames, apiName } = stackTrace; const { frames, apiName } = stackTrace;
const id = ++this._lastId; const id = ++this._lastId;

View file

@ -383,9 +383,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
} }
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return this._wrapApiCall(async (channel: channels.PageChannel) => { return this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`);
return this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`);
});
} }
private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> { private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> {

View file

@ -17,7 +17,6 @@
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import type { Size } from '../common/types'; import type { Size } from '../common/types';
import type { ParsedStackTrace } from '../utils/stackTrace';
export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray } from '../common/types'; export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray } from '../common/types';
type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error'; type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
@ -27,7 +26,8 @@ export interface Logger {
} }
export interface ClientSideInstrumentation { export interface ClientSideInstrumentation {
onApiCall(stackTrace: ParsedStackTrace): ((error?: Error) => void) | undefined; onApiCallBegin(apiCall: string): { userObject: any };
onApiCallEnd(userData: { userObject: any }, error?: Error): any;
} }
export type StrictOptions = { strict?: boolean }; export type StrictOptions = { strict?: boolean };

View file

@ -313,7 +313,8 @@ export class Dispatcher {
}; };
steps.set(params.stepId, step); steps.set(params.stepId, step);
(parentStep || result).steps.push(step); (parentStep || result).steps.push(step);
stepStack.add(step); if (params.canHaveChildren)
stepStack.add(step);
this._reporter.onStepBegin?.(test, result, step); this._reporter.onStepBegin?.(test, result, step);
}); });
worker.on('stepEnd', (params: StepEndPayload) => { worker.on('stepEnd', (params: StepEndPayload) => {

View file

@ -75,7 +75,7 @@ function wrap(matcherName: string, matcher: any) {
const INTERNAL_STACK_LENGTH = 3; const INTERNAL_STACK_LENGTH = 3;
const stackLines = new Error().stack!.split('\n').slice(INTERNAL_STACK_LENGTH + 1); const stackLines = new Error().stack!.split('\n').slice(INTERNAL_STACK_LENGTH + 1);
const step = testInfo._addStep('expect', `expect${this.isNot ? '.not' : ''}.${matcherName}`); const step = testInfo._addStep('expect', `expect${this.isNot ? '.not' : ''}.${matcherName}`, true);
const reportStepEnd = (result: any) => { const reportStepEnd = (result: any) => {
const success = result.pass !== this.isNot; const success = result.pass !== this.isNot;

View file

@ -20,7 +20,6 @@ import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, Browse
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../../types/test'; import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../../types/test';
import { rootTestType } from './testType'; import { rootTestType } from './testType';
import { createGuid, removeFolders } from '../utils/utils'; import { createGuid, removeFolders } from '../utils/utils';
import { TestInfoImpl } from './types';
export { expect } from './expect'; export { expect } from './expect';
export const _baseTest: TestType<{}, {}> = rootTestType.test; export const _baseTest: TestType<{}, {}> = rootTestType.test;
@ -203,13 +202,14 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
await context.tracing.stop(); await context.tracing.stop();
} }
(context as any)._csi = { (context as any)._csi = {
onApiCall: (stackTrace: ParsedStackTrace) => { onApiCallBegin: (apiCall: string) => {
const testInfoImpl = testInfo as TestInfoImpl; const testInfoImpl = testInfo as any;
const existingStep = testInfoImpl._currentSteps().find(step => step.category === 'pw:api' || step.category === 'expect'); const step = testInfoImpl._addStep('pw:api', apiCall, false);
const newStep = existingStep ? undefined : testInfoImpl._addStep('pw:api', stackTrace.apiName); return { userObject: step };
return (error?: Error) => { },
newStep?.complete(error); onApiCallEnd: (data: { userObject: any }, error?: Error) => {
}; const step = data.userObject;
step?.complete(error);
}, },
}; };
}; };

View file

@ -51,6 +51,7 @@ export type StepBeginPayload = {
stepId: string; stepId: string;
title: string; title: string;
category: string; category: string;
canHaveChildren: boolean;
wallTime: number; // milliseconds since unix epoch wallTime: number; // milliseconds since unix epoch
}; };

View file

@ -186,7 +186,7 @@ export class TestTypeImpl {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)
throw errorWithLocation(location, `test.step() can only be called from a test`); throw errorWithLocation(location, `test.step() can only be called from a test`);
const step = testInfo._addStep('test.step', title); const step = testInfo._addStep('test.step', title, true);
try { try {
await body(); await body();
step.complete(); step.complete();

View file

@ -28,10 +28,10 @@ export type Annotations = { type: string, description?: string }[];
export interface TestStepInternal { export interface TestStepInternal {
complete(error?: Error | TestError): void; complete(error?: Error | TestError): void;
category: string; category: string;
canHaveChildren: boolean;
} }
export interface TestInfoImpl extends TestInfo { export interface TestInfoImpl extends TestInfo {
_testFinished: Promise<void>; _testFinished: Promise<void>;
_addStep: (category: string, title: string) => TestStepInternal; _addStep: (category: string, title: string, canHaveChildren: boolean) => TestStepInternal;
_currentSteps(): TestStepInternal[];
} }

View file

@ -219,7 +219,6 @@ export class WorkerRunner extends EventEmitter {
let testFinishedCallback = () => {}; let testFinishedCallback = () => {};
let lastStepId = 0; let lastStepId = 0;
const stepStack = new Set<TestStepInternal>();
const testInfo: TestInfoImpl = { const testInfo: TestInfoImpl = {
workerIndex: this._params.workerIndex, workerIndex: this._params.workerIndex,
project: this._project.config, project: this._project.config,
@ -268,18 +267,18 @@ export class WorkerRunner extends EventEmitter {
deadlineRunner.updateDeadline(deadline()); deadlineRunner.updateDeadline(deadline());
}, },
_testFinished: new Promise(f => testFinishedCallback = f), _testFinished: new Promise(f => testFinishedCallback = f),
_addStep: (category: string, title: string) => { _addStep: (category: string, title: string, canHaveChildren: boolean) => {
const stepId = `${category}@${title}@${++lastStepId}`; const stepId = `${category}@${title}@${++lastStepId}`;
let callbackHandled = false; let callbackHandled = false;
const step: TestStepInternal = { const step: TestStepInternal = {
category, category,
canHaveChildren,
complete: (error?: Error | TestError) => { complete: (error?: Error | TestError) => {
if (callbackHandled) if (callbackHandled)
return; return;
callbackHandled = true; callbackHandled = true;
if (error instanceof Error) if (error instanceof Error)
error = serializeError(error); error = serializeError(error);
stepStack.delete(step);
const payload: StepEndPayload = { const payload: StepEndPayload = {
testId, testId,
stepId, stepId,
@ -290,11 +289,11 @@ export class WorkerRunner extends EventEmitter {
this.emit('stepEnd', payload); this.emit('stepEnd', payload);
} }
}; };
stepStack.add(step);
const payload: StepBeginPayload = { const payload: StepBeginPayload = {
testId, testId,
stepId, stepId,
category, category,
canHaveChildren,
title, title,
wallTime: Date.now(), wallTime: Date.now(),
}; };
@ -302,7 +301,6 @@ export class WorkerRunner extends EventEmitter {
this.emit('stepBegin', payload); this.emit('stepBegin', payload);
return step; return step;
}, },
_currentSteps: () => [...stepStack],
}; };
// Inherit test.setTimeout() from parent suites. // Inherit test.setTimeout() from parent suites.
@ -425,7 +423,7 @@ export class WorkerRunner extends EventEmitter {
} }
private async _runTestWithBeforeHooks(test: TestCase, testInfo: TestInfoImpl) { private async _runTestWithBeforeHooks(test: TestCase, testInfo: TestInfoImpl) {
const step = testInfo._addStep('hook', 'Before Hooks'); const step = testInfo._addStep('hook', 'Before Hooks', true);
if (test._type === 'test') if (test._type === 'test')
await this._runBeforeHooks(test, testInfo); await this._runBeforeHooks(test, testInfo);
@ -459,7 +457,7 @@ export class WorkerRunner extends EventEmitter {
let step: TestStepInternal | undefined; let step: TestStepInternal | undefined;
let teardownError: TestError | undefined; let teardownError: TestError | undefined;
try { try {
step = testInfo._addStep('hook', 'After Hooks'); step = testInfo._addStep('hook', 'After Hooks', true);
if (test._type === 'test') if (test._type === 'test')
await this._runHooks(test.parent!, 'afterEach', testInfo); await this._runHooks(test.parent!, 'afterEach', testInfo);
} catch (error) { } catch (error) {

View file

@ -35,6 +35,7 @@ const CLIENT_LIB = path.join(ROOT_DIR, 'lib', 'client');
const CLIENT_SRC = path.join(ROOT_DIR, 'src', 'client'); const CLIENT_SRC = path.join(ROOT_DIR, 'src', 'client');
export type ParsedStackTrace = { export type ParsedStackTrace = {
allFrames: StackFrame[];
frames: StackFrame[]; frames: StackFrame[];
frameTexts: string[]; frameTexts: string[];
apiName: string; apiName: string;
@ -78,6 +79,7 @@ export function captureStackTrace(): ParsedStackTrace {
let apiName = ''; let apiName = '';
// Deepest transition between non-client code calling into client code // Deepest transition between non-client code calling into client code
// is the api entry. // is the api entry.
const allFrames = parsedFrames;
for (let i = 0; i < parsedFrames.length - 1; i++) { for (let i = 0; i < parsedFrames.length - 1; i++) {
if (parsedFrames[i].inClient && !parsedFrames[i + 1].inClient) { if (parsedFrames[i].inClient && !parsedFrames[i + 1].inClient) {
const frame = parsedFrames[i].frame; const frame = parsedFrames[i].frame;
@ -88,6 +90,7 @@ export function captureStackTrace(): ParsedStackTrace {
} }
return { return {
allFrames: allFrames.map(p => p.frame),
frames: parsedFrames.map(p => p.frame), frames: parsedFrames.map(p => p.frame),
frameTexts: parsedFrames.map(p => p.frameText), frameTexts: parsedFrames.map(p => p.frameText),
apiName apiName

View file

@ -80,7 +80,7 @@ const ProjectTreeItemView: React.FC<{
<StatsView stats={project.stats}></StatsView> <StatsView stats={project.stats}></StatsView>
</div> </div>
} loadChildren={hasChildren ? () => { } loadChildren={hasChildren ? () => {
return project.suites.filter(s => !(failingOnly && s.stats.ok)).map((s, i) => <SuiteTreeItemView key={i} suite={s} setTestId={setTestId} testId={testId} depth={1} showFileName={true} failingOnly={failingOnly}></SuiteTreeItemView>) || []; return project.suites.filter(s => !(failingOnly && s.stats.ok)).map((s, i) => <SuiteTreeItemView key={i} suite={s} setTestId={setTestId} testId={testId} depth={1} failingOnly={failingOnly}></SuiteTreeItemView>) || [];
} : undefined} depth={0} expandByDefault={true}></TreeItem>; } : undefined} depth={0} expandByDefault={true}></TreeItem>;
}; };
@ -90,17 +90,14 @@ const SuiteTreeItemView: React.FC<{
setTestId: (id: TestId) => void; setTestId: (id: TestId) => void;
failingOnly: boolean; failingOnly: boolean;
depth: number, depth: number,
showFileName: boolean, }> = ({ suite, testId, setTestId, failingOnly, depth }) => {
}> = ({ suite, testId, setTestId, showFileName, failingOnly, depth }) => {
const location = renderLocation(suite.location, showFileName);
return <TreeItem title={<div className='hbox'> return <TreeItem title={<div className='hbox'>
<div className='tree-text' title={suite.title}>{suite.title}</div> <div className='tree-text' title={suite.title}>{suite.title}</div>
<div style={{ flex: 'auto' }}></div> <div style={{ flex: 'auto' }}></div>
<StatsView stats={suite.stats}></StatsView> <StatsView stats={suite.stats}></StatsView>
{!!suite.location?.line && location && <div style={{ flex: 'none', padding: '0 4px', color: '#666' }}>{location}</div>}
</div> </div>
} loadChildren={() => { } loadChildren={() => {
const suiteChildren = suite.suites.filter(s => !(failingOnly && s.stats.ok)).map((s, i) => <SuiteTreeItemView key={i} suite={s} setTestId={setTestId} testId={testId} depth={depth + 1} showFileName={false} failingOnly={failingOnly}></SuiteTreeItemView>) || []; const suiteChildren = suite.suites.filter(s => !(failingOnly && s.stats.ok)).map((s, i) => <SuiteTreeItemView key={i} suite={s} setTestId={setTestId} testId={testId} depth={depth + 1} failingOnly={failingOnly}></SuiteTreeItemView>) || [];
const suiteCount = suite.suites.length; const suiteCount = suite.suites.length;
const testChildren = suite.tests.filter(t => !(failingOnly && t.ok)).map((t, i) => <TestTreeItemView key={i + suiteCount} test={t} setTestId={setTestId} testId={testId} depth={depth + 1}></TestTreeItemView>) || []; const testChildren = suite.tests.filter(t => !(failingOnly && t.ok)).map((t, i) => <TestTreeItemView key={i + suiteCount} test={t} setTestId={setTestId} testId={testId} depth={depth + 1}></TestTreeItemView>) || [];
return [...suiteChildren, ...testChildren]; return [...suiteChildren, ...testChildren];

View file

@ -141,11 +141,14 @@ export const playwrightFixtures: Fixtures<PlaywrightTestOptions & PlaywrightTest
if (trace) if (trace)
await context.tracing.start({ screenshots: true, snapshots: true }); await context.tracing.start({ screenshots: true, snapshots: true });
(context as any)._csi = { (context as any)._csi = {
onApiCall: (stackTrace: any) => { onApiCallBegin: (apiCall: string) => {
const testInfoImpl = testInfo as any; const testInfoImpl = testInfo as any;
const existingStep = testInfoImpl._currentSteps().find(step => step.category === 'pw:api' || step.category === 'expect'); const step = testInfoImpl._addStep('pw:api', apiCall, false);
const newStep = existingStep ? undefined : testInfoImpl._addStep('pw:api', stackTrace.apiName); return { userObject: step };
return (error?: Error) => newStep?.complete(error); },
onApiCallEnd: (data: { userObject: any }, error?: Error) => {
const step = data.userObject;
step?.complete(error);
}, },
}; };
return context; return context;

View file

@ -229,7 +229,9 @@ test('should report expect steps', async ({ runInlineTest }) => {
`%% 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\":\"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\"}`, `%% begin {\"title\":\"page.title\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"page.title\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\",\"steps\":[{\"title\":\"page.title\",\"category\":\"pw:api\"}]}`,
`%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
`%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
@ -281,22 +283,22 @@ test('should report api steps', async ({ runInlineTest }) => {
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`, `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"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\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"page.click(button)\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"page.click\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"page.click(button)\",\"category\":\"pw:api\"}`,
`%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
`%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"After Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.close\",\"category\":\"pw:api\"}]}`, `%% end {\"title\":\"After Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.close\",\"category\":\"pw:api\"}]}`,
`%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
`%% begin {\"title\":\"page.click\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"page.click(button)\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"page.click\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"page.click(button)\",\"category\":\"pw:api\"}`,
`%% 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\"}`,
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
`%% begin {\"title\":\"page.click\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"page.click(button)\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"page.click\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"page.click(button)\",\"category\":\"pw:api\"}`,
`%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
`%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
]); ]);
@ -328,8 +330,8 @@ test('should report api step failure', async ({ runInlineTest }) => {
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`, `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"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\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"page.click(input)\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"page.click\",\"category\":\"pw:api\",\"error\":{\"message\":\"page.click: Timeout 1ms exceeded.\\n=========================== logs ===========================\\nwaiting for selector \\\"input\\\"\\n============================================================\",\"stack\":\"<stack>\"}}`, `%% end {\"title\":\"page.click(input)\",\"category\":\"pw:api\",\"error\":{\"message\":\"page.click: Timeout 1ms exceeded.\\n=========================== logs ===========================\\nwaiting for selector \\\"input\\\"\\n============================================================\",\"stack\":\"<stack>\"}}`,
`%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
`%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,