Repository: microsoft/playwright
Pull request #36058: chore: two-line trace view (3)
Author: login: pavelfeldman
Created: 2025-05-22T22:52:35Z | Merged: 2025-05-23T02:00:33Z



Diff:
diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts
index 03bde8c975bde..e731143a9a27e 100644
--- a/packages/playwright-core/src/client/channelOwner.ts
+++ b/packages/playwright-core/src/client/channelOwner.ts
@@ -16,7 +16,7 @@
 
 import { EventEmitter } from './eventEmitter';
 import { ValidationError, maybeFindValidator  } from '../protocol/validator';
-import { methodMetainfo } from '../protocol/debug';
+import { methodMetainfo } from '../utils/isomorphic/protocolMetainfo';
 import { captureLibraryStackTrace } from './clientStackTrace';
 import { stringifyStackFrames } from '../utils/isomorphic/stackTrace';
 
diff --git a/packages/playwright-core/src/server/debugger.ts b/packages/playwright-core/src/server/debugger.ts
index 1bec695569fa5..2d158e859e845 100644
--- a/packages/playwright-core/src/server/debugger.ts
+++ b/packages/playwright-core/src/server/debugger.ts
@@ -18,7 +18,7 @@ import { EventEmitter } from 'events';
 
 import { debugMode, isUnderTest, monotonicTime } from '../utils';
 import { BrowserContext } from './browserContext';
-import { methodMetainfo } from '../protocol/debug';
+import { methodMetainfo } from '../utils/isomorphic/protocolMetainfo';
 
 import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
 
diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
index 565fd419bdf65..b73c07bae1ce5 100644
--- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
@@ -18,13 +18,12 @@ import { EventEmitter } from 'events';
 
 import { eventsHelper } from '../utils/eventsHelper';
 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 { TargetClosedError, isTargetClosedError, serializeError } from '../errors';
 import { SdkObject } from '../instrumentation';
 import { isProtocolError } from '../protocolError';
 import { compressCallLog } from '../callLog';
-import { methodMetainfo } from '../../protocol/debug';
 
 import type { CallMetadata } from '../instrumentation';
 import type { PlaywrightDispatcher } from './playwrightDispatcher';
@@ -309,7 +308,7 @@ export class DispatcherConnection {
     const callMetadata: CallMetadata = {
       id: `call@${id}`,
       location: validMetadata.location,
-      title: renderTitle(dispatcher._type, method, params, validMetadata.title),
+      title: validMetadata.title,
       internal: validMetadata.internal,
       stepId: validMetadata.stepId,
       objectId: sdkObject?.guid,
@@ -389,10 +388,3 @@ function closeReason(sdkObject: SdkObject): string | undefined {
     sdkObject.attribution.context?._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);
-  });
-}
diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts
index 6d4fc32a71d97..2150e6c6f5718 100644
--- a/packages/playwright-core/src/server/frames.ts
+++ b/packages/playwright-core/src/server/frames.ts
@@ -27,7 +27,7 @@ import * as network from './network';
 import { Page } from './page';
 import { ProgressController } from './progress';
 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 { debugLogger } from './utils/debugLogger';
 import { eventsHelper } from './utils/eventsHelper';
@@ -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.title}${timeout ? ` with timeout ${timeout}ms` : ''}`);
+        progress.log(`${renderTitleForCall(metadata)}${timeout ? ` with timeout ${timeout}ms` : ''}`);
         progress.log(`waiting for ${this._asLocator(selector)}`);
         await this._page.performActionPreChecks(progress);
       }, timeout);
diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts
index ae5449bbe65e6..48ad2b739a277 100644
--- a/packages/playwright-core/src/server/page.ts
+++ b/packages/playwright-core/src/server/page.ts
@@ -27,7 +27,7 @@ import { SdkObject } from './instrumentation';
 import * as js from './javascript';
 import { ProgressController } from './progress';
 import { Screenshotter, validateScreenshotOptions } from './screenshotter';
-import { LongStandingScope, assert, trimStringWithEllipsis } from '../utils';
+import { LongStandingScope, assert, renderTitleForCall, trimStringWithEllipsis } from '../utils';
 import { asLocator } from '../utils';
 import { getComparator } from './utils/comparators';
 import { debugLogger } from './utils/debugLogger';
@@ -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.title}${callTimeout ? ` with timeout ${callTimeout}ms` : ''}`);
+      progress.log(`${renderTitleForCall(metadata)}${callTimeout ? ` with timeout ${callTimeout}ms` : ''}`);
       if (options.expected)
         progress.log(`  verifying given screenshot expectation`);
       else
diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts
index 39b13fb2bfd30..1487ae1c39a79 100644
--- a/packages/playwright-core/src/server/recorder/recorderUtils.ts
+++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import { renderTitleForCall } from '../../utils/isomorphic/protocolFormatter';
+
 import type { Frame } from '../frames';
 import type { CallMetadata } from '../instrumentation';
 import type { Page } from '../page';
@@ -25,7 +27,7 @@ export function buildFullSelector(framePath: string[], selector: string) {
 }
 
 export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
-  const title = metadata.title;
+  const title = renderTitleForCall(metadata);
   if (metadata.error)
     status = 'error';
   const params = {
diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts
index 1b77156e47986..b95afc594a3ea 100644
--- a/packages/playwright-core/src/server/trace/recorder/tracing.ts
+++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts
@@ -19,7 +19,7 @@ import os from 'os';
 import path from 'path';
 
 import { Snapshotter } from './snapshotter';
-import { methodMetainfo } from '../../../protocol/debug';
+import { methodMetainfo } from '../../../utils/isomorphic/protocolMetainfo';
 import { assert } from '../../../utils/isomorphic/assert';
 import { monotonicTime } from '../../../utils/isomorphic/time';
 import { eventsHelper  } from '../../utils/eventsHelper';
diff --git a/packages/playwright-core/src/utils.ts b/packages/playwright-core/src/utils.ts
index 090d50d42b3c6..4037c44c14c2c 100644
--- a/packages/playwright-core/src/utils.ts
+++ b/packages/playwright-core/src/utils.ts
@@ -21,6 +21,8 @@ export * from './utils/isomorphic/locatorGenerators';
 export * from './utils/isomorphic/manualPromise';
 export * from './utils/isomorphic/mimeType';
 export * from './utils/isomorphic/multimap';
+export * from './utils/isomorphic/protocolFormatter';
+export * from './utils/isomorphic/protocolMetainfo';
 export * from './utils/isomorphic/rtti';
 export * from './utils/isomorphic/semaphore';
 export * from './utils/isomorphic/stackTrace';
@@ -52,7 +54,4 @@ 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';
diff --git a/packages/playwright-core/src/protocol/formatter.ts b/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts
similarity index 73%
rename from packages/playwright-core/src/protocol/formatter.ts
rename to packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts
index 6d273cf5a6593..80045bb2e2f54 100644
--- a/packages/playwright-core/src/protocol/formatter.ts
+++ b/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import { methodMetainfo } from './protocolMetainfo';
+
 export function formatProtocolParam(params: Record<string, string> | undefined, name: string): string {
   if (!params)
     return '';
@@ -29,8 +31,10 @@ export function formatProtocolParam(params: Record<string, string> | undefined,
       return params[name];
     }
   }
-  if (name === 'timeNumber')
+  if (name === 'timeNumber') {
+    // eslint-disable-next-line no-restricted-globals
     return new Date(params[name]).toString();
+  }
   return deepParam(params, name);
 }
 
@@ -46,3 +50,10 @@ function deepParam(params: Record<string, any>, name: string): string {
     return '';
   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);
+  });
+}
diff --git a/packages/playwright-core/src/protocol/debug.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts
similarity index 100%
rename from packages/playwright-core/src/protocol/debug.ts
rename to packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts
diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts
index b1d907acbd67d..bea4d31157495 100644
--- a/packages/playwright/src/index.ts
+++ b/packages/playwright/src/index.ts
@@ -18,7 +18,7 @@ import fs from 'fs';
 import path from 'path';
 
 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 { rootTestType } from './common/testType';
