diff --git a/packages/trace-viewer/src/ui/actionList.css b/packages/trace-viewer/src/ui/actionList.css
index 10e3c39f98522..0cb3cb5f5469f 100644
--- a/packages/trace-viewer/src/ui/actionList.css
+++ b/packages/trace-viewer/src/ui/actionList.css
@@ -70,13 +70,20 @@
flex: none;
}
-.action-selector {
+.action-parameter {
display: inline;
flex: none;
padding-left: 5px;
+}
+
+.action-locator-parameter {
color: var(--vscode-charts-orange);
}
+.action-generic-parameter {
+ color: var(--vscode-charts-purple);
+}
+
.action-url {
display: inline;
flex: none;
diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx
index 1deb8ecd88c9a..87e78416b0fb0 100644
--- a/packages/trace-viewer/src/ui/actionList.tsx
+++ b/packages/trace-viewer/src/ui/actionList.tsx
@@ -19,8 +19,7 @@ import { msToString } from '@web/uiUtils';
import * as React from 'react';
import './actionList.css';
import * as modelUtil from './modelUtil';
-import { asLocator } from '@isomorphic/locatorGenerators';
-import type { Language } from '@isomorphic/locatorGenerators';
+import { asLocator, type Language } from '@isomorphic/locatorGenerators';
import type { TreeState } from '@web/components/treeView';
import { TreeView } from '@web/components/treeView';
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
@@ -116,9 +115,10 @@ export const renderAction = (
}) => {
const { sdkLanguage, revealConsole, revealAttachment, isLive, showDuration, showBadges } = options;
const { errors, warnings } = modelUtil.stats(action);
- const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector) : undefined;
const showAttachments = !!action.attachments?.length && !!revealAttachment;
+ const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
+
let time: string = '';
if (action.endTime)
time = msToString(action.endTime - action.startTime);
@@ -129,7 +129,23 @@ export const renderAction = (
return <>
{action.apiName}
- {locator &&
{locator}
}
+ {parameterString &&
+ (parameterString.type === 'locator' ? (
+ <>
+
+ {parameterString.value}
+
+ {parameterString.childDisplayString && (
+
+ {parameterString.childDisplayString.value}
+
+ )}
+ >
+ ) : (
+
+ {parameterString.value}
+
+ ))}
{action.method === 'goto' && action.params.url &&
{action.params.url}
}
{action.class === 'APIRequestContext' && action.params.url &&
{excludeOrigin(action.params.url)}
}
@@ -151,3 +167,154 @@ function excludeOrigin(url: string): string {
return url;
}
}
+
+type ActionParameterDisplayString =
+ | {
+ type: 'generic';
+ value: string;
+ }
+ | {
+ type: 'locator';
+ value: string;
+ childDisplayString?: ActionParameterDisplayString;
+ };
+
+const clockDisplayString = (
+ action: ActionTraceEvent,
+): ActionParameterDisplayString | undefined => {
+ switch (action.method) {
+ case 'clockPauseAt':
+ case 'clockSetFixedTime':
+ case 'clockSetSystemTime': {
+ if (
+ action.params.timeString === undefined &&
+ action.params.timeNumber === undefined
+ )
+ return undefined;
+ return {
+ type: 'generic',
+ value: new Date(
+ action.params.timeString ?? action.params.timeNumber,
+ ).toLocaleString(undefined, { timeZone: 'UTC' }),
+ };
+ }
+ case 'clockFastForward':
+ case 'clockRunFor': {
+ if (
+ action.params.ticksNumber === undefined &&
+ action.params.ticksString === undefined
+ )
+ return undefined;
+ return {
+ type: 'generic',
+ value: action.params.ticksString ?? `${action.params.ticksNumber}ms`,
+ };
+ }
+ }
+
+ return undefined;
+};
+
+const keyboardDisplayString = (
+ action: ActionTraceEvent,
+): ActionParameterDisplayString | undefined => {
+ switch (action.method) {
+ case 'press':
+ case 'keyboardPress':
+ case 'keyboardDown':
+ case 'keyboardUp': {
+ if (action.params.key === undefined)
+ return undefined;
+ return { type: 'generic', value: action.params.key };
+ }
+ case 'type':
+ case 'fill':
+ case 'keyboardType':
+ case 'keyboardInsertText': {
+ const string = action.params.text ?? action.params.value;
+ if (string === undefined)
+ return undefined;
+ return { type: 'generic', value: `"${string}"` };
+ }
+ }
+};
+
+const mouseDisplayString = (
+ action: ActionTraceEvent,
+): ActionParameterDisplayString | undefined => {
+ switch (action.method) {
+ case 'click':
+ case 'dblclick':
+ case 'mouseClick':
+ case 'mouseMove': {
+ if (action.params.x === undefined || action.params.y === undefined)
+ return undefined;
+ return {
+ type: 'generic',
+ value: `(${action.params.x}, ${action.params.y})`,
+ };
+ }
+ case 'mouseWheel': {
+ if (
+ action.params.deltaX === undefined ||
+ action.params.deltaY === undefined
+ )
+ return undefined;
+ return {
+ type: 'generic',
+ value: `(${action.params.deltaX}, ${action.params.deltaY})`,
+ };
+ }
+ }
+};
+
+const touchscreenDisplayString = (
+ action: ActionTraceEvent,
+): ActionParameterDisplayString | undefined => {
+ switch (action.method) {
+ case 'tap': {
+ if (action.params.x === undefined || action.params.y === undefined)
+ return undefined;
+ return {
+ type: 'generic',
+ value: `(${action.params.x}, ${action.params.y})`,
+ };
+ }
+ }
+};
+
+const actionParameterDisplayString = (
+ action: ActionTraceEvent,
+ sdkLanguage: Language,
+ ignoreLocator: boolean = false,
+): ActionParameterDisplayString | undefined => {
+ const params = action.params;
+
+ // Locators have many possible classes, so follow existing logic and use `selector` presence
+ if (!ignoreLocator && params.selector !== undefined) {
+ return {
+ type: 'locator',
+ value: asLocator(sdkLanguage, params.selector),
+ childDisplayString: actionParameterDisplayString(
+ action,
+ sdkLanguage,
+ true,
+ ),
+ };
+ }
+
+ switch (action.class.toLowerCase()) {
+ case 'browsercontext':
+ return clockDisplayString(action);
+ case 'page':
+ case 'frame':
+ case 'elementhandle':
+ return (
+ keyboardDisplayString(action) ??
+ mouseDisplayString(action) ??
+ touchscreenDisplayString(action)
+ );
+ }
+
+ return undefined;
+};
diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts
index 71ecfe0764ad3..9bb67884f5db4 100644
--- a/tests/library/trace-viewer.spec.ts
+++ b/tests/library/trace-viewer.spec.ts
@@ -166,6 +166,61 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
]);
});
+test('should show action context on locators and other common actions', async ({
+ runAndTrace,
+ page,
+}) => {
+ const traceViewer = await runAndTrace(async () => {
+ await page.setContent('');
+ await page.locator('input').click({ button: 'right' });
+ await page.getByRole('textbox').click();
+ await expect(page.locator('input')).toHaveText('');
+ await page.locator('input').press('Enter');
+ await page.keyboard.type(
+ 'Hello world this is a very long string what happens when it overflows?',
+ );
+ await page.keyboard.press('Control+c');
+ await page.keyboard.down('Shift');
+ await page.keyboard.insertText('Hello world');
+ await page.keyboard.up('Shift');
+ await page.mouse.move(0, 0);
+ await page.mouse.down();
+ await page.mouse.move(100, 200);
+ await page.mouse.wheel(5, 7);
+ await page.mouse.up();
+ await page.clock.fastForward(1000);
+ await page.clock.fastForward('30:00');
+ await page.clock.pauseAt(new Date('2020-02-02T00:00:00Z'));
+ await page.clock.runFor(10);
+ await page.clock.setFixedTime(new Date('2020-02-02T00:00:00Z'));
+ await page.clock.setSystemTime(new Date('2020-02-02T00:00:00Z'));
+ });
+
+ await expect(traceViewer.actionTitles).toHaveText([
+ /page.setContent/,
+ /locator.clicklocator\('input'\)/,
+ /locator.clickgetByRole\('textbox'\)/,
+ /expect.toHaveTextlocator\('input'\)/,
+ /locator.presslocator\('input'\)Enter/,
+ /keyboard.type\"Hello world this is a very long string what happens when it overflows\?\"/,
+ /keyboard.pressControl\+c/,
+ /keyboard.downShift/,
+ /keyboard.insertText\"Hello world\"/,
+ /keyboard.upShift/,
+ /mouse.move\(0, 0\)/,
+ /mouse.down/,
+ /mouse.move\(100, 200\)/,
+ /mouse.wheel\(5, 7\)/,
+ /mouse.up/,
+ /clock.fastForward1000ms/,
+ /clock.fastForward30:00/,
+ /clock.pauseAt2\/2\/2020, 12:00:00 AM/,
+ /clock.runFor10ms/,
+ /clock.setFixedTime2\/2\/2020, 12:00:00 AM/,
+ /clock.setSystemTime2\/2\/2020, 12:00:00 AM/,
+ ]);
+});
+
test('should complain about newer version of trace in old viewer', async ({ showTraceViewer, asset }, testInfo) => {
const traceViewer = await showTraceViewer([asset('trace-from-the-future.zip')]);
await expect(traceViewer.page.getByText('The trace was created by a newer version of Playwright and is not supported by this version of the viewer.')).toBeVisible();