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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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-context {
display: inline;
flex: none;
padding-left: 5px;
}

.action-locator-context {
color: var(--vscode-charts-orange);
}

.action-generic-context {
color: var(--vscode-charts-purple);
}

.action-url {
display: inline;
flex: none;
Expand Down
30 changes: 26 additions & 4 deletions packages/trace-viewer/src/ui/actionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
*/

import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace';
import { msToString } from '@web/uiUtils';
import { clsx, 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 type { TreeState } from '@web/components/treeView';
import { TreeView } from '@web/components/treeView';
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
import type { Boundaries } from './geometry';
import { ToolbarButton } from '@web/components/toolbarButton';
import { actionContextString } from './string';

export interface ActionListProps {
actions: ActionTraceEventInContext[],
Expand Down Expand Up @@ -104,6 +104,29 @@ export const ActionList: React.FC<ActionListProps> = ({
</div>;
};

const ActionContext: React.FC<{
agg23 marked this conversation as resolved.
Show resolved Hide resolved
action: ActionTraceEvent;
sdkLanguage: Language;
}> = ({ action, sdkLanguage }) => {
const contextString = actionContextString(action, sdkLanguage);

if (contextString === undefined)
return null;

return (
<div
className={clsx(
'action-context',
action.apiName.startsWith('locator')
? 'action-locator-context'
: 'action-generic-context',
)}
>
{contextString}
</div>
);
};

export const renderAction = (
action: ActionTraceEvent,
options: {
Expand All @@ -116,7 +139,6 @@ 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;

let time: string = '';
Expand All @@ -129,7 +151,7 @@ export const renderAction = (
return <>
<div className='action-title' title={action.apiName}>
<span>{action.apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
<ActionContext action={action} sdkLanguage={sdkLanguage || 'javascript'} />
{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 Down
119 changes: 119 additions & 0 deletions packages/trace-viewer/src/ui/string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { ActionTraceEvent } from '@trace/trace';
import { asLocator, type Language } from '@isomorphic/locatorGenerators';

const formatClockParams = (params: {
ticksNumber?: number;
ticksString?: string;
timeNumber?: number;
}): string | undefined => {
if (params.ticksNumber !== undefined) {
// clock.fastForward/runFor
return `${params.ticksNumber}ms`;
} else if (params.ticksString !== undefined) {
// clock.fastForward/runFor
return params.ticksString;
} else if (params.timeNumber !== undefined) {
// clock.pauseAt/setFixedTime/setSystemTime
try {
return new Date(params.timeNumber).toLocaleString(undefined, {
timeZone: 'UTC',
});
} catch (e) {
return undefined;
}
}

return undefined;
};

const formatLocatorParams = (
sdkLanguage: Language,
params: { selector?: string },
): string | undefined =>
params.selector !== undefined
? asLocator(sdkLanguage, params.selector)
: undefined;

const formatKeyboardParams = (params: {
key?: string;
text?: string;
}): string | undefined => {
if (params.key !== undefined) {
// keyboard.press/down/up
return params.key;
} else if (params.text !== undefined) {
// keyboard.type/insertText
return `"${params.text}"`;
}

return undefined;
};

const formatMouseParams = (params: {
x?: number;
y?: number;
deltaX?: number;
deltaY?: number;
}): string | undefined => {
if (params.x !== undefined && params.y !== undefined) {
// mouse.click/dblclick/move
return `(${params.x}, ${params.y})`;
} else if (params.deltaX !== undefined && params.deltaY !== undefined) {
// mouse.wheel
return `(${params.deltaX}, ${params.deltaY})`;
}

return undefined;
};

const formatTouchscreenParams = (params: {
x?: number;
y?: number;
}): string | undefined => {
if (params.x && params.y) {
// touchscreen.tap
return `(${params.x}, ${params.y})`;
}

return undefined;
};

export const actionContextString = (
action: ActionTraceEvent,
sdkLanguage: Language,
): string | undefined => {
const params = action.params;
const apiName = action.apiName;

switch (true) {
case apiName.startsWith('clock'):
agg23 marked this conversation as resolved.
Show resolved Hide resolved
return formatClockParams(params);
case apiName.startsWith('keyboard'):
return formatKeyboardParams(params);
case apiName.startsWith('locator'):
case apiName.startsWith('expect'):
return formatLocatorParams(sdkLanguage, params);
case apiName.startsWith('mouse'):
return formatMouseParams(params);
case apiName.startsWith('touchscreen'):
return formatTouchscreenParams(params);
default:
return undefined;
}
};
51 changes: 51 additions & 0 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,57 @@ 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 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'\)/,
/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