@@ -758,8 +758,7 @@ class ArtifactsRecorder {
 }
 
 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));
+  const prefix = renderTitleForCall({ title, type, method, params });
   let selector;
   if (params?.['selector'])
     selector = asLocatorDescription('javascript', params.selector);
diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx
index 5a08951a47bc9..6e86410e24d33 100644
--- a/packages/trace-viewer/src/ui/actionList.tsx
+++ b/packages/trace-viewer/src/ui/actionList.tsx
@@ -26,6 +26,8 @@ import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
 import type { Boundaries } from './geometry';
 import { ToolbarButton } from '@web/components/toolbarButton';
 import { testStatusIcon } from './testUtils';
+import { methodMetainfo } from '@isomorphic/protocolMetainfo';
+import { formatProtocolParam } from '@isomorphic/protocolFormatter';
 
 export interface ActionListProps {
   actions: ActionTraceEventInContext[],
@@ -128,10 +130,10 @@ export const renderAction = (
     time = 'Timed out';
   else if (!isLive)
     time = '-';
-  const renderedTitle = highlightQuotedText(action.title || action.method);
+  const { elements, title } = renderTitleForCall(action);
   return <div className='action-title vbox'>
     <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>}
       {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>}
@@ -145,19 +147,33 @@ export const renderAction = (
   </div>;
 };
 
-function highlightQuotedText(text: string): React.ReactNode[] {
-  const result: React.ReactNode[] = [];
+export function renderTitleForCall(action: ActionTraceEvent): { elements: React.ReactNode[], title: string } {
+  const titleFormat = action.title ?? methodMetainfo.get(action.class + '.' + action.method)?.title ?? action.method;
+
+  const elements: React.ReactNode[] = [];
+  const title: string[] = [];
   let currentIndex = 0;
-  const regex = /("[^"]*")/g;
+  const regex = /\{([^}]+)\}/g;
   let match;
 
-  while ((match = regex.exec(text)) !== null) {
+  while ((match = regex.exec(titleFormat)) !== null) {
     const [fullMatch, quotedText] = match;
-    result.push(text.slice(currentIndex, match.index));
-    result.push(<span className='action-title-param'>{quotedText}</span>);
+    const chunk = titleFormat.slice(currentIndex, match.index);
+
+    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;
   }
-  if (currentIndex < text.length)
-    result.push(text.slice(currentIndex));
-  return result;
+
+  if (currentIndex < titleFormat.length) {
+    const chunk = titleFormat.slice(currentIndex);
+    elements.push(chunk);
+    title.push(chunk);
+  }
+
+  return { elements, title: title.join('') };
 }
diff --git a/tests/config/utils.ts b/tests/config/utils.ts
index b3d3792428a51..293fbb81a8ca1 100644
--- a/tests/config/utils.ts
+++ b/tests/config/utils.ts
@@ -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 type { ActionTraceEvent, ConsoleMessageTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace';
 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> {
   const handle = await page.evaluateHandle(async ({ frameId, url }) => {
@@ -151,7 +152,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
   return {
     events,
     resources,
-    actions: actionObjects.map(a => a.title ?? a.class.toLowerCase() + '.' + a.method),
+    actions: actionObjects.map(a => renderTitleForCall({ ...a, type: a.class })),
     actionObjects,
     stacks,
   };
@@ -165,13 +166,14 @@ export async function parseTrace(file: string): Promise<{ resources: Map<string,
   const { rootItem } = buildActionTree(model.actions);
   const actionTree: 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)
       visit(child, indent + '  ');
   };
   rootItem.children.forEach(a => visit(a, ''));
   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,
     actions: model.actions,
     events: model.events,
diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts
index d1abdb0332715..87f7681cb0e64 100644
--- a/tests/library/trace-viewer.spec.ts
+++ b/tests/library/trace-viewer.spec.ts
@@ -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(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');
   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 }) => {
   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();
-  await (await traceViewer.actionIcons('EVALUATE')).click();
+  await (await traceViewer.actionIcons('Evaluate')).click();
   expect(await traceViewer.page.waitForSelector('.console-tab')).toBeTruthy();
 });
 
 test('should show params and return value', async ({ showTraceViewer }) => {
   const traceViewer = await showTraceViewer([traceFile]);
-  await traceViewer.selectAction('EVALUATE');
+  await traceViewer.selectAction('Evaluate');
   await expect(traceViewer.callLines).toHaveText([
-    /Evaluate/,
+    '',
     /start:[\d\.]+m?s/,
     /duration:[\d]+ms/,
     /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 }) => {
   const traceViewer = await showTraceViewer([traceFile]);
-  await traceViewer.selectAction('EVALUATE', 1);
+  await traceViewer.selectAction('Evaluate', 1);
   await expect(traceViewer.callLines).toHaveText([
-    /Evaluate/,
+    '',
     /start:[\d\.]+m?s/,
     /duration:[\d]+ms/,
     'expression:"() => 1 + 1"',
@@ -354,7 +354,7 @@ test('should have correct stack trace', async ({ showTraceViewer }) => {
 
 test('should have network requests', async ({ showTraceViewer }) => {
   const traceViewer = await showTraceViewer([traceFile]);
-  await traceViewer.selectAction('NAVIGATE');
+  await traceViewer.selectAction('Navigate');
   await traceViewer.showNetworkTab();
   await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]);
   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.evaluate(() => (window as any).donePromise);
   });
-  await traceViewer.selectAction('NAVIGATE');
+  await traceViewer.selectAction('Navigate');
   await traceViewer.showNetworkTab();
 
   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.evaluate(() => (window as any).donePromise);
   });
-  await traceViewer.selectAction('NAVIGATE');
+  await traceViewer.selectAction('Navigate');
   await traceViewer.showNetworkTab();
 
   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.evaluate(() => (window as any).donePromise);
   });
-  await traceViewer.selectAction('NAVIGATE');
+  await traceViewer.selectAction('Navigate');
   await traceViewer.showNetworkTab();
 
   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.goto(server.PREFIX + '/frames/frame.html');
   });
-  await traceViewer.selectAction('NAVIGATE');
+  await traceViewer.selectAction('Navigate');
   await traceViewer.showNetworkTab();
   await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]);
   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.goto(server.PREFIX + '/frames/frame.html');
   });
