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();