This commit is contained in:
Dmitry Gozman 2025-01-04 10:44:19 +00:00
parent 5a22475ea8
commit 6429c7672e
3 changed files with 54 additions and 47 deletions

View file

@ -148,15 +148,15 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
if (validator) { if (validator) {
return async (params: any) => { return async (params: any) => {
return await this._wrapApiCall(async apiZone => { return await this._wrapApiCall(async apiZone => {
const { apiName, frames, csi, callCookie, stepId } = apiZone.reported ? { apiName: undefined, csi: undefined, callCookie: undefined, frames: [], stepId: undefined } : apiZone; const validatedParams = validator(params, '', { tChannelImpl: tChannelImplToWire, binary: this._connection.rawBuffers() ? 'buffer' : 'toBase64' });
apiZone.reported = true; if (!apiZone.isInternal && !apiZone.reported) {
let currentStepId = stepId; apiZone.params = params;
if (csi && apiName) { apiZone.reported = true;
const out: { stepId?: string } = {}; logApiCall(this._logger, `=> ${apiZone.apiName} started`);
csi.onApiCallBegin(apiName, params, frames, callCookie, out); apiZone.csi?.onApiCallBegin(apiZone);
currentStepId = out.stepId; return await this._connection.sendMessageToServer(this, prop, validatedParams, apiZone.apiName, apiZone.frames, apiZone.stepId);
} }
return await this._connection.sendMessageToServer(this, prop, validator(params, '', { tChannelImpl: tChannelImplToWire, binary: this._connection.rawBuffers() ? 'buffer' : 'toBase64' }), apiName, frames, currentStepId); return await this._connection.sendMessageToServer(this, prop, validatedParams, undefined, [], undefined);
}); });
}; };
} }
@ -170,9 +170,9 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> { async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> {
const logger = this._logger; const logger = this._logger;
const apiZone = zones.zoneData<ApiZone>('apiZone'); const existingApiZone = zones.zoneData<ApiZone>('apiZone');
if (apiZone) if (existingApiZone)
return await func(apiZone); return await func(existingApiZone);
const stackTrace = captureLibraryStackTrace(); const stackTrace = captureLibraryStackTrace();
let apiName: string | undefined = stackTrace.apiName; let apiName: string | undefined = stackTrace.apiName;
@ -180,26 +180,23 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
if (isInternal === undefined) if (isInternal === undefined)
isInternal = this._isInternalType; isInternal = this._isInternalType;
if (isInternal)
apiName = undefined;
// Enclosing zone could have provided the apiName and wallTime. // Enclosing zone could have provided the apiName and wallTime.
const expectZone = zones.zoneData<ExpectZone>('expectZone'); const expectZone = zones.zoneData<ExpectZone>('expectZone');
const stepId = expectZone?.stepId; const stepId = expectZone?.stepId;
if (!isInternal && expectZone) if (expectZone)
apiName = expectZone.title; apiName = expectZone.title;
// If we are coming from the expectZone, there is no need to generate a new // If we are coming from the expectZone, there is no need to generate a new
// step for the API call, since it will be generated by the expect itself. // step for the API call, since it will be generated by the expect itself.
const csi = isInternal || expectZone ? undefined : this._instrumentation; const csi = isInternal || expectZone ? undefined : this._instrumentation;
const callCookie: any = {}; const apiZone: ApiZone = { apiName, frames, isInternal, reported: false, csi, userData: undefined, stepId };
try { try {
logApiCall(logger, `=> ${apiName} started`, isInternal);
const apiZone: ApiZone = { apiName, frames, isInternal, reported: false, csi, callCookie, stepId };
const result = await zones.run('apiZone', apiZone, async () => await func(apiZone)); const result = await zones.run('apiZone', apiZone, async () => await func(apiZone));
csi?.onApiCallEnd(callCookie); csi?.onApiCallEnd(apiZone);
logApiCall(logger, `<= ${apiName} succeeded`, isInternal); if (!isInternal)
logApiCall(logger, `<= ${apiName} succeeded`);
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 : '';
@ -210,8 +207,9 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
e.stack = e.message + stackFrames; e.stack = e.message + stackFrames;
else else
e.stack = ''; e.stack = '';
csi?.onApiCallEnd(callCookie, e); csi?.onApiCallEnd(apiZone);
logApiCall(logger, `<= ${apiName} failed`, isInternal); if (!isInternal)
logApiCall(logger, `<= ${apiName} failed`);
throw e; throw e;
} }
} }
@ -232,9 +230,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
} }
} }
function logApiCall(logger: Logger | undefined, message: string, isNested: boolean) { function logApiCall(logger: Logger | undefined, message: string) {
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);
@ -247,11 +243,13 @@ function tChannelImplToWire(names: '*' | string[], arg: any, path: string, conte
} }
type ApiZone = { type ApiZone = {
apiName: string | undefined; apiName: string;
params?: Record<string, any>;
frames: channels.StackFrame[]; frames: channels.StackFrame[];
isInternal: boolean; isInternal: boolean;
reported: boolean; reported: boolean;
csi: ClientInstrumentation | undefined; csi: ClientInstrumentation | undefined;
callCookie: any; userData: any;
stepId?: string; stepId?: string;
error?: Error;
}; };

