chore: two-line trace view (3) (#36058)

This commit is contained in:
Pavel Feldman 2025-05-22 19:00:33 -07:00 committed by GitHub
parent 3a8592910f
commit a15e94aa3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 95 additions and 82 deletions

View File

@ -16,7 +16,7 @@
import { EventEmitter } from './eventEmitter'; import { EventEmitter } from './eventEmitter';
import { ValidationError, maybeFindValidator } from '../protocol/validator'; import { ValidationError, maybeFindValidator } from '../protocol/validator';
import { methodMetainfo } from '../protocol/debug'; import { methodMetainfo } from '../utils/isomorphic/protocolMetainfo';
import { captureLibraryStackTrace } from './clientStackTrace'; import { captureLibraryStackTrace } from './clientStackTrace';
import { stringifyStackFrames } from '../utils/isomorphic/stackTrace'; import { stringifyStackFrames } from '../utils/isomorphic/stackTrace';

View File

@ -18,7 +18,7 @@ import { EventEmitter } from 'events';
import { debugMode, isUnderTest, monotonicTime } from '../utils'; import { debugMode, isUnderTest, monotonicTime } from '../utils';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
import { methodMetainfo } from '../protocol/debug'; import { methodMetainfo } from '../utils/isomorphic/protocolMetainfo';
import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';

View File

@ -18,13 +18,12 @@ import { EventEmitter } from 'events';
import { eventsHelper } from '../utils/eventsHelper'; import { eventsHelper } from '../utils/eventsHelper';
import { ValidationError, createMetadataValidator, findValidator } from '../../protocol/validator'; import { ValidationError, createMetadataValidator, findValidator } from '../../protocol/validator';
import { LongStandingScope, assert, formatProtocolParam, monotonicTime, rewriteErrorMessage } from '../../utils'; import { LongStandingScope, assert, monotonicTime, rewriteErrorMessage } from '../../utils';
import { isUnderTest } from '../utils/debug'; import { isUnderTest } from '../utils/debug';
import { TargetClosedError, isTargetClosedError, serializeError } from '../errors'; import { TargetClosedError, isTargetClosedError, serializeError } from '../errors';
import { SdkObject } from '../instrumentation'; import { SdkObject } from '../instrumentation';
import { isProtocolError } from '../protocolError'; import { isProtocolError } from '../protocolError';
import { compressCallLog } from '../callLog'; import { compressCallLog } from '../callLog';
import { methodMetainfo } from '../../protocol/debug';
import type { CallMetadata } from '../instrumentation'; import type { CallMetadata } from '../instrumentation';
import type { PlaywrightDispatcher } from './playwrightDispatcher'; import type { PlaywrightDispatcher } from './playwrightDispatcher';
@ -309,7 +308,7 @@ export class DispatcherConnection {
const callMetadata: CallMetadata = { const callMetadata: CallMetadata = {
id: `call@${id}`, id: `call@${id}`,
location: validMetadata.location, location: validMetadata.location,
title: renderTitle(dispatcher._type, method, params, validMetadata.title), title: validMetadata.title,
internal: validMetadata.internal, internal: validMetadata.internal,
stepId: validMetadata.stepId, stepId: validMetadata.stepId,
objectId: sdkObject?.guid, objectId: sdkObject?.guid,
@ -389,10 +388,3 @@ function closeReason(sdkObject: SdkObject): string | undefined {
sdkObject.attribution.context?._closeReason || sdkObject.attribution.context?._closeReason ||
sdkObject.attribution.browser?._closeReason; sdkObject.attribution.browser?._closeReason;
} }
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 formatProtocolParam(params, p1);
});
}

View File

