feat(trace-viewer): Render context string for most actions (#34292)
This commit is contained in:
parent
be6caed8df
commit
84bbc5fd35
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <>
|
||||
<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>
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue