{
@@ -61,10 +44,8 @@ export const CallLogView: React.FC = ({
newOverrides.set(callLog.id, !isExpanded);
setExpandOverrides(newOverrides);
}}>
- { titlePrefix }
- { callLog.params.url ? {callLog.params.url} : undefined }
- { locator ? {`page.${locator}`} : undefined }
- { titleSuffix }
+ { callLog.subtitle ? callLog.title + ' ' : callLog.title }
+ { callLog.subtitle ? {callLog.subtitle} : undefined }
{ typeof callLog.duration === 'number' ? — {msToString(callLog.duration)} : undefined}
diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx
index 548649d65bb4b..b4154039fdc84 100644
--- a/packages/recorder/src/recorder.tsx
+++ b/packages/recorder/src/recorder.tsx
@@ -256,7 +256,7 @@ export const Recorder: React.FC = ({}) => {
{
id: 'log',
title: 'Log',
- render: () =>
+ render: () =>
},
{
id: 'aria',
diff --git a/packages/recorder/src/recorderTypes.d.ts b/packages/recorder/src/recorderTypes.d.ts
index 69dea99fb7985..920a0baac0e9f 100644
--- a/packages/recorder/src/recorderTypes.d.ts
+++ b/packages/recorder/src/recorderTypes.d.ts
@@ -64,15 +64,12 @@ export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused';
export type CallLog = {
id: string;
title: string;
+ subtitle?: string;
messages: string[];
status: CallLogStatus;
error?: string;
reveal?: boolean;
duration?: number;
- params: {
- url?: string;
- selector?: string;
- };
};
export type SourceHighlight = {
diff --git a/packages/trace-viewer/src/ui/actionList.css b/packages/trace-viewer/src/ui/actionList.css
index f600b25cceffb..7161559c62af6 100644
--- a/packages/trace-viewer/src/ui/actionList.css
+++ b/packages/trace-viewer/src/ui/actionList.css
@@ -24,7 +24,7 @@
text-overflow: ellipsis;
}
-.action-title-selector {
+.action-title-subtitle {
text-overflow: ellipsis;
overflow: hidden;
color: var(--vscode-tab-inactiveForeground);
diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx
index bf6e2ff314ca1..17ed0cb07ee37 100644
--- a/packages/trace-viewer/src/ui/actionList.tsx
+++ b/packages/trace-viewer/src/ui/actionList.tsx
@@ -20,7 +20,7 @@ import { msToString } from '@isomorphic/formatUtils';
import * as React from 'react';
import './actionList.css';
import { buildActionTree } from '@isomorphic/trace/traceModel';
-import { asLocatorDescription, type Language } from '@isomorphic/locatorGenerators';
+import { type Language } from '@isomorphic/locatorGenerators';
import type { TreeState } from '@web/components/treeView';
import { TreeView } from '@web/components/treeView';
import type { ActionTreeItem, TraceModel } from '@isomorphic/trace/traceModel';
@@ -30,7 +30,7 @@ import type { Boundaries } from './geometry';
import { ToolbarButton } from '@web/components/toolbarButton';
import { testStatusIcon } from './testUtils';
import { getMetainfo } from '@isomorphic/protocolMetainfo';
-import { formatProtocolParam } from '@isomorphic/protocolFormatter';
+import { formatProtocolParam, renderSubtitleForCall } from '@isomorphic/protocolFormatter';
export interface ActionListProps {
actions: ActionEntry[],
@@ -90,12 +90,13 @@ export const ActionList: React.FC
= ({
const timeVisible = !selectedTime || !item.action || (item.action.startTime <= selectedTime.maximum && item.action.endTime >= selectedTime.minimum);
if (!timeVisible)
return false;
- const title = renderTitleForCall(item.action).title;
if (!actionFilterText)
return true;
- const isIncluded = title.toLowerCase().includes(actionFilterText.toLowerCase());
+ const { title, subtitle } = renderTitleForCall(item.action, sdkLanguage);
+ const text = subtitle ? `${title} ${subtitle}` : title;
+ const isIncluded = text.toLowerCase().includes(actionFilterText.toLowerCase());
return isIncluded ? true : 'if-needed';
- }, [selectedTime, actionFilterText]);
+ }, [selectedTime, actionFilterText, sdkLanguage]);
const onSelectedAction = React.useCallback((item: ActionTreeItem) => {
onSelected?.(item.action);
@@ -147,8 +148,6 @@ export const renderAction = (
const { errors, warnings } = model?.stats(action) ?? { errors: 0, warnings: 0 };
const badgeLabel = [pluralize(errors, 'error'), pluralize(warnings, 'warning')].filter(Boolean).join(', ');
- const locator = action.params.selector ? asLocatorDescription(sdkLanguage || 'javascript', action.params.selector) : undefined;
-
const isSkipped = action.class === 'Test' && action.method === 'test.step' && action.annotations?.some(a => a.type === 'skip');
let time: string = '';
if (action.endTime)
@@ -157,7 +156,7 @@ export const renderAction = (
time = 'Timed out';
else if (!isLive)
time = '-';
- const { elements, title } = renderTitleForCall(action);
+ const { elements, title, subtitle } = renderTitleForCall(action, sdkLanguage);
return
{elements}
@@ -174,11 +173,11 @@ export const renderAction = (
{!!warnings && {warnings}}
}
- {locator &&
{locator}
}
+ {subtitle &&
{subtitle}
}
;
};
-export function renderTitleForCall(action: ActionTraceEvent, sdkLanguage?: Language): { elements: React.ReactNode[], title: string } {
+export function renderTitleForCall(action: ActionTraceEvent, sdkLanguage?: Language): { elements: React.ReactNode[], title: string, subtitle?: string } {
let titleFormat = action.title ?? getMetainfo({ type: action.class, method: action.method })?.title ?? action.method;
titleFormat = titleFormat.replace(/\n/g, ' ');
@@ -195,7 +194,7 @@ export function renderTitleForCall(action: ActionTraceEvent, sdkLanguage?: Langu
elements.push(chunk);
title.push(chunk);
- const param = formatProtocolParam(action.params, quotedText);
+ const param = formatProtocolParam(action.params, quotedText, sdkLanguage);
if (param === undefined) {
elements.push(fullMatch);
title.push(fullMatch);
@@ -215,12 +214,8 @@ export function renderTitleForCall(action: ActionTraceEvent, sdkLanguage?: Langu
title.push(chunk);
}
- const locator = action.params.selector ? asLocatorDescription(sdkLanguage || 'javascript', action.params.selector) : undefined;
- if (locator) {
- title.push(' ');
- title.push(locator);
- }
- return { elements, title: title.join('') };
+ const subtitle = renderSubtitleForCall({ type: action.class, method: action.method, params: action.params, subtitle: action.subtitle }, sdkLanguage);
+ return { elements, title: title.join(''), subtitle };
}
function pluralize(count: number, noun: string): string {
diff --git a/packages/trace-viewer/src/ui/callTab.tsx b/packages/trace-viewer/src/ui/callTab.tsx
index 2465f90a72330..1e49091ac1a87 100644
--- a/packages/trace-viewer/src/ui/callTab.tsx
+++ b/packages/trace-viewer/src/ui/callTab.tsx
@@ -41,11 +41,11 @@ export const CallTab: React.FunctionComponent<{
const startTimeMillis = action.startTime - startTimeOffset;
const startTime = msToString(startTimeMillis);
- const { title } = renderTitleForCall(action);
+ const { title, subtitle } = renderTitleForCall(action, sdkLanguage);
return (
-
{title}
+
{subtitle ? `${title} ${subtitle}` : title}
Time
{renderProperty({ name: 'start', type: 'literal', text: startTime })}
{renderProperty({ name: 'duration', type: 'literal', text: renderDuration(action) })}
diff --git a/packages/trace/src/trace.ts b/packages/trace/src/trace.ts
index a0ef448183be5..4ef8f0c6e21c6 100644
--- a/packages/trace/src/trace.ts
+++ b/packages/trace/src/trace.ts
@@ -135,6 +135,7 @@ export type BeforeActionTraceEvent = {
callId: string;
startTime: number;
title?: string;
+ subtitle?: string;
class: string;
method: string;
params: Record
;
diff --git a/tests/config/utils.ts b/tests/config/utils.ts
index eca03a8006092..4f96e4d811b8d 100644
--- a/tests/config/utils.ts
+++ b/tests/config/utils.ts
@@ -159,7 +159,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
return {
events,
resources,
- actions: actionObjects.map(a => iso.renderTitleForCall({ ...a, type: a.class })),
+ actions: actionObjects.map(a => iso.renderFullTitleForCall({ ...a, type: a.class })),
actionObjects,
stacks,
};
diff --git a/tests/library/inspector/pause.spec.ts b/tests/library/inspector/pause.spec.ts
index 0f7c3bd280532..3e79cdc400a6f 100644
--- a/tests/library/inspector/pause.spec.ts
+++ b/tests/library/inspector/pause.spec.ts
@@ -276,7 +276,7 @@ it.describe('pause', () => {
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
- 'Click(page.locator(\'button\'))- XXms',
+ 'Click locator(\'button\')- XXms',
'Pause',
]);
await recorderPage.click('[title="Resume (F8)"]');
@@ -324,8 +324,8 @@ it.describe('pause', () => {
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
- 'Expect "toHaveText"(page.locator(\'button\'))- XXms',
- 'Expect "not toHaveText"(page.locator(\'button\'))- XXms',
+ 'Expect "toHaveText" locator(\'button\')- XXms',
+ 'Expect "not toHaveText" locator(\'button\')- XXms',
'Pause',
]);
await recorderPage.click('[title="Resume (F8)"]');
@@ -368,7 +368,7 @@ it.describe('pause', () => {
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
'Wait for event "console"- XXms',
- 'Click(page.getByRole(\'button\', { name: \'Submit\' }))- XXms',
+ 'Click getByRole(\'button\', { name: \'Submit\' })- XXms',
'Pause',
]);
await recorderPage.click('[title="Resume (F8)"]');
@@ -387,7 +387,7 @@ it.describe('pause', () => {
await recorderPage.waitForSelector('.source-line-error-underline');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
- 'Is checked(page.getByRole(\'button\'))- XXms',
+ 'Is checked getByRole(\'button\')- XXms',
'waiting for getByRole(\'button\')',
'error: Error: Not a checkbox or radio button',
]);
@@ -407,8 +407,8 @@ it.describe('pause', () => {
await recorderPage.waitForSelector('.source-line-error-underline');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
- 'Expect "toHaveText"(page.getByRole(\'button\'))- XXms',
- 'Expect "toHaveText" with timeout 1ms',
+ 'Expect "toHaveText" getByRole(\'button\')- XXms',
+ 'Expect "toHaveText" getByRole(\'button\') with timeout 1ms',
'waiting for getByRole(\'button\')',
'error: Expect failed',
]);
diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts
index d9a7a02f15f8d..7c7d855c88377 100644
--- a/tests/library/trace-viewer.spec.ts
+++ b/tests/library/trace-viewer.spec.ts
@@ -178,7 +178,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer(traceFile);
await expect(traceViewer.actionTitles).toHaveText([
/Create page/,
- /Navigate to "data:"/,
+ /Navigate.*data:/,
/^Expect "toHaveTitle"[\d]+ms$/,
/^Expect "toHaveURL"[\d]+ms$/,
/Set content/,
@@ -191,7 +191,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
/Wait for navigation/,
/Wait for event "response"/,
/Wait for timeout/,
- /Navigate to "\/frames\/frame.html"/,
+ /Navigate.*\/frames\/frame.html/,
/Set viewport size/,
/Hover/,
/Close page/,
@@ -258,7 +258,7 @@ test('should open uncompressed trace directory', async ({ showTraceViewer }) =>
const traceViewer = await showTraceViewer(traceDir);
await expect(traceViewer.actionTitles).toHaveText([
/Create page/,
- /Navigate to "data:"/,
+ /Navigate.*data:/,
/^Expect "toHaveTitle"[\d]+ms$/,
/^Expect "toHaveURL"[\d]+ms$/,
/Set content/,
@@ -271,7 +271,7 @@ test('should open uncompressed trace directory', async ({ showTraceViewer }) =>
/Wait for navigation/,
/Wait for event "response"/,
/Wait for timeout/,
- /Navigate to "\/frames\/frame.html"/,
+ /Navigate.*\/frames\/frame.html/,
/Set viewport size/,
/Hover/,
/Close page/,
@@ -1855,7 +1855,7 @@ test('should not record route actions', {
});
await expect(traceViewer.actionTitles).toHaveText([
- /Navigate to "\/empty.html"/,
+ /Navigate.*\/empty.html/,
]);
});
@@ -2256,7 +2256,7 @@ test('should render blob trace received from message', async ({ showTraceViewer
await expect(traceViewer.page.locator('.drop-target')).not.toBeVisible();
await expect(traceViewer.actionTitles).toHaveText([
/Create page/,
- /Navigate to "data:"/,
+ /Navigate.*data:/,
/toHaveTitle/,
/toHaveURL/,
/Set content/,
@@ -2269,7 +2269,7 @@ test('should render blob trace received from message', async ({ showTraceViewer
/Wait for navigation/,
/Wait for event "response"/,
/Wait for timeout/,
- /Navigate to "\/frames\/frame.html"/,
+ /Navigate.*\/frames\/frame.html/,
/Set viewport size/,
/Hover/,
/Close page/,
@@ -2378,7 +2378,7 @@ test('should filter actions', async ({ runAndTrace, page }) => {
});
await expect(traceViewer.actionTitles).toHaveText([
- /Navigate to/,
+ /Navigate/,
/Expect "toBeChecked"/,
]);
await expect(traceViewer.page.getByText('3 hidden', { exact: true })).toBeVisible();
@@ -2393,7 +2393,7 @@ test('should filter actions', async ({ runAndTrace, page }) => {
await traceViewer.page.locator('.setting').getByText('Getters').click();
await expect(traceViewer.actionTitles).toHaveText([
- /Navigate to/,
+ /Navigate/,
/Get attribute "checked"/,
/Expect "toBeChecked"/,
]);
@@ -2402,7 +2402,7 @@ test('should filter actions', async ({ runAndTrace, page }) => {
await traceViewer.page.locator('.setting').getByText('Network routes').click();
await expect(traceViewer.actionTitles).toHaveText([
/Route requests/,
- /Navigate to/,
+ /Navigate/,
/Fulfill request/,
/Get attribute "checked"/,
/Expect "toBeChecked"/,
diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts
index 57e24c2baaa8c..898379927a7ea 100644
--- a/tests/library/tracing.spec.ts
+++ b/tests/library/tracing.spec.ts
@@ -42,14 +42,14 @@ test('should collect trace with resources, but no js', async ({ context, page, s
const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
expect(events[0].type).toBe('context-options');
expect(actions).toEqual([
- 'Navigate to "/frames/frame.html"',
+ 'Navigate /frames/frame.html',
'Set content',
- 'Click',
+ `Click locator('text="Click"')`,
'Mouse move',
'Double click',
'Insert "abc"',
- 'Navigate to "/input/fileupload.html"',
- 'Set input files',
+ 'Navigate /input/fileupload.html',
+ `Set input files locator('input[type="file"]')`,
'Wait for timeout',
'Close page',
]);
@@ -85,9 +85,9 @@ test('should use the correct title for event driven callbacks', async ({ context
expect(events[0].type).toBe('context-options');
expect(actions).toEqual([
'Route requests',
- 'Navigate to "/empty.html"',
+ 'Navigate /empty.html',
'Continue request',
- 'Navigate to "/grid.html"',
+ 'Navigate /grid.html',
'Evaluate',
'Reload',
'Evaluate',
@@ -222,14 +222,14 @@ test('should record context API request trace independently', async ({ context,
await context.request.tracing.stop({ path: apiTracePath });
const browserTrace = await parseTraceRaw(browserTracePath);
- expect(browserTrace.actions).toContain('Navigate to "/one-style.html"');
- expect(browserTrace.actions).not.toContain('POST "/simple.json"');
+ expect(browserTrace.actions).toContain('Navigate /one-style.html');
+ expect(browserTrace.actions).not.toContain('POST /simple.json');
expect(browserTrace.events.some(event => event.type === 'resource-snapshot' && event.snapshot.request.url.endsWith('/simple.json'))).toBe(false);
expect(browserTrace.events.some(event => event.type === 'resource-snapshot' && event.snapshot.request.url.endsWith('/one-style.html'))).toBe(true);
const apiTrace = await parseTraceRaw(apiTracePath);
- expect(apiTrace.actions).toContain('POST "/simple.json"');
- expect(apiTrace.actions).not.toContain('Navigate to "/one-style.html"');
+ expect(apiTrace.actions).toContain('POST /simple.json');
+ expect(apiTrace.actions).not.toContain('Navigate /one-style.html');
const apiAction = apiTrace.actionObjects.find(action => action.class === 'APIRequestContext' && action.method === 'fetch')!;
expect(relativeStack(apiAction, apiTrace.stacks)).toEqual(['tracing.spec.ts']);
expect(apiTrace.events.filter(event => event.type === 'resource-snapshot').map(event => event.snapshot.request.url)).toEqual([apiURL]);
@@ -252,9 +252,9 @@ test('should collect two traces', async ({ context, page, server }, testInfo) =>
const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace1.zip'));
expect(events[0].type).toBe('context-options');
expect(actions).toEqual([
- 'Navigate to "/empty.html"',
+ 'Navigate /empty.html',
'Set content',
- 'Click',
+ `Click locator('text="Click"')`,
]);
}
@@ -262,7 +262,7 @@ test('should collect two traces', async ({ context, page, server }, testInfo) =>
const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace2.zip'));
expect(events[0].type).toBe('context-options');
expect(actions).toEqual([
- 'Double click',
+ `Double click locator('text="Click"')`,
'Close page',
]);
}
@@ -296,7 +296,7 @@ test('should respect tracesDir and name', async ({ browserType, server, mode },
{
const { resources, actions } = await parseTraceRaw(testInfo.outputPath('trace1.zip'));
- expect(actions).toEqual(['Navigate to "/one-style.html"']);
+ expect(actions).toEqual(['Navigate /one-style.html']);
expect(resourceNames(resources)).toEqual([
'resources/XXX.css',
'resources/XXX.html',
@@ -308,7 +308,7 @@ test('should respect tracesDir and name', async ({ browserType, server, mode },
{
const { resources, actions } = await parseTraceRaw(testInfo.outputPath('trace2.zip'));
- expect(actions).toEqual(['Navigate to "/har.html"']);
+ expect(actions).toEqual(['Navigate /har.html']);
expect(resourceNames(resources)).toEqual([
'resources/XXX.css',
'resources/XXX.html',
@@ -549,7 +549,7 @@ test('should include interrupted actions', async ({ context, page, server }, tes
await context.close();
const { actions } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
- expect(actions).toContain('Click');
+ expect(actions).toContain(`Click locator('text="ClickNoButton"')`);
});
test('should throw when starting with different options', async ({ context }) => {
@@ -590,8 +590,8 @@ test('should work with multiple chunks', async ({ context, page, server }, testI
expect(trace1.events[0].type).toBe('context-options');
expect(trace1.actions).toEqual([
'Set content',
- 'Click',
- 'Click',
+ `Click locator('text="Click"')`,
+ `Click locator('text="ClickNoButton"')`,
'Evaluate',
]);
expect(trace1.events.some(e => e.type === 'frame-snapshot')).toBeTruthy();
@@ -600,7 +600,7 @@ test('should work with multiple chunks', async ({ context, page, server }, testI
const trace2 = await parseTraceRaw(testInfo.outputPath('trace2.zip'));
expect(trace2.events[0].type).toBe('context-options');
expect(trace2.actions).toEqual([
- 'Hover',
+ `Hover locator('text="Click"')`,
]);
expect(trace2.events.some(e => e.type === 'frame-snapshot')).toBeTruthy();
expect(trace2.events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css'))).toBeTruthy();
@@ -648,7 +648,7 @@ test('should ignore iframes in head', async ({ context, page, server }, testInfo
await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') });
const trace = await parseTraceRaw(testInfo.outputPath('trace.zip'));
- expect(trace.actions).toEqual(['Click']);
+ expect(trace.actions).toEqual([`Click locator('button')`]);
expect(trace.events.find(e => e.type === 'frame-snapshot')).toBeTruthy();
expect(trace.events.find(e => e.type === 'frame-snapshot' && JSON.stringify(e.snapshot.html).includes('IFRAME'))).toBeFalsy();
});
diff --git a/tests/mcp/cli-test.spec.ts b/tests/mcp/cli-test.spec.ts
index 7a246c7d05f8d..aba99d794e582 100644
--- a/tests/mcp/cli-test.spec.ts
+++ b/tests/mcp/cli-test.spec.ts
@@ -58,14 +58,14 @@ test('debug test and snapshot', async ({ cliEnv, cli, childProcess }) => {
const { output: stepOutput } = await cli(`--session=${session}`, 'step-over');
expect(stepOutput).toContain('### Paused');
- expect(stepOutput).toContain(`- Expect "toBeVisible" at subdir${path.sep}a.test.ts:5`);
+ expect(stepOutput).toContain(`- Expect "toBeVisible" getByRole('button', { name: 'Submit' }) at subdir${path.sep}a.test.ts:5`);
const snapshotResult = await cli(`--session=${session}`, 'snapshot');
expect(snapshotResult.inlineSnapshot).toContain('button "Submit"');
const { output: pauseAtOutput } = await cli(`--session=${session}`, 'pause-at', 'a.test.ts:7');
expect(pauseAtOutput).toContain('### Paused');
- expect(pauseAtOutput).toContain(`- Expect "toBeVisible" at subdir${path.sep}a.test.ts:7`);
+ expect(pauseAtOutput).toContain(`- Expect "toBeVisible" getByRole('button', { name: 'Close' }) at subdir${path.sep}a.test.ts:7`);
await cli(`--session=${session}`, 'resume');
diff --git a/tests/mcp/test-debug.spec.ts b/tests/mcp/test-debug.spec.ts
index 880d73f3f5ae9..4389d0443482c 100644
--- a/tests/mcp/test-debug.spec.ts
+++ b/tests/mcp/test-debug.spec.ts
@@ -64,7 +64,7 @@ Timeout: 1000ms
Error: element(s) not found
Call log:
- - Expect "toBeVisible" with timeout 1000ms
+ - Expect "toBeVisible" getByRole('button', { name: 'Missing' }) with timeout 1000ms
- waiting for getByRole('button', { name: 'Missing' })
@@ -183,7 +183,7 @@ Timeout: 1000ms
Error: element(s) not found
Call log:
- - Expect "toBeVisible" with timeout 1000ms
+ - Expect "toBeVisible" getByRole('button', { name: 'Missing' }) with timeout 1000ms
- waiting for getByRole('button', { name: 'Missing' })
diff --git a/tests/page/expect-boolean.spec.ts b/tests/page/expect-boolean.spec.ts
index f2eddbc3f1b6e..043c55f695d9f 100644
--- a/tests/page/expect-boolean.spec.ts
+++ b/tests/page/expect-boolean.spec.ts
@@ -61,7 +61,7 @@ Locator: locator('input')
Expected: checked
Received: unchecked
Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" locator('input') with timeout 1000ms`);
});
test('with not', async ({ page }) => {
@@ -86,7 +86,7 @@ Locator: locator('input')
Expected: not checked
Received: checked
Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" locator('input') with timeout 1000ms`);
expect(stripAnsi(error.message)).toContain(`locator resolved to `);
});
@@ -100,7 +100,7 @@ Locator: locator('input')
Expected: unchecked
Received: checked
Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" locator('input') with timeout 1000ms`);
expect(stripAnsi(error.message)).toContain(`locator resolved to `);
});
@@ -114,7 +114,7 @@ Locator: locator('input')
Expected: indeterminate
Received: unchecked
Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" locator('input') with timeout 1000ms`);
});
test('fail missing', async ({ page }) => {
@@ -129,7 +129,7 @@ Timeout: 1000ms
Error: element(s) not found
Call log:
- - Expect "not toBeChecked" with timeout 1000ms
+ - Expect "not toBeChecked" locator('input2') with timeout 1000ms
- waiting for locator('input2')
`);
});
@@ -475,7 +475,7 @@ Timeout: 1000ms
Error: element(s) not found
Call log:
- - Expect "not toBeHidden" with timeout 1000ms
+ - Expect "not toBeHidden" locator('button') with timeout 1000ms
`);
});
diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts
index a118d402717b2..76d66d5287feb 100644
--- a/tests/page/expect-misc.spec.ts
+++ b/tests/page/expect-misc.spec.ts
@@ -73,7 +73,7 @@ Locator: locator('span')
Expected: 0
Received: 1
Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toHaveCount" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toHaveCount" locator('span') with timeout 1000ms`);
});
test('fail zero 2', async ({ page }) => {
@@ -86,7 +86,7 @@ Locator: locator('span')
Expected: not 1
Received: 1
Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveCount" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveCount" locator('span') with timeout 1000ms`);
});
});
@@ -124,7 +124,7 @@ Locator: locator('div')
Expected: "error"
Received: "string"
Timeout: 200ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`);
});
test('pass number', async ({ page }) => {
@@ -145,7 +145,7 @@ Locator: locator('div')
Expected: 1
Received: 2021
Timeout: 200ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`);
});
test('pass boolean', async ({ page }) => {
@@ -166,7 +166,7 @@ Locator: locator('div')
Expected: true
Received: false
Timeout: 200ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`);
});
test('pass boolean 2', async ({ page }) => {
@@ -187,7 +187,7 @@ Locator: locator('div')
Expected: true
Received: false
Timeout: 200ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`);
});
test('pass undefined', async ({ page }) => {
@@ -249,7 +249,7 @@ Timeout: 1000ms
Call log:
`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" locator('div') with timeout 1000ms`);
});
test('pass with array', async ({ page }) => {
@@ -264,7 +264,7 @@ Call log:
const error = await expect(locator).toHaveClass(['foo', 'bar', /[a-z]az/], { timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`expect(locator).toHaveClass(expected) failed`);
expect(stripAnsi(error.message)).toContain(`Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" locator('div') with timeout 1000ms`);
});
});
@@ -298,7 +298,7 @@ Timeout: 1000ms
Call log:
`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" locator('div') with timeout 1000ms`);
});
test('pass with array', async ({ page }) => {
@@ -316,7 +316,7 @@ Call log:
const error = await expect(locator).toContainClass(['foo', 'bar', 'baz'], { timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`expect(locator).toContainClass(expected) failed`);
expect(stripAnsi(error.message)).toContain(`Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" locator('div') with timeout 1000ms`);
});
});
@@ -442,7 +442,7 @@ Timeout: 1000ms
Call log:
`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" locator('#node') with timeout 1000ms`);
}
{
const error = await expect(locator).toHaveAttribute('disabled', /.*/, { timeout: 1000 }).catch(e => e);
@@ -455,7 +455,7 @@ Timeout: 1000ms
Call log:
`);
- expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" locator('#node') with timeout 1000ms`);
}
await expect(locator).not.toHaveAttribute('disabled', '');
await expect(locator).not.toHaveAttribute('disabled', /.*/);
@@ -477,7 +477,7 @@ Timeout: 1000ms
Call log:
`);
- expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" locator('#node') with timeout 1000ms`);
}
{
const error = await expect(locator).not.toHaveAttribute('checked', /.*/, { timeout: 1000 }).catch(e => e);
@@ -487,7 +487,7 @@ Locator: locator('#node')
Expected pattern: not /.*/
Received string: ""
Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" locator('#node') with timeout 1000ms`);
}
});
diff --git a/tests/page/expect-timeout.spec.ts b/tests/page/expect-timeout.spec.ts
index 6753716a1c25b..04f8b6544d61c 100644
--- a/tests/page/expect-timeout.spec.ts
+++ b/tests/page/expect-timeout.spec.ts
@@ -136,7 +136,7 @@ Expected: visible
Error: element(s) not found
Call log:
- - Expect "toBeVisible" with timeout 5000ms
+ - Expect "toBeVisible" locator('span') with timeout 5000ms
- waiting for locator('span')
- operation was aborted: stop it`);
});
diff --git a/tests/page/expect-to-have-text.spec.ts b/tests/page/expect-to-have-text.spec.ts
index 081e9a2ecafb7..e11e7d813c739 100644
--- a/tests/page/expect-to-have-text.spec.ts
+++ b/tests/page/expect-to-have-text.spec.ts
@@ -259,7 +259,7 @@ test.describe('toHaveText with array', () => {
const error = await expect(locator).not.toHaveText([], { timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`expect(locator).not.toHaveText(expected)`);
expect(stripAnsi(error.message)).toContain(`Timeout: 1000ms`);
- expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveText" with timeout 1000ms`);
+ expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveText" locator('p') with timeout 1000ms`);
});
test('pass eventually empty', async ({ page }) => {
@@ -289,7 +289,7 @@ Timeout: 1000ms
]
Call log:
- - Expect \"toHaveText\" with timeout 1000ms
+ - Expect \"toHaveText\" locator('div') with timeout 1000ms
- waiting for locator('div')
`);
expect(stripAnsi(error.message)).toContain('locator resolved to 2 elements');
diff --git a/tests/playwright-test/playwright.reuse.browser.spec.ts b/tests/playwright-test/playwright.reuse.browser.spec.ts
index 3f8ad95789ddc..5a3af6921abbb 100644
--- a/tests/playwright-test/playwright.reuse.browser.spec.ts
+++ b/tests/playwright-test/playwright.reuse.browser.spec.ts
@@ -104,11 +104,14 @@ test('should produce correct test steps', async ({ runInlineTest, runServer }) =
const result = await runInlineTest({
'reporter.ts': `
class Reporter {
+ stepTitle(step) {
+ return step.subtitle ? step.title + ' ' + step.subtitle : step.title;
+ }
onStepBegin(test, result, step) {
- console.log('%% onStepBegin [' + step.category + '] ' + step.title);
+ console.log('%% onStepBegin [' + step.category + '] ' + this.stepTitle(step));
}
onStepEnd(test, result, step) {
- console.log('%% onStepEnd [' + step.category + '] ' + step.title);
+ console.log('%% onStepEnd [' + step.category + '] ' + this.stepTitle(step));
}
}
module.exports = Reporter;
@@ -137,8 +140,8 @@ test('should produce correct test steps', async ({ runInlineTest, runServer }) =
'onStepEnd [pw:api] Create page',
'onStepEnd [fixture] Fixture "page"',
'onStepEnd [hook] Before Hooks',
- 'onStepBegin [pw:api] Navigate to "about:blank"',
- 'onStepEnd [pw:api] Navigate to "about:blank"',
+ 'onStepBegin [pw:api] Navigate about:blank',
+ 'onStepEnd [pw:api] Navigate about:blank',
'onStepBegin [pw:api] Evaluate',
'onStepEnd [pw:api] Evaluate',
'onStepBegin [hook] After Hooks',
diff --git a/tests/playwright-test/playwright.reuse.spec.ts b/tests/playwright-test/playwright.reuse.spec.ts
index 047ec35d2952c..b6bae8cab2b45 100644
--- a/tests/playwright-test/playwright.reuse.spec.ts
+++ b/tests/playwright-test/playwright.reuse.spec.ts
@@ -139,7 +139,7 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
' Fixture "page"',
' Create page',
'Set content',
- 'Click',
+ `Click locator('button')`,
'After Hooks',
' Fixture "page"',
' Fixture "context"',
@@ -154,8 +154,8 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
' Fixture "page"',
'Expect "toBe"',
'Set content',
- 'Fill "value"',
- 'Click',
+ `Fill "value" locator('input')`,
+ `Click locator('input')`,
'After Hooks',
' Fixture "page"',
' Fixture "context"',
@@ -485,15 +485,15 @@ test('should reset tracing', async ({ runInlineTest }, testInfo) => {
const trace1 = await parseTrace(traceFile1);
expect(trace1.model.renderActionTree()).toEqual([
'Set content',
- 'Click',
+ `Click locator('button')`,
]);
expect(trace1.snapshots.snapshotsForTest().length).toBeGreaterThan(0);
const trace2 = await parseTrace(traceFile2);
expect(trace2.model.renderActionTree()).toEqual([
'Set content',
- 'Fill "value"',
- 'Click',
+ `Fill "value" locator('input')`,
+ `Click locator('input')`,
]);
expect(trace1.snapshots.snapshotsForTest().length).toBeGreaterThan(0);
});
diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts
index 4ed1a3874a161..31319a666793a 100644
--- a/tests/playwright-test/playwright.trace.spec.ts
+++ b/tests/playwright-test/playwright.trace.spec.ts
@@ -97,8 +97,8 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
' Create context',
' Fixture "page"',
' Create page',
- 'Navigate to "about:blank"',
- 'GET "/empty.html"',
+ 'Navigate about:blank',
+ 'GET /empty.html',
'After Hooks',
' Fixture "page"',
' Fixture "context"',
@@ -109,7 +109,7 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
expect(trace2.model.renderActionTree()).toEqual([
'Before Hooks',
'Create request context',
- 'GET "/empty.html"',
+ 'GET /empty.html',
'After Hooks',
]);
const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'));
@@ -121,8 +121,8 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
' Create context',
' Fixture "page"',
' Create page',
- 'Navigate to "about:blank"',
- 'GET "/empty.html"',
+ 'Navigate about:blank',
+ 'GET /empty.html',
'Expect "toBe"',
'After Hooks',
' Fixture "page"',
@@ -412,7 +412,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve
' Create context',
' Fixture "page"',
' Create page',
- 'Navigate to "about:blank"',
+ 'Navigate about:blank',
'After Hooks',
' Fixture "page"',
' Fixture "context"',
@@ -420,7 +420,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve
' afterAll hook',
' Fixture "request"',
' Create request context',
- ' GET "/empty.html"',
+ ' GET /empty.html',
' Fixture "request"',
'Worker Cleanup',
' Fixture "browser"',
@@ -538,7 +538,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa
}, { trace: 'retain-on-failure' });
const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip');
const trace = await parseTrace(tracePath);
- expect(trace.model.renderActionTree()).toContain('Navigate to "about:blank"');
+ expect(trace.model.renderActionTree()).toContain('Navigate about:blank');
expect(result.failed).toBe(1);
});
@@ -560,7 +560,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa
}, { trace: 'retain-on-failure' });
const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip');
const trace = await parseTrace(tracePath);
- expect(trace.model.renderActionTree()).toContain(' Navigate to "about:blank"');
+ expect(trace.model.renderActionTree()).toContain(' Navigate about:blank');
expect(result.failed).toBe(1);
});
@@ -580,7 +580,7 @@ test(`trace:retain-on-failure should create trace if request context is disposed
}, { trace: 'retain-on-failure' });
const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip');
const trace = await parseTrace(tracePath);
- expect(trace.model.renderActionTree()).toContain('GET "/empty.html"');
+ expect(trace.model.renderActionTree()).toContain('GET /empty.html');
expect(result.failed).toBe(1);
});
@@ -702,11 +702,11 @@ test('should expand expect.toPass', async ({ runInlineTest }, testInfo) => {
' Fixture "page"',
' Create page',
'Expect "toPass"',
- ' Navigate to "data:"',
+ ' Navigate data:',
' Expect "toBe"',
- ' Navigate to "data:"',
+ ' Navigate data:',
' Expect "toBe"',
- ' Navigate to "data:"',
+ ' Navigate data:',
' Expect "toBe"',
'After Hooks',
' Fixture "page"',
@@ -871,7 +871,7 @@ test('should use custom expect message in trace', async ({ runInlineTest }, test
' Create context',
' Fixture "page"',
' Create page',
- 'expect to have text: find a hotel',
+ `expect to have text: find a hotel getByRole('button', { name: 'Find a hotel' })`,
'After Hooks',
' Fixture "page"',
' Fixture "context"',
@@ -1187,7 +1187,7 @@ test('trace:retain-on-first-failure should create trace but only on first failur
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
const trace = await parseTrace(tracePath);
- expect(trace.model.renderActionTree()).toContain('Navigate to "about:blank"');
+ expect(trace.model.renderActionTree()).toContain('Navigate about:blank');
expect(result.failed).toBe(1);
});
@@ -1204,7 +1204,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef
}, { trace: 'retain-on-first-failure' });
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
const trace = await parseTrace(tracePath);
- expect(trace.model.renderActionTree()).toContain('Navigate to "about:blank"');
+ expect(trace.model.renderActionTree()).toContain('Navigate about:blank');
expect(result.failed).toBe(1);
});
@@ -1223,7 +1223,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef
}, { trace: 'retain-on-first-failure' });
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
const trace = await parseTrace(tracePath);
- expect(trace.model.renderActionTree()).toContain(' Navigate to "about:blank"');
+ expect(trace.model.renderActionTree()).toContain(' Navigate about:blank');
expect(result.failed).toBe(1);
});
@@ -1240,7 +1240,7 @@ test('trace:retain-on-first-failure should create trace if request context is di
}, { trace: 'retain-on-first-failure' });
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
const trace = await parseTrace(tracePath);
- expect(trace.model.renderActionTree()).toContain('GET "/empty.html"');
+ expect(trace.model.renderActionTree()).toContain('GET /empty.html');
expect(result.failed).toBe(1);
});
@@ -1387,9 +1387,9 @@ test('should not nest top level expect into unfinished api calls ', {
' Create context',
' Fixture "page"',
' Create page',
- 'Navigate to "/index"',
- 'GET "/hang"',
- 'Expect "toBeVisible"',
+ 'Navigate /index',
+ 'GET /hang',
+ `Expect "toBeVisible" getByText('Hello!')`,
'After Hooks',
' Fixture "page"',
' Fixture "context"',
@@ -1464,9 +1464,9 @@ test('should record trace snapshot for more obscure commands', async ({ runInlin
' Launch browser',
'Create page',
'Set content',
- 'Query count',
+ `Query count locator('div')`,
'Expect "toBe"',
- 'Bounding box',
+ `Bounding box locator('div')`,
'After Hooks',
]);
diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts
index 3a6912791a597..25a6c659aa4fd 100644
--- a/tests/playwright-test/reporter-blob.spec.ts
+++ b/tests/playwright-test/reporter-blob.spec.ts
@@ -1263,7 +1263,7 @@ test('preserve step params', async ({ runInlineTest, mergeReports }) => {
class ParamsReporter {
onStepEnd(test, result, step) {
if (step.category === 'test.step' || step.title.startsWith('Navigate'))
- console.log('%%' + step.title + ' | ' + JSON.stringify(step.params));
+ console.log('%%' + (step.subtitle ? step.title + ' ' + step.subtitle : step.title) + ' | ' + JSON.stringify(step.params));
}
}
module.exports = ParamsReporter;
@@ -1285,7 +1285,7 @@ test('preserve step params', async ({ runInlineTest, mergeReports }) => {
const { exitCode, outputLines } = await mergeReports(reportDir, undefined, { additionalArgs: ['--reporter', './params-reporter.js'] });
expect(exitCode).toBe(0);
expect(outputLines).toEqual([
- `Navigate to "about:blank" | {"url":"about:blank"}`,
+ `Navigate about:blank | {"url":"about:blank"}`,
`my step | {"foo":"bar","count":7}`,
]);
});
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index d282e54708301..b4e06167548a6 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -779,10 +779,12 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await page.getByRole('link', { name: 'View Trace' }).click();
// Trace viewer should not hang here when displaying parallal requests.
- await expect(page.getByTestId('actions-tree')).toContainText('GET');
- await page.getByText('GET "/empty.html"').nth(2).click();
- await page.getByText('GET "/empty.html"').nth(1).click();
- await page.getByText('GET "/empty.html"').nth(0).click();
+ const getActions = page.getByTestId('actions-tree').getByRole('treeitem', { name: /GET/ });
+ await expect(getActions).toHaveCount(4);
+ await expect(getActions.first()).toContainText('/empty.html');
+ await getActions.nth(2).click();
+ await getActions.nth(1).click();
+ await getActions.nth(0).click();
});
test('should warn user when viewing via file:// protocol', async ({ runInlineTest, page, showReport }, testInfo) => {
@@ -934,6 +936,30 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await expect(page.locator('.tree-item-title', { hasText: 'fill password' })).toBeHidden();
});
+ test('should highlight filter matches in step title and subtitle', async ({ runInlineTest, page, showReport }) => {
+ const result = await runInlineTest({
+ 'a.test.js': `
+ import { test, expect } from '@playwright/test';
+ test('has steps', async ({ page }) => {
+ await page.setContent('');
+ await page.click('#target');
+ });
+ `,
+ }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+
+ await showReport();
+ await page.getByRole('link', { name: 'has steps' }).click();
+
+ const filterInput = page.getByLabel('Filter steps');
+ await filterInput.fill('click');
+ await expect(page.locator('.step-title-highlight')).toHaveText(['Click']);
+
+ await filterInput.fill('#target');
+ await expect(page.locator('.step-subtitle .step-title-highlight')).toHaveText(['#target']);
+ });
+
test('should show step snippets from non-root', async ({ runInlineTest, page, showReport }) => {
const result = await runInlineTest({
'playwright.config.js': `
diff --git a/tests/playwright-test/reporter-perfetto.spec.ts b/tests/playwright-test/reporter-perfetto.spec.ts
index cd89c21d2389b..15d230405e996 100644
--- a/tests/playwright-test/reporter-perfetto.spec.ts
+++ b/tests/playwright-test/reporter-perfetto.spec.ts
@@ -208,7 +208,7 @@ test('should report step params', async ({ runInlineTest }, testInfo) => {
expect(result.exitCode).toBe(0);
const events = slices(readTrace(testInfo.outputPath()).traceEvents);
- expect(findSlice(events, 'Navigate to "about:blank"')!.args.params).toEqual({ url: 'about:blank' });
+ expect(findSlice(events, 'Navigate about:blank')!.args.params).toEqual({ url: 'about:blank' });
expect(findSlice(events, 'Set content')!.args.params).toBe(undefined);
expect(findSlice(events, `Click getByRole('button')`)!.args.params).toEqual({ locator: `getByRole('button')` });
expect(findSlice(events, `Expect "toBeVisible" getByRole('button')`)!.args.params).toEqual({ locator: `getByRole('button')` });
diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts
index da821b6f25109..559e00323fb00 100644
--- a/tests/playwright-test/test-step.spec.ts
+++ b/tests/playwright-test/test-step.spec.ts
@@ -77,7 +77,8 @@ export default class MyReporter implements Reporter {
location = formatLocation(step.location);
const skip = step.annotations?.find(a => a.type === 'skip');
const skipped = skip?.description ? ' (skipped: ' + skip.description + ')' : skip ? ' (skipped)' : '';
- console.log(formatPrefix(step.category) + indent + step.title + location + skipped);
+ const title = step.subtitle ? step.title + ' ' + step.subtitle : step.title;
+ console.log(formatPrefix(step.category) + indent + title + location + skipped);
if (step.error) {
const errorLocation = this.printErrorLocation ? formatLocation(step.error.location) : '';
console.log(formatPrefix(step.category) + indent + '↪ error: ' + this.trimError(step.error.message!) + errorLocation);
@@ -1001,12 +1002,12 @@ pw:api | Create context
fixture | Fixture "page"
pw:api | Create page
expect |Expect "toPass" @ a.test.ts:11
-pw:api | Navigate to "about:blank" @ a.test.ts:6
+pw:api | Navigate about:blank @ a.test.ts:6
test.step | inner step attempt: 0 @ a.test.ts:7
test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality
expect | Expect "toBe" @ a.test.ts:9
expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality
-pw:api | Navigate to "about:blank" @ a.test.ts:6
+pw:api | Navigate about:blank @ a.test.ts:6
test.step | inner step attempt: 1 @ a.test.ts:7
expect | Expect "toBe" @ a.test.ts:9
hook |After Hooks
@@ -1053,12 +1054,12 @@ pw:api | Create context
fixture | Fixture "page"
pw:api | Create page
expect |Expect "poll toHaveLength" @ a.test.ts:14
-pw:api | Navigate to "about:blank" @ a.test.ts:7
+pw:api | Navigate about:blank @ a.test.ts:7
test.step | inner step attempt: 0 @ a.test.ts:8
expect | Expect "toBe" @ a.test.ts:10
expect | Expect "toHaveLength" @ a.test.ts:6
expect | ↪ error: Error: expect(received).toHaveLength(expected)
-pw:api | Navigate to "about:blank" @ a.test.ts:7
+pw:api | Navigate about:blank @ a.test.ts:7
test.step | inner step attempt: 1 @ a.test.ts:8
expect | Expect "toBe" @ a.test.ts:10
expect | Expect "toHaveLength" @ a.test.ts:6
@@ -1246,12 +1247,12 @@ pw:api | Create page
fixture | Fixture "request"
pw:api | Create request context
pw:api |Wait for navigation @ a.test.ts:5
-pw:api |Navigate to "data:" @ a.test.ts:6
+pw:api |Navigate data: @ a.test.ts:6
pw:api |Click locator('button') @ a.test.ts:8
pw:api |Click getByRole('button') @ a.test.ts:9
-pw:api |GET "/empty.html" @ a.test.ts:10
+pw:api |GET /empty.html @ a.test.ts:10
pw:api |↪ error:
-pw:api |GET "/empty.html" @ a.test.ts:11
+pw:api |GET /empty.html @ a.test.ts:11
pw:api |↪ error:
hook |After Hooks
fixture | Fixture "request"
@@ -1463,7 +1464,7 @@ fixture | Fixture "context"
pw:api | Create context
fixture | Fixture "page"
pw:api | Create page
-pw:api |Navigate to "/empty.html" @ a.test.ts:4
+pw:api |Navigate /empty.html @ a.test.ts:4
pw:api |Set content @ a.test.ts:5
test.step |custom step @ a.test.ts:6
pw:api | Wait for event "response" @ a.test.ts:7
@@ -1510,7 +1511,7 @@ fixture | Fixture "page"
pw:api | Create page
pw:api |Wait for event "request" @ a.test.ts:5
pw:api |Wait for event "response" @ a.test.ts:6
-pw:api |Navigate to "/empty.html" @ a.test.ts:7
+pw:api |Navigate /empty.html @ a.test.ts:7
hook |After Hooks
fixture | Fixture "page"
fixture | Fixture "context"
@@ -1553,8 +1554,8 @@ pw:api | Create context
fixture | Fixture "page"
pw:api | Create page
test.step |custom step @ a.test.ts:4
-pw:api | Navigate to "/empty.html" @ a.test.ts:12
-pw:api | GET "/empty.html" @ a.test.ts:6
+pw:api | Navigate /empty.html @ a.test.ts:12
+pw:api | GET /empty.html @ a.test.ts:6
expect | Expect "toBe" @ a.test.ts:8
hook |After Hooks
fixture | Fixture "page"
@@ -1844,10 +1845,10 @@ pw:api | Create page
fixture | Fixture "bar" @ a.test.ts:4
pw:api | Set content @ a.test.ts:14
test.step | inner step @ a.test.ts:15
-pw:api | Navigate to "data:" @ a.test.ts:16
+pw:api | Navigate data: @ a.test.ts:16
pw:api | Set content @ a.test.ts:22
test.step | inner step @ a.test.ts:23
-pw:api | Navigate to "data:" @ a.test.ts:24
+pw:api | Navigate data: @ a.test.ts:24
expect |Expect "toBeVisible" locator('body') @ a.test.ts:32
expect |Expect "toBe" @ a.test.ts:33
expect |Expect "toBe" @ a.test.ts:34
@@ -1869,7 +1870,7 @@ test('should report step params', async ({ runInlineTest }) => {
export default class MyReporter implements Reporter {
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
if (step.location?.file.endsWith('a.test.ts'))
- console.log('%%' + step.category + ' | ' + step.title + ' | ' + JSON.stringify(step.params));
+ console.log('%%' + step.category + ' | ' + step.title + ' | ' + step.subtitle + ' | ' + JSON.stringify(step.params));
}
}
`,
@@ -1890,11 +1891,11 @@ test('should report step params', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0);
expect(result.outputLines).toEqual([
- `pw:api | Navigate to "about:blank" | {"url":"about:blank"}`,
- `pw:api | Set content | undefined`,
- `pw:api | Click getByRole('button') | {"locator":"getByRole('button')"}`,
- `expect | Expect "toBeVisible" getByRole('button') | {"locator":"getByRole('button')"}`,
- `test.step | my step | {"foo":"bar","count":7}`,
+ `pw:api | Navigate | about:blank | {"url":"about:blank"}`,
+ `pw:api | Set content | undefined | undefined`,
+ `pw:api | Click | getByRole('button') | {"locator":"getByRole('button')"}`,
+ `expect | Expect "toBeVisible" | getByRole('button') | {"locator":"getByRole('button')"}`,
+ `test.step | my step | undefined | {"foo":"bar","count":7}`,
]);
});
@@ -1905,7 +1906,7 @@ test('should report input step params', async ({ runInlineTest }) => {
export default class MyReporter implements Reporter {
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
if (step.location?.file.endsWith('a.test.ts'))
- console.log('%%' + step.title + ' | ' + JSON.stringify(step.params));
+ console.log('%%' + (step.subtitle ? step.title + ' ' + step.subtitle : step.title) + ' | ' + JSON.stringify(step.params));
}
}
`,
diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts
index 22551fa95d1c8..9e83293d77c4e 100644
--- a/tests/playwright-test/to-have-screenshot.spec.ts
+++ b/tests/playwright-test/to-have-screenshot.spec.ts
@@ -478,7 +478,7 @@ test('should fail to screenshot an element with infinite animation', async ({ ru
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Timeout 2000ms exceeded`);
- expect(result.output).toContain(`Expect "toHaveScreenshot" with timeout 2000ms`);
+ expect(result.output).toContain(`Expect "toHaveScreenshot" locator('body') with timeout 2000ms`);
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-previous.png'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(false);
diff --git a/tests/playwright-test/ui-mode-test-progress.spec.ts b/tests/playwright-test/ui-mode-test-progress.spec.ts
index 932f3c5cf1303..e144c5e929ca7 100644
--- a/tests/playwright-test/ui-mode-test-progress.spec.ts
+++ b/tests/playwright-test/ui-mode-test-progress.spec.ts
@@ -55,13 +55,13 @@ test('should update trace live', async ({ runUITest, server }) => {
'action list'
).toHaveText([
/Before Hooks[\d.]+m?s/,
- /Navigate to "\/one.html"/,
+ /Navigate.*\/one.html/,
]);
await expect(
listItem.locator(':scope[aria-selected="true"]'),
'last action to be selected'
- ).toHaveText(/Navigate to/);
+ ).toHaveText(/Navigate/);
await expect(
listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'),
'spinner'
@@ -81,13 +81,13 @@ test('should update trace live', async ({ runUITest, server }) => {
).toHaveText('One');
await expect(listItem).toHaveText([
/Before Hooks[\d.]+m?s/,
- /Navigate to "\/one.html"/,
- /Navigate to "\/two.html"/
+ /Navigate.*\/one.html/,
+ /Navigate.*\/two.html/
]);
await expect(
listItem.locator(':scope[aria-selected="true"]'),
'last action to be selected'
- ).toHaveText(/Navigate to/);
+ ).toHaveText(/Navigate/);
await expect(
listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'),
'spinner'
@@ -108,8 +108,8 @@ test('should update trace live', async ({ runUITest, server }) => {
await expect(listItem).toHaveText([
/Before Hooks[\d.]+m?s/,
- /Navigate to "\/one.html"/,
- /Navigate to "\/two.html"/,
+ /Navigate.*\/one.html/,
+ /Navigate.*\/two.html/,
/After Hooks[\d.]+m?s/,
]);
});
@@ -140,12 +140,12 @@ test('should preserve action list selection upon live trace update', async ({ ru
'action list'
).toHaveText([
/Before Hooks[\d.]+m?s/,
- /Navigate to "about:blank"/,
+ /Navigate.*about:blank/,
/Set content/,
]);
// Manually select page.goto.
- await page.getByTestId('actions-tree').getByText('Navigate to').click();
+ await page.getByTestId('actions-tree').getByText('Navigate').click();
// Generate more actions and check that we are still on the page.goto action.
latch.open();
@@ -154,14 +154,14 @@ test('should preserve action list selection upon live trace update', async ({ ru
'action list'
).toHaveText([
/Before Hooks[\d.]+m?s/,
- /Navigate to "about:blank"/,
+ /Navigate.*about:blank/,
/Set content/,
/Set content/,
]);
await expect(
listItem.locator(':scope[aria-selected="true"]'),
'selected action stays the same'
- ).toHaveText(/Navigate to/);
+ ).toHaveText(/Navigate/);
});
test('should update tracing network live', async ({ runUITest, server }) => {
@@ -201,7 +201,7 @@ test('should update tracing network live', async ({ runUITest, server }) => {
'action list'
).toHaveText([
/Before Hooks[\d.]+m?s/,
- /Navigate to "\/one.html"/,
+ /Navigate.*\/one.html/,
/Set content/,
]);
@@ -241,7 +241,7 @@ test('should show trace w/ multiple contexts', async ({ runUITest, server, creat
'action list'
).toHaveText([
/Before Hooks[\d.]+m?s/,
- /Navigate to "about:blank"/,
+ /Navigate.*about:blank/,
]);
latch.open();
diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts
index b337fba3b44ff..14aa61d3e93a2 100644
--- a/tests/playwright-test/ui-mode-trace.spec.ts
+++ b/tests/playwright-test/ui-mode-trace.spec.ts
@@ -314,7 +314,7 @@ test('should not fail on internal page logs', async ({ runUITest, server }) => {
/Before Hooks/,
/Create context/,
/Create page/,
- /Navigate to "\/empty.html"/,
+ /Navigate.*\/empty.html/,
/After Hooks/,
]);
});
@@ -492,12 +492,12 @@ test('should filter actions tab on double-click', async ({ runUITest, server })
const actionsTree = page.getByTestId('actions-tree');
await expect(actionsTree.getByRole('treeitem')).toHaveText([
/Before Hooks/,
- /Navigate to "\/empty.html"/,
+ /Navigate.*\/empty.html/,
/After Hooks/,
]);
- await actionsTree.getByRole('treeitem', { name: 'Navigate to "\/empty.html"' }).dblclick();
+ await actionsTree.getByRole('treeitem', { name: 'Navigate' }).dblclick();
await expect(actionsTree.getByRole('treeitem')).toHaveText([
- /Navigate to "\/empty.html"/,
+ /Navigate.*\/empty.html/,
]);
});
diff --git a/utils/generate_channels.js b/utils/generate_channels.js
index b0b44bb84647c..ded01189edca6 100755
--- a/utils/generate_channels.js
+++ b/utils/generate_channels.js
@@ -327,15 +327,33 @@ function generateChannels(target) {
throw new Error(`Method "${className}.${methodName}" must should not specify "group" because it is "internal" in protocol.yml`);
if (method.group && !['getter', 'configuration', 'route', 'default'].includes(method.group))
throw new Error(`Unknown group "${method.group}" for method "${className}.${methodName}" in protocol.yml`);
+ if (method.subtitle && method.internal)
+ throw new Error(`Method "${className}.${methodName}" should not specify "subtitle" because it is "internal" in protocol.yml`);
+ if (!method.subtitle && !method.internal && (method.parameters || {}).selector)
+ throw new Error(`Method "${className}.${methodName}" has a "selector" parameter, so it must have an explicit "subtitle" in protocol.yml`);
+ if (method.renderParams) {
+ if (method.internal)
+ throw new Error(`Method "${className}.${methodName}" should not specify "renderParams" because it is "internal" in protocol.yml`);
+ if (!Array.isArray(method.renderParams) || method.renderParams.some(entry => typeof entry !== 'string'))
+ throw new Error(`Method "${className}.${methodName}" must specify "renderParams" as a list of strings in protocol.yml`);
+ for (const entry of method.renderParams) {
+ // Each entry is "[key=]path[:selector]", path root must be a parameter.
+ const paramName = entry.split(':')[0].split('=').pop().split('.')[0];
+ if (!(method.parameters || {})[paramName])
+ throw new Error(`Method "${className}.${methodName}" renderParams entry "${entry}" does not match any parameter in protocol.yml`);
+ }
+ }
const internalProp = method.internal ? ` internal: ${method.internal},` : '';
const titleProp = method.title ? ` title: '${method.title}',` : '';
+ const subtitleProp = method.subtitle ? ` subtitle: '${method.subtitle}',` : '';
+ const renderParamsProp = method.renderParams ? ` renderParams: [${method.renderParams.map(entry => `'${entry}'`).join(', ')}],` : '';
const groupProp = method.group ? ` group: '${method.group}',` : '';
const slowMoProp = method.flags?.slowMo ? ` slowMo: ${method.flags.slowMo},` : '';
const snapshotProp = method.flags?.snapshot ? ` snapshot: ${method.flags.snapshot},` : '';
const pauseProp = method.flags?.pause ? ` pause: ${method.flags.pause},` : '';
const inputProp = method.flags?.input ? ` input: ${method.flags.input},` : '';
const isAutoWaitingProp = method.flags?.isAutoWaiting ? ` isAutoWaiting: ${method.flags.isAutoWaiting},` : '';
- methodMetainfo.push(`['${className + '.' + methodName}', {${internalProp}${titleProp}${slowMoProp}${snapshotProp}${pauseProp}${inputProp}${isAutoWaitingProp}${groupProp} }]`);
+ methodMetainfo.push(`['${className + '.' + methodName}', {${internalProp}${titleProp}${subtitleProp}${renderParamsProp}${slowMoProp}${snapshotProp}${pauseProp}${inputProp}${isAutoWaitingProp}${groupProp} }]`);
}
const parameters = objectType(method.parameters || {}, '');
@@ -432,7 +450,7 @@ const structs_ts = generateStructs();
const client_channels_ts = generateChannels('Channel');
-metainfo_ts.push(`export type MethodMetainfo = { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string };
+metainfo_ts.push(`export type MethodMetainfo = { internal?: boolean, title?: string, subtitle?: string, renderParams?: string[], slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string };
export const methodMetainfo = new Map([
${methodMetainfo.join(`,\n `)}
diff --git a/utils/generate_types/overrides-testReporter.d.ts b/utils/generate_types/overrides-testReporter.d.ts
index 6ca13fdcf2157..78dd048a9e554 100644
--- a/utils/generate_types/overrides-testReporter.d.ts
+++ b/utils/generate_types/overrides-testReporter.d.ts
@@ -132,6 +132,7 @@ export interface JSONReportTestResult {
export interface JSONReportTestStep {
title: string;
+ subtitle?: string;
duration: number;
error: TestError | undefined;
steps?: JSONReportTestStep[];