@ -27,7 +27,7 @@ import * as network from './network';
import { Page } from './page'; import { Page } from './page';
import { ProgressController } from './progress'; import { ProgressController } from './progress';
import * as types from './types'; import * as types from './types';
import { LongStandingScope, asLocator, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils'; import { LongStandingScope, asLocator, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime, renderTitleForCall } from '../utils';
import { isSessionClosedError } from './protocolError'; import { isSessionClosedError } from './protocolError';
import { debugLogger } from './utils/debugLogger'; import { debugLogger } from './utils/debugLogger';
import { eventsHelper } from './utils/eventsHelper'; import { eventsHelper } from './utils/eventsHelper';
@ -1432,7 +1432,7 @@ export class Frame extends SdkObject {
// Step 1: perform locator handlers checkpoint with a specified timeout. // Step 1: perform locator handlers checkpoint with a specified timeout.
await (new ProgressController(metadata, this)).run(async progress => { await (new ProgressController(metadata, this)).run(async progress => {
progress.log(`${metadata.title}${timeout ? ` with timeout ${timeout}ms` : ''}`); progress.log(`${renderTitleForCall(metadata)}${timeout ? ` with timeout ${timeout}ms` : ''}`);
progress.log(`waiting for ${this._asLocator(selector)}`); progress.log(`waiting for ${this._asLocator(selector)}`);
await this._page.performActionPreChecks(progress); await this._page.performActionPreChecks(progress);
}, timeout); }, timeout);

View File

@ -27,7 +27,7 @@ import { SdkObject } from './instrumentation';
import * as js from './javascript'; import * as js from './javascript';
import { ProgressController } from './progress'; import { ProgressController } from './progress';
import { Screenshotter, validateScreenshotOptions } from './screenshotter'; import { Screenshotter, validateScreenshotOptions } from './screenshotter';
import { LongStandingScope, assert, trimStringWithEllipsis } from '../utils'; import { LongStandingScope, assert, renderTitleForCall, trimStringWithEllipsis } from '../utils';
import { asLocator } from '../utils'; import { asLocator } from '../utils';
import { getComparator } from './utils/comparators'; import { getComparator } from './utils/comparators';
import { debugLogger } from './utils/debugLogger'; import { debugLogger } from './utils/debugLogger';
@ -624,7 +624,7 @@ export class Page extends SdkObject {
let actual: Buffer | undefined; let actual: Buffer | undefined;
let previous: Buffer | undefined; let previous: Buffer | undefined;
const pollIntervals = [0, 100, 250, 500]; const pollIntervals = [0, 100, 250, 500];
progress.log(`${metadata.title}${callTimeout ? ` with timeout ${callTimeout}ms` : ''}`); progress.log(`${renderTitleForCall(metadata)}${callTimeout ? ` with timeout ${callTimeout}ms` : ''}`);
if (options.expected) if (options.expected)
progress.log(` verifying given screenshot expectation`); progress.log(` verifying given screenshot expectation`);
else else

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { renderTitleForCall } from '../../utils/isomorphic/protocolFormatter';
import type { Frame } from '../frames'; import type { Frame } from '../frames';
import type { CallMetadata } from '../instrumentation'; import type { CallMetadata } from '../instrumentation';
import type { Page } from '../page'; import type { Page } from '../page';
@ -25,7 +27,7 @@ export function buildFullSelector(framePath: string[], selector: string) {
} }
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
const title = metadata.title; const title = renderTitleForCall(metadata);
if (metadata.error) if (metadata.error)
status = 'error'; status = 'error';
const params = { const params = {

View File

@ -19,7 +19,7 @@ import os from 'os';
import path from 'path'; import path from 'path';
import { Snapshotter } from './snapshotter'; import { Snapshotter } from './snapshotter';
import { methodMetainfo } from '../../../protocol/debug'; import { methodMetainfo } from '../../../utils/isomorphic/protocolMetainfo';
import { assert } from '../../../utils/isomorphic/assert'; import { assert } from '../../../utils/isomorphic/assert';
import { monotonicTime } from '../../../utils/isomorphic/time'; import { monotonicTime } from '../../../utils/isomorphic/time';
import { eventsHelper } from '../../utils/eventsHelper'; import { eventsHelper } from '../../utils/eventsHelper';

View File

@ -21,6 +21,8 @@ export * from './utils/isomorphic/locatorGenerators';
export * from './utils/isomorphic/manualPromise'; export * from './utils/isomorphic/manualPromise';
export * from './utils/isomorphic/mimeType'; export * from './utils/isomorphic/mimeType';
export * from './utils/isomorphic/multimap'; export * from './utils/isomorphic/multimap';
export * from './utils/isomorphic/protocolFormatter';
export * from './utils/isomorphic/protocolMetainfo';
export * from './utils/isomorphic/rtti'; export * from './utils/isomorphic/rtti';
export * from './utils/isomorphic/semaphore'; export * from './utils/isomorphic/semaphore';
export * from './utils/isomorphic/stackTrace'; export * from './utils/isomorphic/stackTrace';
@ -52,7 +54,4 @@ export * from './server/utils/wsServer';
export * from './server/utils/zipFile'; export * from './server/utils/zipFile';
export * from './server/utils/zones'; export * from './server/utils/zones';
export * from './protocol/debug';
export * from './protocol/formatter';
export { colors } from './utilsBundle'; export { colors } from './utilsBundle';

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { methodMetainfo } from './protocolMetainfo';
export function formatProtocolParam(params: Record<string, string> | undefined, name: string): string { export function formatProtocolParam(params: Record<string, string> | undefined, name: string): string {
if (!params) if (!params)
return ''; return '';
@ -29,8 +31,10 @@ export function formatProtocolParam(params: Record<string, string> | undefined,
return params[name]; return params[name];
} }
} }
if (name === 'timeNumber') if (name === 'timeNumber') {
// eslint-disable-next-line no-restricted-globals
return new Date(params[name]).toString(); return new Date(params[name]).toString();
}
return deepParam(params, name); return deepParam(params, name);
} }
@ -46,3 +50,10 @@ function deepParam(params: Record<string, any>, name: string): string {
return ''; return '';
return String(current); return String(current);
} }
export function renderTitleForCall(metadata: { title?: string, type: string, method: string, params: Record<string, string> | undefined }) {
const titleFormat = metadata.title ?? methodMetainfo.get(metadata.type + '.' + metadata.method)?.title ?? metadata.method;
return titleFormat.replace(/\{([^}]+)\}/g, (_, p1) => {
return formatProtocolParam(metadata.params, p1);
});
}

