Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(trace-viewer): Render context string for most actions #34292

Merged
merged 17 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/trace-viewer/src/ui/actionList.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
175 changes: 171 additions & 4 deletions packages/trace-viewer/src/ui/actionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -129,7 +129,23 @@ export const renderAction = (
return <>
<div className='action-title' title={action.apiName}>
<span>{action.apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{parameterString &&
(parameterString.type === 'locator' ? (
<>
<span className='action-parameter action-locator-parameter'>
{parameterString.value}
</span>
{parameterString.childDisplayString && (
<span className='action-parameter action-generic-parameter'>
{parameterString.childDisplayString.value}
</span>
)}
</>
) : (
<span className='action-parameter action-generic-parameter'>
{parameterString.value}
</span>
))}
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
{action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>}
</div>
Expand All @@ -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 (
agg23 marked this conversation as resolved.
Show resolved Hide resolved
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;
};
55 changes: 55 additions & 0 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<input type="text" />');
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();
Expand Down
Loading