View file

@ -18,12 +18,22 @@ import type { StackFrame } from '@protocol/channels';
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
import type { APIRequestContext } from './fetch'; import type { APIRequestContext } from './fetch';
// Instrumentation can mutate the data, for example change apiName or stepId.
export interface ApiCallData {
apiName: string;
params?: Record<string, any>;
frames: StackFrame[];
userData: any;
stepId?: string;
error?: Error;
}
export interface ClientInstrumentation { export interface ClientInstrumentation {
addListener(listener: ClientInstrumentationListener): void; addListener(listener: ClientInstrumentationListener): void;
removeListener(listener: ClientInstrumentationListener): void; removeListener(listener: ClientInstrumentationListener): void;
removeAllListeners(): void; removeAllListeners(): void;
onApiCallBegin(apiCall: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }): void; onApiCallBegin(apiCall: ApiCallData): void;
onApiCallEnd(userData: any, error?: Error): void; onApiCallEnd(apiCal: ApiCallData): void;
onWillPause(options: { keepTestTimeout: boolean }): void; onWillPause(options: { keepTestTimeout: boolean }): void;
runAfterCreateBrowserContext(context: BrowserContext): Promise<void>; runAfterCreateBrowserContext(context: BrowserContext): Promise<void>;
@ -33,8 +43,8 @@ export interface ClientInstrumentation {
} }
export interface ClientInstrumentationListener { export interface ClientInstrumentationListener {
onApiCallBegin?(apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }): void; onApiCallBegin?(apiCall: ApiCallData): void;
onApiCallEnd?(userData: any, error?: Error): void; onApiCallEnd?(apiCall: ApiCallData): void;
onWillPause?(options: { keepTestTimeout: boolean }): void; onWillPause?(options: { keepTestTimeout: boolean }): void;
runAfterCreateBrowserContext?(context: BrowserContext): Promise<void>; runAfterCreateBrowserContext?(context: BrowserContext): Promise<void>;

View file

@ -23,7 +23,7 @@ import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWor
import type { TestInfoImpl, TestStepInternal } from './worker/testInfo'; import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
import { rootTestType } from './common/testType'; import { rootTestType } from './common/testType';
import type { ContextReuseMode } from './common/config'; import type { ContextReuseMode } from './common/config';
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation'; import type { ApiCallData, ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
import { currentTestInfo } from './common/globals'; import { currentTestInfo } from './common/globals';
export { expect } from './matchers/expect'; export { expect } from './matchers/expect';
export const _baseTest: TestType<{}, {}> = rootTestType.test; export const _baseTest: TestType<{}, {}> = rootTestType.test;
@ -258,34 +258,33 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
const tracingGroupSteps: TestStepInternal[] = []; const tracingGroupSteps: TestStepInternal[] = [];
const csiListener: ClientInstrumentationListener = { const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) => { onApiCallBegin: (data: ApiCallData) => {
userData.apiName = apiName;
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo || apiName.includes('setTestIdAttribute') || apiName === 'tracing.groupEnd') if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd')
return; return;
const step = testInfo._addStep({ const step = testInfo._addStep({
location: frames[0] as any, location: data.frames[0],
category: 'pw:api', category: 'pw:api',
title: renderApiCall(apiName, params), title: renderApiCall(data.apiName, data.params),
apiName, apiName: data.apiName,
params, params: data.params,
}, tracingGroupSteps[tracingGroupSteps.length - 1]); }, tracingGroupSteps[tracingGroupSteps.length - 1]);
userData.step = step; data.userData = step;
out.stepId = step.stepId; data.stepId = step.stepId;
if (apiName === 'tracing.group') if (data.apiName === 'tracing.group')
tracingGroupSteps.push(step); tracingGroupSteps.push(step);
}, },
onApiCallEnd: (userData: any, error?: Error) => { onApiCallEnd: (data: ApiCallData) => {
// "tracing.group" step will end later, when "tracing.groupEnd" finishes. // "tracing.group" step will end later, when "tracing.groupEnd" finishes.
if (userData.apiName === 'tracing.group') if (data.apiName === 'tracing.group')
return; return;
if (userData.apiName === 'tracing.groupEnd') { if (data.apiName === 'tracing.groupEnd') {
const step = tracingGroupSteps.pop(); const step = tracingGroupSteps.pop();
step?.complete({ error }); step?.complete({ error: data.error });
return; return;
} }
const step = userData.step; const step = data.userData;
step?.complete({ error }); step?.complete({ error: data.error });
}, },
onWillPause: ({ keepTestTimeout }) => { onWillPause: ({ keepTestTimeout }) => {
if (!keepTestTimeout) if (!keepTestTimeout)