-  await traceViewer.selectAction('NAVIGATE');
+  await traceViewer.selectAction('Navigate');
   await traceViewer.showNetworkTab();
   await expect.soft(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html.*/]);
   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());
     });
   });
-  await traceViewer.selectAction('EVALUATE');
+  await traceViewer.selectAction('Evaluate');
   await traceViewer.showNetworkTab();
   await expect(traceViewer.networkRequests).toContainText([/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' },
 }, async ({ showTraceViewer }) => {
   const traceViewer = await showTraceViewer([traceFile]);
-  await traceViewer.selectAction('EVALUATE');
+  await traceViewer.selectAction('Evaluate');
   await traceViewer.showMetadataTab();
   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`);
 
-  const frame1 = await forceRecorder('NAVIGATE');
+  const frame1 = await forceRecorder('Navigate');
   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 traceViewer.page.requestGC();
   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`);
 
-  const frame4 = await forceRecorder('NAVIGATE');
+  const frame4 = await forceRecorder('Navigate');
   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 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 }) => {
   const traceViewer = await showTraceViewer([traceFile]);
-  await traceViewer.selectAction('NAVIGATE');
+  await traceViewer.selectAction('Navigate');
   await traceViewer.showSettings();
   await expect(traceViewer.settingsDialog).toBeVisible();
 });
 
 test('should toggle theme color', async ({ showTraceViewer, page }) => {
   const traceViewer = await showTraceViewer([traceFile]);
-  await traceViewer.selectAction('NAVIGATE');
+  await traceViewer.selectAction('Navigate');
   await traceViewer.showSettings();
 
   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/'));
 
   // Click on the action with a canvas snapshot
-  await traceViewer.selectAction('NAVIGATE', 0);
+  await traceViewer.selectAction('Navigate', 0);
 
   let snapshotRequest = await snapshotRequestPromise;
 
@@ -1794,12 +1794,12 @@ test('should toggle canvas rendering', async ({ runAndTrace, page }) => {
   await expect(traceViewer.displayCanvasContentSetting).toBeChecked({ checked: true });
 
   // Deselect canvas
-  await traceViewer.selectAction('NAVIGATE', 1);
+  await traceViewer.selectAction('Navigate', 1);
 
   snapshotRequestPromise = traceViewer.page.waitForRequest(request => request.url().includes('/snapshot/'));
 
   // Select canvas again
-  await traceViewer.selectAction('NAVIGATE', 0);
+  await traceViewer.selectAction('Navigate', 0);
 
   snapshotRequest = await snapshotRequestPromise;
 
diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts
index 2749876a4de98..1a88370e38338 100644
--- a/tests/library/tracing.spec.ts
+++ b/tests/library/tracing.spec.ts
@@ -134,7 +134,7 @@ test('should not include buffers in the trace', async ({ context, page, server }
   await page.screenshot();
   await context.tracing.stop({ path: 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.afterSnapshot).toBeTruthy();
   expect(screenshotEvent.result).toEqual({
@@ -160,13 +160,12 @@ test('should exclude internal pages', async ({ browserName, context, page, serve
   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 page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } });
   await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
-  const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
-  const postEvent = events.find(e => e.title === 'Fetch "/simple.json"');
-  expect(postEvent).toBeTruthy();
+  const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
+  expect(actions).toContain('Fetch "/simple.json"');
   const harEntry = events.find(e => e.type === 'resource-snapshot');
   expect(harEntry).toBeTruthy();
   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.close();
 
-  const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
-  const clickEvent = events.find(e => e.title === 'Click');
-  expect(clickEvent).toBeTruthy();
+  const { actions } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
+  expect(actions).toContain('Click');
 });
 
 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 () => {
     const traceName = fs.readdirSync(dir).find(name => name.endsWith(testId + '.trace'));
     content = await fs.promises.readFile(path.join(dir, traceName), 'utf8');
-    expect(content).toContain('Evaluate');
     expect(content).toContain('31415926');
   }).toPass();
   expect(content).not.toContain('hello 0');
@@ -823,17 +820,14 @@ test('should not emit after w/o before', async ({ browserType, mode }, testInfo)
       {
         type: 'before',
         callId: expect.any(Number),
-        title: 'Evaluate'
       },
       {
         type: 'before',
         callId: expect.any(Number),
-        title: 'Wait for event "console"'
       },
       {
         type: 'after',
         callId: expect.any(Number),
-        title: undefined,
       },
     ]);
     call1 = sanitized[0].callId;
@@ -849,12 +843,10 @@ test('should not emit after w/o before', async ({ browserType, mode }, testInfo)
       {
         type: 'before',
         callId: expect.any(Number),
-        title: 'Evaluate'
       },
       {
         type: 'after',
         callId: expect.any(Number),
-        title: undefined
       }
     ]);
     call2before = sanitized[0].callId;
diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts
index 159d3cc50ed8e..256ce335d4c7d 100644
--- a/tests/playwright-test/playwright.trace.spec.ts
+++ b/tests/playwright-test/playwright.trace.spec.ts
@@ -1346,7 +1346,7 @@ test('should record trace snapshot for more obscure commands', async ({ runInlin
   const snapshots = trace.traceModel.storage();
   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.afterSnapshot).toBeTruthy();
   expect(snapshots.snapshotByName(snapshotFrameOrPageId, countAction.beforeSnapshot)).toBeTruthy();
diff --git a/utils/generate_channels.js b/utils/generate_channels.js
index 8746941be56d9..3389f16275f06 100755
--- a/utils/generate_channels.js
+++ b/utils/generate_channels.js
@@ -157,7 +157,7 @@ export type { Validator, ValidatorContext } from './validatorPrimitives';
 export { ValidationError, findValidator, maybeFindValidator, createMetadataValidator } from './validatorPrimitives';
 `];
 
-const debug_ts = [
+const metainfo_ts = [
 `/**
  * 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  `)}
 ]);`);
 
@@ -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', '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');
 process.exit(hasChanges ? 1 : 0);


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR generates a new `metainfo.ts` for protocol methods and centralizes call‐title rendering via a new `renderTitleForCall` helper, updating usages across the codebase and adjusting tests/UI to match.

- Added generation of `packages/protocol/src/metainfo.ts` 
- Replaced inline `formatProtocolParam` logic with `renderTitleForCall` in core modules
- Updated tests and the trace viewer UI to use new method‐based titles

### Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                               | Description                                           |
| -------------------------------------------------- | ----------------------------------------------------- |
| utils/generate_channels.js                         | Invoke `writeFile` to emit the new `metainfo.ts`      |
| tests/playwright-test/playwright.trace.spec.ts     | Tests updated to use `method` instead of `title`      |
| tests/library/tracing.spec.ts                      | Adjusted parsing helpers and assertions for actions   |
| tests/library/trace-viewer.spec.ts                 | Updated all uppercase action selectors to proper case |
| tests/config/utils.ts                              | Switched `actions` generation to use `renderTitleForCall` |
| packages/trace-viewer/src/ui/actionList.tsx        | UI now uses `renderTitleForCall` over inline parsing |
| packages/protocol/src/metainfo.ts                  | Generated map of method metadata and titles           |
| packages/playwright/src/index.ts                   | Replaced protocol‐param formatting with `renderTitleForCall` |
| packages/playwright-core/src/server/recorder/recorderUtils.ts | Use `renderTitleForCall` for call logs          |
| packages/playwright-core/src/server/page.ts        | Log messages now use `renderTitleForCall`             |
| packages/playwright-core/src/server/frames.ts      | Frame progress logs updated to `renderTitleForCall`   |
| packages/playwright-core/src/server/dispatchers/dispatcher.ts | Removed redundant formatting helper               |
| packages/playwright-core/src/protocol/formatter.ts | Exposed `renderTitleForCall` helper                  |
</details>



<details>
<summary>Comments suppressed due to low confidence (3)</summary>

**tests/library/tracing.spec.ts:741**
* [nitpick] The test assertion for 'Evaluate' was removed, reducing coverage for formatted trace output; consider reintroducing a check to ensure the Evaluate title appears as expected.
```
expect(content).toContain('Evaluate');
```
**tests/library/tracing.spec.ts:823**
* [nitpick] Removed expectations on action titles in the sanitized call metadata; this may miss regressions in title formatting—add assertions back for title handling.
```
title: 'Evaluate'
```
**packages/trace-viewer/src/ui/actionList.tsx:132**
* renderTitleForCall is used here but not imported; add `import { renderTitleForCall } from '…/protocol/formatter';` (or the correct module) at the top.
```
const { elements, title } = renderTitleForCall(action);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T23:07:53Z
user: login: yury-s
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T23:11:26Z
user: login: pavelfeldman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:42:48Z
user: login: yury-s
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T00:05:33Z

Review comments:
diff_hunk: @@ -145,19 +146,66 @@ export const renderAction = (
   </div>;
 };
 
-function highlightQuotedText(text: string): React.ReactNode[] {
-  const result: React.ReactNode[] = [];
+function formatProtocolParam(params: Record<string, string> | undefined, name: string): string {
path: packages/trace-viewer/src/ui/actionList.tsx
user: login: yury-s
body: Let's reuse the one in formatter.ts?
created_at: 2025-05-22T23:09:11Z
author_association: MEMBER
diff_hunk: @@ -145,19 +146,66 @@ export const renderAction = (
   </div>;
 };
 
-function highlightQuotedText(text: string): React.ReactNode[] {
-  const result: React.ReactNode[] = [];
+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 {
path: packages/trace-viewer/src/ui/actionList.tsx
user: login: yury-s
body: ditto
created_at: 2025-05-22T23:10:06Z
author_association: MEMBER
diff_hunk: @@ -145,19 +146,66 @@ export const renderAction = (
   </div>;
 };
 
-function highlightQuotedText(text: string): React.ReactNode[] {
-  const result: React.ReactNode[] = [];
+function formatProtocolParam(params: Record<string, string> | undefined, name: string): string {
path: packages/trace-viewer/src/ui/actionList.tsx
user: login: pavelfeldman
body: I can try making it isomorphic. Non-trivial due to the dependency on metainfo. Let me think about it.

created_at: 2025-05-22T23:42:47Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-22T23:42:12Z
body: <!-- Generated by Playwright markdown reporter for tests 1 in job merge-reports -->
### [Test results](https://mspwblobreport.z1.web.core.windows.net/run-15198351644-1-3a8592910fba1a7786f1660c34eb4c7662710093/index.html) for "tests 1"
**3 failed**
:x: [default] › run-tests.spec.ts:1305:5 › should provide page snapshot to copilot @vscode-extension
:x: [default-reuse] › run-tests.spec.ts:1305:5 › should provide page snapshot to copilot @vscode-extension
:x: [default-trace] › run-tests.spec.ts:1305:5 › should provide page snapshot to copilot @vscode-extension

<details>
<summary><b>3 flaky</b></summary>
:warning: [firefox-library] › library/inspector/cli-codegen-1.spec.ts:986:7 › cli codegen › should not throw csp directive violation errors @firefox-ubuntu-22.04-node18 <br/>
:warning: [webkit-library] › library/ignorehttpserrors.spec.ts:30:3 › should isolate contexts @webkit-ubuntu-22.04-node18 <br/>
:warning: [playwright-test] › ui-mode-test-watch.spec.ts:145:5 › should watch all @windows-latest-node18-1 <br/>

</details>

**39213 passed, 804 skipped**
:heavy_check_mark::heavy_check_mark::heavy_check_mark:


Merge [workflow run](https://github.com/microsoft/playwright/actions/runs/15198945969).
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-23T00:50:16Z
body: <!-- Generated by Playwright markdown reporter for tests 1 in job merge-reports -->
### [Test results](https://mspwblobreport.z1.web.core.windows.net/run-15199160387-1-3a8592910fba1a7786f1660c34eb4c7662710093/index.html) for "tests 1"
**3 failed**
:x: [default] › run-tests.spec.ts:1305:5 › should provide page snapshot to copilot @vscode-extension
:x: [default-reuse] › run-tests.spec.ts:1305:5 › should provide page snapshot to copilot @vscode-extension
:x: [default-trace] › run-tests.spec.ts:1305:5 › should provide page snapshot to copilot @vscode-extension

<details>
<summary><b>4 flaky</b></summary>
:warning: [firefox-library] › library/inspector/cli-codegen-1.spec.ts:986:7 › cli codegen › should not throw csp directive violation errors @firefox-ubuntu-22.04-node18 <br/>
:warning: [webkit-library] › library/ignorehttpserrors.spec.ts:30:3 › should isolate contexts @webkit-ubuntu-22.04-node18 <br/>
:warning: [webkit-library] › library/screenshot.spec.ts:205:14 › element screenshot › element screenshot should work with a mobile viewport @webkit-ubuntu-22.04-node18 <br/>
:warning: [playwright-test] › ui-mode-test-watch.spec.ts:145:5 › should watch all @windows-latest-node18-1 <br/>

</details>

**39212 passed, 804 skipped**
:heavy_check_mark::heavy_check_mark::heavy_check_mark:


Merge [workflow run](https://github.com/microsoft/playwright/actions/runs/15199776732).
author_association: CONTRIBUTOR