diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index c3f90b9693..48c0cbb832 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -86,11 +86,13 @@ export class Browser extends ChannelOwner implements ap } async newPage(options: BrowserContextOptions = {}): Promise { - 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 { diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 9851382570..7999255af9 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -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 } 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); diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 2091355c93..92e3c5af40 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -51,8 +51,6 @@ export class BrowserType extends ChannelOwner imple _defaultContextOptions?: BrowserContextOptions; private _defaultLaunchOptions?: LaunchOptions; private _defaultConnectOptions?: ConnectOptions; - private _onDidCreateContext?: (context: BrowserContext) => Promise; - private _onWillCloseContext?: (context: BrowserContext) => Promise; static from(browserType: channels.BrowserTypeChannel): BrowserType { return (browserType as any)._object; @@ -254,11 +252,11 @@ export class BrowserType extends ChannelOwner 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); } } diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index a147dbec4f..0e3d72cef6 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -39,17 +39,17 @@ export abstract class ChannelOwner; _logger: Logger | undefined; - _instrumentation: ClientInstrumentation | undefined; + readonly _instrumentation: ClientInstrumentation; private _eventToSubscriptionMapping: Map = new Map(); - constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits, instrumentation?: ClientInstrumentation) { + constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits) { 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 ${apiName} started`, isInternal); const apiZone = { stackTrace, isInternal, reported: false, csi, callCookie, wallTime }; - const result = await zones.run('apiZone', apiZone, async () => { + const result = await zones.run>('apiZone', apiZone, async () => { return await func(apiZone); }); csi?.onApiCallEnd(callCookie); diff --git a/packages/playwright-core/src/client/clientInstrumentation.ts b/packages/playwright-core/src/client/clientInstrumentation.ts index a90f2964ad..3a6ed4af63 100644 --- a/packages/playwright-core/src/client/clientInstrumentation.ts +++ b/packages/playwright-core/src/client/clientInstrumentation.ts @@ -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; + onDidCreateRequestContext(context: APIRequestContext): Promise; + onWillPause(): void; + onWillCloseBrowserContext(context: BrowserContext): Promise; + onWillCloseRequestContext(context: APIRequestContext): Promise; } 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; + onDidCreateRequestContext?(context: APIRequestContext): Promise; + onWillPause?(): void; + onWillCloseBrowserContext?(context: BrowserContext): Promise; + onWillCloseRequestContext?(context: APIRequestContext): Promise; } export function createInstrumentation(): ClientInstrumentation { diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 4894c68e32..39760fb22c 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -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 { 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(); diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index c1fcdaeb30..a7f830a904 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -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; - _onWillCloseContext?: (context: APIRequestContext) => Promise; 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 { - await this._request?._onWillCloseContext?.(this); + await this._instrumentation.onWillCloseRequestContext(this); await this._channel.dispose(); this._request?._contexts.delete(this); } diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 28c9b27ecb..b9b1d9768b 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -691,6 +691,9 @@ export class Page extends ChannelOwner 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()); } diff --git a/packages/playwright-core/src/utils/zones.ts b/packages/playwright-core/src/utils/zones.ts index c99452d082..051cbf396d 100644 --- a/packages/playwright-core/src/utils/zones.ts +++ b/packages/playwright-core/src/utils/zones.ts @@ -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>(); - run(type: ZoneType, data: T, func: (data: T) => R | Promise): R | Promise { + run(type: ZoneType, data: T, func: (data: T) => R): R { return new Zone(this, ++this.lastZoneId, type, data).run(func); } zoneData(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(callback: () => Promise): Promise { + 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 { @@ -55,16 +70,16 @@ class Zone { this.wallTime = Date.now(); } - run(func: (data: T) => R | Promise): R | Promise { + run(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(func: () => R | Promise, finallyFunc: Function): R | Promise { +export function runWithFinally(func: () => R, finallyFunc: Function): R { try { const result = func(); if (result instanceof Promise) { @@ -74,7 +89,7 @@ export function runWithFinally(func: () => R | Promise, finallyFunc: Funct }).catch(e => { finallyFunc(); throw e; - }); + }) as any; } finallyFunc(); return result; diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index 6e03c03878..71311812ab 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -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 = ({ const reusedContexts = new Set(); 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 = ({ } }; - 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 = ({ 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[] = []; + const promises: (Promise | 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); - 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 = ({ }; // 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); - (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'; diff --git a/packages/playwright-test/src/worker/fixtureRunner.ts b/packages/playwright-test/src/worker/fixtureRunner.ts index 93468b7bdd..9f7935b0b7 100644 --- a/packages/playwright-test/src/worker/fixtureRunner.ts +++ b/packages/playwright-test/src/worker/fixtureRunner.ts @@ -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 diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index faa3cb5f4b..b89505a217 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -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) => { diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index cd42edf223..58f6374751 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -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,)\",\"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"}`, diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index d35af034e4..fef4925516 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -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' diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index dfd4e4a0c1..e83e75e7b6 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -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)`,