View File

@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import * as playwrightLibrary from 'playwright-core'; import * as playwrightLibrary from 'playwright-core';
import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringifyForceASCII, methodMetainfo, asLocatorDescription, formatProtocolParam } from 'playwright-core/lib/utils'; import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringifyForceASCII, asLocatorDescription, renderTitleForCall } from 'playwright-core/lib/utils';
import { currentTestInfo } from './common/globals'; import { currentTestInfo } from './common/globals';
import { rootTestType } from './common/testType'; import { rootTestType } from './common/testType';
@ -758,8 +758,7 @@ class ArtifactsRecorder {
} }
function renderTitle(type: string, method: string, params: Record<string, string> | undefined, title?: string) { function renderTitle(type: string, method: string, params: Record<string, string> | undefined, title?: string) {
const titleFormat = title ?? methodMetainfo.get(type + '.' + method)?.title ?? method; const prefix = renderTitleForCall({ title, type, method, params });
const prefix = titleFormat.replace(/\{([^}]+)\}/g, (_, p1) => formatProtocolParam(params, p1));
let selector; let selector;
if (params?.['selector']) if (params?.['selector'])
selector = asLocatorDescription('javascript', params.selector); selector = asLocatorDescription('javascript', params.selector);

View File

@ -26,6 +26,8 @@ import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
import type { Boundaries } from './geometry'; import type { Boundaries } from './geometry';
import { ToolbarButton } from '@web/components/toolbarButton'; import { ToolbarButton } from '@web/components/toolbarButton';
import { testStatusIcon } from './testUtils'; import { testStatusIcon } from './testUtils';
import { methodMetainfo } from '@isomorphic/protocolMetainfo';
import { formatProtocolParam } from '@isomorphic/protocolFormatter';
export interface ActionListProps { export interface ActionListProps {
actions: ActionTraceEventInContext[], actions: ActionTraceEventInContext[],
@ -128,10 +130,10 @@ export const renderAction = (
time = 'Timed out'; time = 'Timed out';
else if (!isLive) else if (!isLive)
time = '-'; time = '-';
const renderedTitle = highlightQuotedText(action.title || action.method); const { elements, title } = renderTitleForCall(action);
return <div className='action-title vbox'> return <div className='action-title vbox'>
<div className='hbox'> <div className='hbox'>
<span className='action-title-method' title={action.title || action.method}>{renderedTitle}</span> <span className='action-title-method' title={title}>{elements}</span>
{(showDuration || showBadges || showAttachments || isSkipped) && <div className='spacer'></div>} {(showDuration || showBadges || showAttachments || isSkipped) && <div className='spacer'></div>}
{showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={() => revealAttachment(action.attachments![0])} />} {showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={() => revealAttachment(action.attachments![0])} />}
{showDuration && !isSkipped && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>} {showDuration && !isSkipped && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>}
@ -145,19 +147,33 @@ export const renderAction = (
</div>; </div>;
}; };
function highlightQuotedText(text: string): React.ReactNode[] { export function renderTitleForCall(action: ActionTraceEvent): { elements: React.ReactNode[], title: string } {
const result: React.ReactNode[] = []; const titleFormat = action.title ?? methodMetainfo.get(action.class + '.' + action.method)?.title ?? action.method;
const elements: React.ReactNode[] = [];
const title: string[] = [];
let currentIndex = 0; let currentIndex = 0;
const regex = /("[^"]*")/g; const regex = /\{([^}]+)\}/g;
let match; let match;
while ((match = regex.exec(text)) !== null) { while ((match = regex.exec(titleFormat)) !== null) {
const [fullMatch, quotedText] = match; const [fullMatch, quotedText] = match;
result.push(text.slice(currentIndex, match.index)); const chunk = titleFormat.slice(currentIndex, match.index);
result.push(<span className='action-title-param'>{quotedText}</span>);
elements.push(chunk);
title.push(chunk);
const param = formatProtocolParam(action.params, quotedText);
elements.push(<span className='action-title-param'>{param}</span>);
title.push(param);
currentIndex = match.index + fullMatch.length; currentIndex = match.index + fullMatch.length;
} }
if (currentIndex < text.length)
result.push(text.slice(currentIndex)); if (currentIndex < titleFormat.length) {
return result; const chunk = titleFormat.slice(currentIndex);
elements.push(chunk);
title.push(chunk);
}
return { elements, title: title.join('') };
} }

View File

@ -24,6 +24,7 @@ import type { ActionTreeItem } from '../../packages/trace-viewer/src/ui/modelUti
import { buildActionTree, MultiTraceModel } from '../../packages/trace-viewer/src/ui/modelUtil'; import { buildActionTree, MultiTraceModel } from '../../packages/trace-viewer/src/ui/modelUtil';
import type { ActionTraceEvent, ConsoleMessageTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace'; import type { ActionTraceEvent, ConsoleMessageTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace';
import style from 'ansi-styles'; import style from 'ansi-styles';
import { renderTitleForCall } from '../../packages/playwright-core/lib/utils/isomorphic/protocolFormatter';
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> { export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
const handle = await page.evaluateHandle(async ({ frameId, url }) => { const handle = await page.evaluateHandle(async ({ frameId, url }) => {
@ -151,7 +152,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
return { return {
events, events,
resources, resources,
actions: actionObjects.map(a => a.title ?? a.class.toLowerCase() + '.' + a.method), actions: actionObjects.map(a => renderTitleForCall({ ...a, type: a.class })),
actionObjects, actionObjects,
stacks, stacks,
}; };
@ -165,13 +166,14 @@ export async function parseTrace(file: string): Promise<{ resources: Map<string,
const { rootItem } = buildActionTree(model.actions); const { rootItem } = buildActionTree(model.actions);
const actionTree: string[] = []; const actionTree: string[] = [];
const visit = (actionItem: ActionTreeItem, indent: string) => { const visit = (actionItem: ActionTreeItem, indent: string) => {
actionTree.push(`${indent}${actionItem.action?.title || actionItem.id}`); const title = renderTitleForCall({ ...actionItem.action, type: actionItem.action.class });
actionTree.push(`${indent}${title || actionItem.id}`);
for (const child of actionItem.children) for (const child of actionItem.children)
visit(child, indent + ' '); visit(child, indent + ' ');
}; };
rootItem.children.forEach(a => visit(a, '')); rootItem.children.forEach(a => visit(a, ''));
return { return {
titles: model.actions.map(a => a.title ?? a.class.toLowerCase() + '.' + a.method), titles: model.actions.map(a => renderTitleForCall({ ...a, type: a.class })),
resources: backend.entries, resources: backend.entries,
actions: model.actions, actions: model.actions,
events: model.events, events: model.events,

View File

@ -271,7 +271,7 @@ test('should render console', async ({ showTraceViewer, browserName }) => {
await expect.soft(traceViewer.consoleLines.filter({ hasText: 'Cheers!' }).locator('.codicon')).toHaveClass('codicon codicon-browser status-none'); await expect.soft(traceViewer.consoleLines.filter({ hasText: 'Cheers!' }).locator('.codicon')).toHaveClass('codicon codicon-browser status-none');
await expect(traceViewer.consoleStacks.first()).toContainText('Error: Unhandled exception'); await expect(traceViewer.consoleStacks.first()).toContainText('Error: Unhandled exception');
await traceViewer.selectAction('EVALUATE'); await traceViewer.selectAction('Evaluate');
const listViews = traceViewer.page.locator('.console-tab').locator('.list-view-entry'); const listViews = traceViewer.page.locator('.console-tab').locator('.list-view-entry');
await expect(listViews.nth(0)).toHaveClass('list-view-entry'); await expect(listViews.nth(0)).toHaveClass('list-view-entry');
@ -284,17 +284,17 @@ test('should render console', async ({ showTraceViewer, browserName }) => {
test('should open console errors on click', async ({ showTraceViewer, browserName }) => { test('should open console errors on click', async ({ showTraceViewer, browserName }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
expect(await traceViewer.actionIconsText('EVALUATE')).toEqual(['2', '1']); expect(await traceViewer.actionIconsText('Evaluate')).toEqual(['2', '1']);
expect(await traceViewer.page.isHidden('.console-tab')).toBeTruthy(); expect(await traceViewer.page.isHidden('.console-tab')).toBeTruthy();
await (await traceViewer.actionIcons('EVALUATE')).click(); await (await traceViewer.actionIcons('Evaluate')).click();
expect(await traceViewer.page.waitForSelector('.console-tab')).toBeTruthy(); expect(await traceViewer.page.waitForSelector('.console-tab')).toBeTruthy();
}); });
test('should show params and return value', async ({ showTraceViewer }) => { test('should show params and return value', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('EVALUATE'); await traceViewer.selectAction('Evaluate');
await expect(traceViewer.callLines).toHaveText([ await expect(traceViewer.callLines).toHaveText([
/Evaluate/, '',
/start:[\d\.]+m?s/, /start:[\d\.]+m?s/,
/duration:[\d]+ms/, /duration:[\d]+ms/,
/expression:"\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/, /expression:"\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/,
@ -318,9 +318,9 @@ test('should show params and return value', async ({ showTraceViewer }) => {
test('should show null as a param', async ({ showTraceViewer, browserName }) => { test('should show null as a param', async ({ showTraceViewer, browserName }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('EVALUATE', 1); await traceViewer.selectAction('Evaluate', 1);
await expect(traceViewer.callLines).toHaveText([ await expect(traceViewer.callLines).toHaveText([
/Evaluate/, '',
/start:[\d\.]+m?s/, /start:[\d\.]+m?s/,
/duration:[\d]+ms/, /duration:[\d]+ms/,
'expression:"() => 1 + 1"', 'expression:"() => 1 + 1"',
@ -354,7 +354,7 @@ test('should have correct stack trace', async ({ showTraceViewer }) => {
test('should have network requests', async ({ showTraceViewer }) => { test('should have network requests', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('NAVIGATE'); await traceViewer.selectAction('Navigate');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]); await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]);
await expect(traceViewer.networkRequests).toContainText([/style.cssGET200text\/css/]); await expect(traceViewer.networkRequests).toContainText([/style.cssGET200text\/css/]);
@ -369,7 +369,7 @@ test('should filter network requests by resource type', async ({ page, runAndTra
await page.goto(`${server.PREFIX}/network-tab/network.html`); await page.goto(`${server.PREFIX}/network-tab/network.html`);
await page.evaluate(() => (window as any).donePromise); await page.evaluate(() => (window as any).donePromise);
}); });
await traceViewer.selectAction('NAVIGATE'); await traceViewer.selectAction('Navigate');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await traceViewer.page.getByText('JS', { exact: true }).click(); await traceViewer.page.getByText('JS', { exact: true }).click();
@ -402,7 +402,7 @@ test('should show font preview', async ({ page, runAndTrace, server }) => {
await page.goto(`${server.PREFIX}/network-tab/network.html`); await page.goto(`${server.PREFIX}/network-tab/network.html`);
await page.evaluate(() => (window as any).donePromise); await page.evaluate(() => (window as any).donePromise);
}); });
await traceViewer.selectAction('NAVIGATE'); await traceViewer.selectAction('Navigate');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await traceViewer.page.getByText('Font', { exact: true }).click(); await traceViewer.page.getByText('Font', { exact: true }).click();
@ -417,7 +417,7 @@ test('should filter network requests by url', async ({ page, runAndTrace, server
await page.goto(`${server.PREFIX}/network-tab/network.html`); await page.goto(`${server.PREFIX}/network-tab/network.html`);
await page.evaluate(() => (window as any).donePromise); await page.evaluate(() => (window as any).donePromise);
}); });
await traceViewer.selectAction('NAVIGATE'); await traceViewer.selectAction('Navigate');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await traceViewer.page.getByPlaceholder('Filter network').fill('script.'); await traceViewer.page.getByPlaceholder('Filter network').fill('script.');
@ -446,7 +446,7 @@ test('should have network request overrides', async ({ page, server, runAndTrace
await page.route('**/style.css', route => route.abort()); await page.route('**/style.css', route => route.abort());
await page.goto(server.PREFIX + '/frames/frame.html'); await page.goto(server.PREFIX + '/frames/frame.html');
}); });
await traceViewer.selectAction('NAVIGATE'); await traceViewer.selectAction('Navigate');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]); await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]);
await expect(traceViewer.networkRequests).toContainText([/style.cssGETx-unknown.*aborted/]); await expect(traceViewer.networkRequests).toContainText([/style.cssGETx-unknown.*aborted/]);
@ -458,7 +458,7 @@ test('should have network request overrides 2', async ({ page, server, runAndTra
await page.route('**/script.js', route => route.continue()); await page.route('**/script.js', route => route.continue());
await page.goto(server.PREFIX + '/frames/frame.html'); await page.goto(server.PREFIX + '/frames/frame.html');
}); });
await traceViewer.selectAction('NAVIGATE'); await traceViewer.selectAction('Navigate');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await expect.soft(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html.*/]); await expect.soft(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html.*/]);
await expect.soft(traceViewer.networkRequests).toContainText([/script.jsGET200application\/javascript.*continued/]); await expect.soft(traceViewer.networkRequests).toContainText([/script.jsGET200application\/javascript.*continued/]);
@ -1506,7 +1506,7 @@ test('should show correct request start time', {
return fetch('/api').then(r => r.text()); return fetch('/api').then(r => r.text());
}); });
}); });
await traceViewer.selectAction('EVALUATE'); await traceViewer.selectAction('Evaluate');
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests).toContainText([/apiGET200text/]); await expect(traceViewer.networkRequests).toContainText([/apiGET200text/]);
const line = traceViewer.networkRequests.getByText(/apiGET200text/); const line = traceViewer.networkRequests.getByText(/apiGET200text/);
@ -1553,7 +1553,7 @@ test('should show baseURL in metadata pane', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31847' }, annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31847' },
}, async ({ showTraceViewer }) => { }, async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('EVALUATE'); await traceViewer.selectAction('Evaluate');
await traceViewer.showMetadataTab(); await traceViewer.showMetadataTab();
await expect(traceViewer.metadataTab).toContainText('baseURL:https://example.com'); await expect(traceViewer.metadataTab).toContainText('baseURL:https://example.com');
}); });
@ -1595,22 +1595,22 @@ test('should not leak recorders', {
await expect(traceViewer.snapshotContainer.contentFrame().locator('body')).toContainText(`Hi, I'm frame`); await expect(traceViewer.snapshotContainer.contentFrame().locator('body')).toContainText(`Hi, I'm frame`);
const frame1 = await forceRecorder('NAVIGATE'); const frame1 = await forceRecorder('Navigate');
await expect(frame1.locator('body')).toContainText('Hello world'); await expect(frame1.locator('body')).toContainText('Hello world');
const frame2 = await forceRecorder('EVALUATE'); const frame2 = await forceRecorder('Evaluate');
await expect(frame2.locator('button')).toBeVisible(); await expect(frame2.locator('button')).toBeVisible();
await traceViewer.page.requestGC(); await traceViewer.page.requestGC();
await expect.poll(() => aliveCount()).toBeLessThanOrEqual(2); // two snapshot iframes await expect.poll(() => aliveCount()).toBeLessThanOrEqual(2); // two snapshot iframes
const frame3 = await forceRecorder('SET VIEWPORT SIZE'); const frame3 = await forceRecorder('Set viewport size');
await expect(frame3.locator('body')).toContainText(`Hi, I'm frame`); await expect(frame3.locator('body')).toContainText(`Hi, I'm frame`);
const frame4 = await forceRecorder('NAVIGATE'); const frame4 = await forceRecorder('Navigate');
await expect(frame4.locator('body')).toContainText('Hello world'); await expect(frame4.locator('body')).toContainText('Hello world');
const frame5 = await forceRecorder('EVALUATE'); const frame5 = await forceRecorder('Evaluate');
await expect(frame5.locator('button')).toBeVisible(); await expect(frame5.locator('button')).toBeVisible();
await traceViewer.page.requestGC(); await traceViewer.page.requestGC();
@ -1751,14 +1751,14 @@ test('should show a modal dialog', async ({ runAndTrace, page, platform, browser
test('should open settings dialog', async ({ showTraceViewer }) => { test('should open settings dialog', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('NAVIGATE'); await traceViewer.selectAction('Navigate');
await traceViewer.showSettings(); await traceViewer.showSettings();
await expect(traceViewer.settingsDialog).toBeVisible(); await expect(traceViewer.settingsDialog).toBeVisible();
}); });
test('should toggle theme color', async ({ showTraceViewer, page }) => { test('should toggle theme color', async ({ showTraceViewer, page }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('NAVIGATE'); await traceViewer.selectAction('Navigate');
await traceViewer.showSettings(); await traceViewer.showSettings();
await expect(traceViewer.darkModeSetting).toBeChecked({ checked: false }); await expect(traceViewer.darkModeSetting).toBeChecked({ checked: false });
@ -1781,7 +1781,7 @@ test('should toggle canvas rendering', async ({ runAndTrace, page }) => {
let snapshotRequestPromise = traceViewer.page.waitForRequest(request => request.url().includes('/snapshot/')); let snapshotRequestPromise = traceViewer.page.waitForRequest(request => request.url().includes('/snapshot/'));
// Click on the action with a canvas snapshot // Click on the action with a canvas snapshot
await traceViewer.selectAction('NAVIGATE', 0); await traceViewer.selectAction('Navigate', 0);
let snapshotRequest = await snapshotRequestPromise; let snapshotRequest = await snapshotRequestPromise;
@ -1794,12 +1794,12 @@ test('should toggle canvas rendering', async ({ runAndTrace, page }) => {
await expect(traceViewer.displayCanvasContentSetting).toBeChecked({ checked: true }); await expect(traceViewer.displayCanvasContentSetting).toBeChecked({ checked: true });
// Deselect canvas // Deselect canvas
await traceViewer.selectAction('NAVIGATE', 1); await traceViewer.selectAction('Navigate', 1);
snapshotRequestPromise = traceViewer.page.waitForRequest(request => request.url().includes('/snapshot/')); snapshotRequestPromise = traceViewer.page.waitForRequest(request => request.url().includes('/snapshot/'));
// Select canvas again // Select canvas again
await traceViewer.selectAction('NAVIGATE', 0); await traceViewer.selectAction('Navigate', 0);
snapshotRequest = await snapshotRequestPromise; snapshotRequest = await snapshotRequestPromise;

View File

@ -134,7 +134,7 @@ test('should not include buffers in the trace', async ({ context, page, server }
await page.screenshot(); await page.screenshot();
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.zip')); const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
const screenshotEvent = actionObjects.find(a => a.title === 'Screenshot'); const screenshotEvent = actionObjects.find(a => a.method === 'screenshot');
expect(screenshotEvent.beforeSnapshot).toBeTruthy(); expect(screenshotEvent.beforeSnapshot).toBeTruthy();
expect(screenshotEvent.afterSnapshot).toBeTruthy(); expect(screenshotEvent.afterSnapshot).toBeTruthy();
expect(screenshotEvent.result).toEqual({ expect(screenshotEvent.result).toEqual({
@ -160,13 +160,12 @@ test('should exclude internal pages', async ({ browserName, context, page, serve
expect(pageIds.size).toBe(1); expect(pageIds.size).toBe(1);
}); });
test('should include context API requests', async ({ browserName, context, page, server }, testInfo) => { test('should include context API requests', async ({ context, page, server }, testInfo) => {
await context.tracing.start({ snapshots: true }); await context.tracing.start({ snapshots: true });
await page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } }); await page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } });
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip')); const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
const postEvent = events.find(e => e.title === 'Fetch "/simple.json"'); expect(actions).toContain('Fetch "/simple.json"');
expect(postEvent).toBeTruthy();
const harEntry = events.find(e => e.type === 'resource-snapshot'); const harEntry = events.find(e => e.type === 'resource-snapshot');
expect(harEntry).toBeTruthy(); expect(harEntry).toBeTruthy();
expect(harEntry.snapshot.request.url).toBe(server.PREFIX + '/simple.json'); expect(harEntry.snapshot.request.url).toBe(server.PREFIX + '/simple.json');
@ -482,9 +481,8 @@ test('should include interrupted actions', async ({ context, page, server }, tes
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
await context.close(); await context.close();
const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip')); const { actions } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
const clickEvent = events.find(e => e.title === 'Click'); expect(actions).toContain('Click');
expect(clickEvent).toBeTruthy();
}); });
test('should throw when starting with different options', async ({ context }) => { test('should throw when starting with different options', async ({ context }) => {
@ -741,7 +739,6 @@ test('should not flush console events', async ({ context, page, mode }, testInfo
await expect(async () => { await expect(async () => {
const traceName = fs.readdirSync(dir).find(name => name.endsWith(testId + '.trace')); const traceName = fs.readdirSync(dir).find(name => name.endsWith(testId + '.trace'));
content = await fs.promises.readFile(path.join(dir, traceName), 'utf8'); content = await fs.promises.readFile(path.join(dir, traceName), 'utf8');
expect(content).toContain('Evaluate');
expect(content).toContain('31415926'); expect(content).toContain('31415926');
}).toPass(); }).toPass();
expect(content).not.toContain('hello 0'); expect(content).not.toContain('hello 0');
@ -823,17 +820,14 @@ test('should not emit after w/o before', async ({ browserType, mode }, testInfo)
{ {
type: 'before', type: 'before',
callId: expect.any(Number), callId: expect.any(Number),
title: 'Evaluate'
}, },
{ {
type: 'before', type: 'before',
callId: expect.any(Number), callId: expect.any(Number),
title: 'Wait for event "console"'
}, },
{ {
type: 'after', type: 'after',
callId: expect.any(Number), callId: expect.any(Number),
title: undefined,
}, },
]); ]);
call1 = sanitized[0].callId; call1 = sanitized[0].callId;
@ -849,12 +843,10 @@ test('should not emit after w/o before', async ({ browserType, mode }, testInfo)
{ {
type: 'before', type: 'before',
callId: expect.any(Number), callId: expect.any(Number),
title: 'Evaluate'
}, },
{ {
type: 'after', type: 'after',
callId: expect.any(Number), callId: expect.any(Number),
title: undefined
} }
]); ]);
call2before = sanitized[0].callId; call2before = sanitized[0].callId;

View File

@ -1346,7 +1346,7 @@ test('should record trace snapshot for more obscure commands', async ({ runInlin
const snapshots = trace.traceModel.storage(); const snapshots = trace.traceModel.storage();
const snapshotFrameOrPageId = snapshots.snapshotsForTest()[0]; const snapshotFrameOrPageId = snapshots.snapshotsForTest()[0];
const countAction = trace.actions.find(a => a.title === 'Query count'); const countAction = trace.actions.find(a => a.method === 'queryCount');
expect(countAction.beforeSnapshot).toBeTruthy(); expect(countAction.beforeSnapshot).toBeTruthy();
expect(countAction.afterSnapshot).toBeTruthy(); expect(countAction.afterSnapshot).toBeTruthy();
expect(snapshots.snapshotByName(snapshotFrameOrPageId, countAction.beforeSnapshot)).toBeTruthy(); expect(snapshots.snapshotByName(snapshotFrameOrPageId, countAction.beforeSnapshot)).toBeTruthy();

View File

@ -157,7 +157,7 @@ export type { Validator, ValidatorContext } from './validatorPrimitives';
export { ValidationError, findValidator, maybeFindValidator, createMetadataValidator } from './validatorPrimitives'; export { ValidationError, findValidator, maybeFindValidator, createMetadataValidator } from './validatorPrimitives';
`]; `];
const debug_ts = [ const metainfo_ts = [
`/** `/**
* Copyright (c) Microsoft Corporation. * Copyright (c) Microsoft Corporation.
* *
@ -328,7 +328,7 @@ for (const [name, item] of Object.entries(protocol)) {
} }
} }
debug_ts.push(`export const methodMetainfo = new Map<string, { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pausesBeforeInput?: boolean }>([ metainfo_ts.push(`export const methodMetainfo = new Map<string, { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pausesBeforeInput?: boolean }>([
${methodMetainfo.join(`,\n `)} ${methodMetainfo.join(`,\n `)}
]);`); ]);`);
@ -348,6 +348,6 @@ function writeFile(filePath, content) {
} }
writeFile(path.join(__dirname, '..', 'packages', 'protocol', 'src', 'channels.d.ts'), channels_ts.join('\n') + '\n'); writeFile(path.join(__dirname, '..', 'packages', 'protocol', 'src', 'channels.d.ts'), channels_ts.join('\n') + '\n');
writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'debug.ts'), debug_ts.join('\n') + '\n'); writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'utils', 'isomorphic', 'protocolMetainfo.ts'), metainfo_ts.join('\n') + '\n');
writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'validator.ts'), validator_ts.join('\n') + '\n'); writeFile(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'validator.ts'), validator_ts.join('\n') + '\n');
process.exit(hasChanges ? 1 : 0); process.exit(hasChanges ? 1 : 0);