mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
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> {
|
||||
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 {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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"}`,
|
||||
|
@ -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'
|
||||
|
@ -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)`,
|
||||
|
Loading…
Reference in New Issue
Block a user