chore: render typed locators in the trace viewer (#18166)

This commit is contained in:
Pavel Feldman 2022-10-18 22:23:40 -04:00 committed by GitHub
parent 11eb719d13
commit 1b541c9932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 49 additions and 15 deletions

View File

@ -100,6 +100,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
options: {},
platform: process.platform,
wallTime: 0,
sdkLanguage: (context as BrowserContext)?._browser?.options?.sdkLanguage,
};
if (context instanceof BrowserContext) {
this._snapshotter = new Snapshotter(context, this);
@ -112,6 +113,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
async start(options: TracerOptions) {
if (this._isStopping)
throw new Error('Cannot start tracing while stopping');
// Re-write for testing.
this._contextCreatedEvent.sdkLanguage = (this._context as BrowserContext)?._browser?.options?.sdkLanguage;
if (this._state) {
const o = this._state.options;
if (o.name !== options.name || !o.screenshots !== !options.screenshots || !o.snapshots !== !options.snapshots)

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import type { Language } from '../../playwright-core/src/server/isomorphic/locatorGenerators';
import type { ResourceSnapshot } from '@trace/snapshot';
import type * as trace from '@trace/trace';
@ -24,6 +25,7 @@ export type ContextEntry = {
browserName: string;
platform?: string;
wallTime?: number;
sdkLanguage?: Language;
title?: string;
options: trace.BrowserContextEventOptions;
pages: PageEntry[];

View File

@ -130,6 +130,7 @@ export class TraceModel {
this.contextEntry.title = event.title;
this.contextEntry.platform = event.platform;
this.contextEntry.wallTime = event.wallTime;
this.contextEntry.sdkLanguage = event.sdkLanguage;
this.contextEntry.options = event.options;
break;
}

View File

@ -1,4 +1,5 @@
[*]
@isomorphic/**
@web/**
../entries.ts
../geometry.ts

View File

@ -20,11 +20,14 @@ import * as React from 'react';
import './actionList.css';
import * as modelUtil from './modelUtil';
import './tabbedPane.css';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
export interface ActionListProps {
actions: ActionTraceEvent[],
selectedAction: ActionTraceEvent | undefined,
highlightedAction: ActionTraceEvent | undefined,
sdkLanguage: Language | undefined;
onSelected: (action: ActionTraceEvent) => void,
onHighlighted: (action: ActionTraceEvent | undefined) => void,
setSelectedTab: (tab: string) => void,
@ -32,8 +35,9 @@ export interface ActionListProps {
export const ActionList: React.FC<ActionListProps> = ({
actions = [],
selectedAction = undefined,
highlightedAction = undefined,
selectedAction,
highlightedAction,
sdkLanguage,
onSelected = () => {},
onHighlighted = () => {},
setSelectedTab = () => {},
@ -83,6 +87,7 @@ export const ActionList: React.FC<ActionListProps> = ({
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
const error = metadata.error?.error?.message;
const { errors, warnings } = modelUtil.stats(action);
const locator = metadata.params.selector ? asLocator(sdkLanguage || 'javascript', metadata.params.selector) : undefined;
return <div
className={'action-entry' + selectedSuffix + highlightedSuffix}
key={metadata.id}
@ -92,7 +97,7 @@ export const ActionList: React.FC<ActionListProps> = ({
>
<div className='action-title'>
<span>{metadata.apiName}</span>
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
</div>
<div className='action-duration' style={{ flex: 'none' }}>{metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}</div>

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import type { Language } from '@isomorphic/locatorGenerators';
import type { ResourceSnapshot } from '@trace/snapshot';
import type * as trace from '@trace/trace';
import type { ActionTraceEvent } from '@trace/trace';
@ -36,11 +37,13 @@ export class MultiTraceModel {
readonly actions: trace.ActionTraceEvent[];
readonly events: trace.ActionTraceEvent[];
readonly hasSource: boolean;
readonly sdkLanguage: Language | undefined;
constructor(contexts: ContextEntry[]) {
contexts.forEach(contextEntry => indexModel(contextEntry));
this.browserName = contexts[0]?.browserName || '';
this.sdkLanguage = contexts[0]?.sdkLanguage;
this.platform = contexts[0]?.platform || '';
this.title = contexts[0]?.title || '';
this.options = contexts[0]?.options || {};

View File

@ -175,6 +175,7 @@ export const Workbench: React.FunctionComponent<{
<TabbedPane tabs={
[
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
sdkLanguage={model.sdkLanguage}
actions={model.actions}
selectedAction={selectedAction}
highlightedAction={highlightedAction}

View File

@ -16,6 +16,7 @@
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@isomorphic/*": ["../playwright-core/src/server/isomorphic/*"],
"@protocol/*": ["../protocol/src/*"],
"@recorder/*": ["../recorder/src/*"],
"@trace/*": ["../trace/src/*"],

View File

@ -28,6 +28,7 @@ export default defineConfig({
],
resolve: {
alias: {
'@isomorphic': path.resolve(__dirname, '../playwright-core/src/server/isomorphic'),
'@protocol': path.resolve(__dirname, '../protocol/src'),
'@web': path.resolve(__dirname, '../web/src'),
},

View File

@ -15,6 +15,7 @@
*/
import type { CallMetadata } from '@protocol/callMetadata';
import type { Language } from '../../playwright-core/src/server/isomorphic/locatorGenerators';
import type { FrameSnapshot, ResourceSnapshot } from './snapshot';
export type Size = { width: number, height: number };
@ -36,7 +37,8 @@ export type ContextCreatedTraceEvent = {
platform: string,
wallTime: number,
title?: string,
options: BrowserContextEventOptions
options: BrowserContextEventOptions,
sdkLanguage?: Language,
};
export type ScreencastFrameTraceEvent = {

View File

@ -51,7 +51,7 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
await page.evaluate(() => 1 + 1, null);
async function doClick() {
await page.click('"Click"');
await page.getByText('Click').click();
}
await doClick();
@ -92,10 +92,10 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
/browserContext.newPage/,
/page.gotodata:text\/html,<html>Hello world<\/html>/,
/page.setContent/,
/expect.toHaveTextbutton/,
/expect.toHaveTextlocator\('button'\)/,
/page.evaluate/,
/page.evaluate/,
/page.click"Click"/,
/locator.clickgetByText\('Click'\)/,
/page.waitForNavigation/,
/page.waitForResponse/,
/page.waitForTimeout/,
@ -106,7 +106,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
test('should contain action info', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('page.click');
await traceViewer.selectAction('locator.click');
const logLines = await traceViewer.callLines.allTextContents();
expect(logLines.length).toBeGreaterThan(10);
expect(logLines).toContain('attempting click action');
@ -181,7 +181,7 @@ test('should have correct snapshot size', async ({ showTraceViewer }, testInfo)
test('should have correct stack trace', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('page.click');
await traceViewer.selectAction('locator.click');
await traceViewer.showSourceTab();
await expect(traceViewer.stackFrames).toContainText([
/doClick\s+trace-viewer.spec.ts\s+:\d+/,
@ -538,7 +538,7 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
test('should show action source', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('page.click');
await traceViewer.selectAction('locator.click');
const page = traceViewer.page;
await page.click('text=Source');
@ -546,7 +546,7 @@ test('should show action source', async ({ showTraceViewer }) => {
/async.*function.*doClick/,
/page\.click/
]);
await expect(page.locator('.source-line-running')).toContainText('page.click');
await expect(page.locator('.source-line-running')).toContainText('await page.getByText(\'Click\').click()');
await expect(page.locator('.stack-trace-frame.selected')).toHaveText(/doClick.*trace-viewer\.spec\.ts:[\d]+/);
});
@ -603,8 +603,8 @@ test('should open two trace files', async ({ context, page, request, server, sho
const response = await request.head(server.PREFIX + '/simplezip.json');
await expect(response).toBeOK();
}
await page.click('button');
await page.click('button');
await page.locator('button').click();
await page.locator('button').click();
{
const response = await request.post(server.PREFIX + '/one-style.css');
expect(response).toBeOK();
@ -623,8 +623,8 @@ test('should open two trace files', async ({ context, page, request, server, sho
`apiRequestContext.get`,
`page.gotohttp://localhost:${server.PORT}/input/button.html`,
`apiRequestContext.head`,
`page.clickbutton`,
`page.clickbutton`,
`locator.clicklocator('button')`,
`locator.clicklocator('button')`,
`apiRequestContext.post`,
]);
@ -729,3 +729,15 @@ test('should display waitForLoadState even if did not wait for it', async ({ run
/page.waitForLoadState/,
]);
});
test('should display language-specific locators', async ({ runAndTrace, server, page, toImpl }) => {
toImpl(page.context())._browser.options.sdkLanguage = 'python';
const traceViewer = await runAndTrace(async () => {
await page.setContent('<button>Submit</button>');
await page.getByRole('button', { name: 'Submit' }).click();
});
await expect(traceViewer.actionTitles).toHaveText([
/page.setContent/,
/locator.clickget_by_role\("button", name="Submit"\)/,
]);
});