Skip to content

chore: two-line trace view (2) #36055

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/playwright-core/src/client/channelOwner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,8 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
const validatedParams = validator(params, '', this._validatorToWireContext());
if (!apiZone.internal && !apiZone.reported) {
// Reporting/tracing/logging this api call for the first time.
apiZone.params = params;
apiZone.reported = true;
this._instrumentation.onApiCallBegin(apiZone);
this._instrumentation.onApiCallBegin(apiZone, { type: this._type, method: prop, params });
logApiCall(this._platform, this._logger, `=> ${apiZone.apiName} started`);
return await this._connection.sendMessageToServer(this, prop, validatedParams, apiZone);
}
Expand Down Expand Up @@ -238,7 +237,6 @@ function tChannelImplToWire(names: '*' | string[], arg: any, path: string, conte

type ApiZone = {
apiName: string;
params?: Record<string, any>;
frames: channels.StackFrame[];
title?: string;
internal?: boolean;
Expand Down
5 changes: 2 additions & 3 deletions packages/playwright-core/src/client/clientInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import type { StackFrame } from '@protocol/channels';
export interface ApiCallData {
apiName: string;
title?: string;
params?: Record<string, any>;
frames: StackFrame[];
userData: any;
stepId?: string;
Expand All @@ -33,7 +32,7 @@ export interface ClientInstrumentation {
addListener(listener: ClientInstrumentationListener): void;
removeListener(listener: ClientInstrumentationListener): void;
removeAllListeners(): void;
onApiCallBegin(apiCall: ApiCallData): void;
onApiCallBegin(apiCall: ApiCallData, channel: { type: string, method: string, params?: Record<string, any> }): void;
onApiCallEnd(apiCal: ApiCallData): void;
onWillPause(options: { keepTestTimeout: boolean }): void;

Expand All @@ -44,7 +43,7 @@ export interface ClientInstrumentation {
}

export interface ClientInstrumentationListener {
onApiCallBegin?(apiCall: ApiCallData): void;
onApiCallBegin?(apiCall: ApiCallData, channel: { type: string, method: string, params?: Record<string, any> }): void;
onApiCallEnd?(apiCall: ApiCallData): void;
onWillPause?(options: { keepTestTimeout: boolean }): void;

Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class Connection extends EventEmitter {
this._platform.log('channel', 'SEND> ' + JSON.stringify(message));
}
const location = options.frames?.[0] ? { file: options.frames[0].file, line: options.frames[0].line, column: options.frames[0].column } : undefined;
const metadata: channels.Metadata = { title: options.title, location, internal: options.internal, stepId: options.stepId, apiName: options.apiName };
const metadata: channels.Metadata = { title: options.title, location, internal: options.internal, stepId: options.stepId };
if (this._tracingCount && options.frames && type !== 'LocalUtils')
this._localUtils?.addStackToTracingNoReply({ callData: { stack: options.frames ?? [], id } }).catch(() => {});
// We need to exit zones before calling into the server, otherwise
Expand Down
4 changes: 1 addition & 3 deletions packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,7 @@ export class Locator implements api.Locator {
}

async dblclick(options: channels.ElementHandleDblclickOptions & TimeoutOptions = {}): Promise<void> {
await this._frame._wrapApiCall(async () => {
return await this._frame.dblclick(this._selector, { strict: true, ...options });
}, { title: 'Double click' });
await this._frame.dblclick(this._selector, { strict: true, ...options });
}

async dispatchEvent(type: string, eventInit: Object = {}, options?: TimeoutOptions) {
Expand Down
10 changes: 3 additions & 7 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,10 +543,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}

async unrouteAll(options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void> {
await this._wrapApiCall(async () => {
await this._unrouteInternal(this._routes, [], options?.behavior);
this._disposeHarRouters();
}, { title: 'Unroute all' });
await this._unrouteInternal(this._routes, [], options?.behavior);
this._disposeHarRouters();
}

async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise<void> {
Expand Down Expand Up @@ -656,9 +654,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}

async dblclick(selector: string, options?: channels.FrameDblclickOptions & TimeoutOptions) {
await this._wrapApiCall(async () => {
return await this._mainFrame.dblclick(selector, options);
}, { title: 'Double click' });
await this._mainFrame.dblclick(selector, options);
}

async tap(selector: string, options?: channels.FrameTapOptions & TimeoutOptions) {
Expand Down
10 changes: 5 additions & 5 deletions packages/playwright-core/src/protocol/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export const methodMetainfo = new Map<string, { internal?: boolean, title?: stri
['BrowserContext.setExtraHTTPHeaders', { title: 'Set extra HTTP headers', }],
['BrowserContext.setGeolocation', { title: 'Set geolocation', }],
['BrowserContext.setHTTPCredentials', { title: 'Set HTTP credentials', }],
['BrowserContext.setNetworkInterceptionPatterns', { }],
['BrowserContext.setWebSocketInterceptionPatterns', { title: 'Route web sockets', }],
['BrowserContext.setNetworkInterceptionPatterns', { internal: true, }],
['BrowserContext.setWebSocketInterceptionPatterns', { internal: true, }],
['BrowserContext.setOffline', { title: 'Set offline mode', }],
['BrowserContext.storageState', { title: 'Get storage state', }],
['BrowserContext.pause', { title: 'Pause', }],
Expand Down Expand Up @@ -114,8 +114,8 @@ export const methodMetainfo = new Map<string, { internal?: boolean, title?: stri
['Page.expectScreenshot', { title: 'Expect screenshot', snapshot: true, }],
['Page.screenshot', { title: 'Screenshot', snapshot: true, }],
['Page.setExtraHTTPHeaders', { title: 'Set extra HTTP headers', }],
['Page.setNetworkInterceptionPatterns', { title: 'Route', }],
['Page.setWebSocketInterceptionPatterns', { title: 'Route web sockets', }],
['Page.setNetworkInterceptionPatterns', { internal: true, }],
['Page.setWebSocketInterceptionPatterns', { internal: true, }],
['Page.setViewportSize', { title: 'Set viewport size', snapshot: true, }],
['Page.keyboardDown', { title: 'Key down "{key}"', slowMo: true, snapshot: true, }],
['Page.keyboardUp', { title: 'Key up "{key}"', slowMo: true, snapshot: true, }],
Expand Down Expand Up @@ -257,7 +257,7 @@ export const methodMetainfo = new Map<string, { internal?: boolean, title?: stri
['Dialog.dismiss', { title: 'Dismiss dialog', }],
['Tracing.tracingStart', { internal: true, }],
['Tracing.tracingStartChunk', { internal: true, }],
['Tracing.tracingGroup', { }],
['Tracing.tracingGroup', { title: 'Trace "{name}"', }],
['Tracing.tracingGroupEnd', { title: 'Group end', }],
['Tracing.tracingStopChunk', { internal: true, }],
['Tracing.tracingStop', { internal: true, }],
Expand Down
48 changes: 48 additions & 0 deletions packages/playwright-core/src/protocol/formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export function formatProtocolParam(params: Record<string, string> | undefined, name: string): string {
if (!params)
return '';
if (name === 'url') {
try {
const urlObject = new URL(params[name]);
if (urlObject.protocol === 'data:')
return urlObject.protocol;
if (urlObject.protocol === 'about:')
return params[name];
return urlObject.pathname + urlObject.search;
} catch (error) {
return params[name];
}
}
if (name === 'timeNumber')
return new Date(params[name]).toString();
return deepParam(params, name);
}

function deepParam(params: Record<string, any>, name: string): string {
const tokens = name.split('.');
let current = params;
for (const token of tokens) {
if (typeof current !== 'object' || current === null)
return '';
current = current[token];
}
if (current === undefined)
return '';
return String(current);
}
1 change: 0 additions & 1 deletion packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ scheme.Metadata = tObject({
line: tOptional(tNumber),
column: tOptional(tNumber),
})),
apiName: tOptional(tString),
title: tOptional(tString),
internal: tOptional(tBoolean),
stepId: tOptional(tString),
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolea

function shouldPauseBeforeStep(metadata: CallMetadata): boolean {
// Don't stop on internal.
if (!metadata.apiName || metadata.internal)
if (metadata.internal)
return false;
// Always stop on 'close'
if (metadata.method === 'close')
Expand Down
38 changes: 2 additions & 36 deletions packages/playwright-core/src/server/dispatchers/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { EventEmitter } from 'events';

import { eventsHelper } from '../utils/eventsHelper';
import { ValidationError, createMetadataValidator, findValidator } from '../../protocol/validator';
import { LongStandingScope, assert, monotonicTime, rewriteErrorMessage } from '../../utils';
import { LongStandingScope, assert, formatProtocolParam, monotonicTime, rewriteErrorMessage } from '../../utils';
import { isUnderTest } from '../utils/debug';
import { TargetClosedError, isTargetClosedError, serializeError } from '../errors';
import { SdkObject } from '../instrumentation';
Expand Down Expand Up @@ -309,7 +309,6 @@ export class DispatcherConnection {
const callMetadata: CallMetadata = {
id: `call@${id}`,
location: validMetadata.location,
apiName: validMetadata.apiName,
title: renderTitle(dispatcher._type, method, params, validMetadata.title),
internal: validMetadata.internal,
stepId: validMetadata.stepId,
Expand Down Expand Up @@ -391,42 +390,9 @@ function closeReason(sdkObject: SdkObject): string | undefined {
sdkObject.attribution.browser?._closeReason;
}

function formatParam(params: Record<string, string> | undefined, name: string): string {
if (!params)
return '';
if (name === 'url') {
try {
const urlObject = new URL(params[name]);
if (urlObject.protocol === 'data:')
return urlObject.protocol;
if (urlObject.protocol === 'about:')
return params[name];
return urlObject.pathname + urlObject.search;
} catch (error) {
return params[name];
}
}
if (name === 'timeNumber')
return new Date(params[name]).toString();
return deepParam(params, name);
}

function deepParam(params: Record<string, any>, name: string): string {
const tokens = name.split('.');
let current = params;
for (const token of tokens) {
if (typeof current !== 'object' || current === null)
return '';
current = current[token];
}
if (current === undefined)
return '';
return String(current);
}

function renderTitle(type: string, method: string, params: Record<string, string> | undefined, title?: string) {
const titleFormat = title ?? methodMetainfo.get(type + '.' + method)?.title ?? method;
return titleFormat.replace(/\{([^}]+)\}/g, (_, p1) => {
return formatParam(params, p1);
return formatProtocolParam(params, p1);
});
}
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1432,7 +1432,7 @@ export class Frame extends SdkObject {

// Step 1: perform locator handlers checkpoint with a specified timeout.
await (new ProgressController(metadata, this)).run(async progress => {
progress.log(`${metadata.apiName}${timeout ? ` with timeout ${timeout}ms` : ''}`);
progress.log(`${metadata.title}${timeout ? ` with timeout ${timeout}ms` : ''}`);
progress.log(`waiting for ${this._asLocator(selector)}`);
await this._page.performActionPreChecks(progress);
}, timeout);
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ export class Page extends SdkObject {
let actual: Buffer | undefined;
let previous: Buffer | undefined;
const pollIntervals = [0, 100, 250, 500];
progress.log(`${metadata.apiName}${callTimeout ? ` with timeout ${callTimeout}ms` : ''}`);
progress.log(`${metadata.title}${callTimeout ? ` with timeout ${callTimeout}ms` : ''}`);
if (options.expected)
progress.log(` verifying given screenshot expectation`);
else
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,7 @@ export * from './server/utils/wsServer';
export * from './server/utils/zipFile';
export * from './server/utils/zones';

export * from './protocol/debug';
export * from './protocol/formatter';

export { colors } from './utilsBundle';
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ export interface LocatorFactory {
chainLocators(locators: string[]): string;
}

export function asLocatorDescription(selector: string): string | undefined {
export function asLocatorDescription(lang: Language, selector: string): string | undefined {
const parsed = parseSelector(selector);
const describe = parsed.parts.findLast(part => part.name === 'internal:describe');
if (describe)
return JSON.parse(describe.body as string);
return undefined;
return asLocator(lang, selector);
}

export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false): string {
Expand Down
61 changes: 14 additions & 47 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path';

import * as playwrightLibrary from 'playwright-core';
import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, isString, jsonStringifyForceASCII, asLocator, asLocatorDescription } from 'playwright-core/lib/utils';
import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringifyForceASCII, methodMetainfo, asLocatorDescription, formatProtocolParam } from 'playwright-core/lib/utils';

import { currentTestInfo } from './common/globals';
import { rootTestType } from './common/testType';
Expand All @@ -27,7 +27,7 @@ import { attachErrorContext } from './errorContext';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { ContextReuseMode } from './common/config';
import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
import type { ApiCallData, ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
import type { Playwright as PlaywrightImpl } from '../../playwright-core/src/client/playwright';
import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';

Expand Down Expand Up @@ -258,7 +258,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({

const tracingGroupSteps: TestStepInternal[] = [];
const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (data: ApiCallData) => {
onApiCallBegin: (data, channel) => {
const testInfo = currentTestInfo();
// Some special calls do not get into steps.
if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd')
Expand All @@ -274,20 +274,21 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
data.stepId = zone.stepId;
return;
}

// In the general case, create a step for each api call and connect them through the stepId.
const step = testInfo._addStep({
location: data.frames[0],
category: 'pw:api',
title: renderApiCall(data.apiName, data.params),
title: renderTitle(channel.type, channel.method, channel.params, data.title),
apiName: data.apiName,
params: data.params,
params: channel.params,
}, tracingGroupSteps[tracingGroupSteps.length - 1]);
data.userData = step;
data.stepId = step.stepId;
if (data.apiName === 'tracing.group')
tracingGroupSteps.push(step);
},
onApiCallEnd: (data: ApiCallData) => {
onApiCallEnd: data => {
// "tracing.group" step will end later, when "tracing.groupEnd" finishes.
if (data.apiName === 'tracing.group')
return;
Expand Down Expand Up @@ -756,47 +757,13 @@ class ArtifactsRecorder {
}
}

function paramsToRender(apiName: string) {
switch (apiName) {
case 'locator.fill':
return ['value'];
default:
return ['url', 'selector', 'text', 'key'];
}
}

function renderApiCall(apiName: string, params: any) {
if (apiName === 'tracing.group')
return params.name;
const paramsArray = [];
if (params) {
for (const name of paramsToRender(apiName)) {
if (!(name in params))
continue;
if (name === 'selector' && isString(params[name])) {
const description = asLocatorDescription(params[name]);
if (description) {
const replacement = JSON.stringify(description);
apiName = apiName.replace(/^locator\.(.*)/, `$1 ${replacement}`);
apiName = apiName.replace(/^page\.(.*)/, `$1 ${replacement}`);
apiName = apiName.replace(/^frame\.(.*)/, `$1 ${replacement}`);
} else if (params[name].startsWith('internal:')) {
const replacement = asLocator('javascript', params[name]) + '.';
apiName = apiName.replace(/^locator\./, replacement);
apiName = apiName.replace(/^page\./, replacement);
apiName = apiName.replace(/^frame\./, replacement);
} else {
const value = params[name];
paramsArray.push(value);
}
} else {
const value = params[name];
paramsArray.push(value);
}
}
}
const paramsText = paramsArray.length ? '(' + paramsArray.join(', ') + ')' : '';
return apiName + paramsText;
function renderTitle(type: string, method: string, params: Record<string, string> | undefined, title?: string) {
const titleFormat = title ?? methodMetainfo.get(type + '.' + method)?.title ?? method;
const prefix = titleFormat.replace(/\{([^}]+)\}/g, (_, p1) => formatProtocolParam(params, p1));
let selector;
if (params?.['selector'])
selector = asLocatorDescription('javascript', params.selector);
return prefix + (selector ? ` ${selector}` : '');
}

function tracing() {
Expand Down
Loading
Loading