chore: render typed locators in the trace viewer (#18166)
This commit is contained in:
parent
11eb719d13
commit
1b541c9932
|
|
@ -100,6 +100,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
options: {},
|
options: {},
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
wallTime: 0,
|
wallTime: 0,
|
||||||
|
sdkLanguage: (context as BrowserContext)?._browser?.options?.sdkLanguage,
|
||||||
};
|
};
|
||||||
if (context instanceof BrowserContext) {
|
if (context instanceof BrowserContext) {
|
||||||
this._snapshotter = new Snapshotter(context, this);
|
this._snapshotter = new Snapshotter(context, this);
|
||||||
|
|
@ -112,6 +113,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
async start(options: TracerOptions) {
|
async start(options: TracerOptions) {
|
||||||
if (this._isStopping)
|
if (this._isStopping)
|
||||||
throw new Error('Cannot start tracing while stopping');
|
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) {
|
if (this._state) {
|
||||||
const o = this._state.options;
|
const o = this._state.options;
|
||||||
if (o.name !== options.name || !o.screenshots !== !options.screenshots || !o.snapshots !== !options.snapshots)
|
if (o.name !== options.name || !o.screenshots !== !options.screenshots || !o.snapshots !== !options.snapshots)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Language } from '../../playwright-core/src/server/isomorphic/locatorGenerators';
|
||||||
import type { ResourceSnapshot } from '@trace/snapshot';
|
import type { ResourceSnapshot } from '@trace/snapshot';
|
||||||
import type * as trace from '@trace/trace';
|
import type * as trace from '@trace/trace';
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ export type ContextEntry = {
|
||||||
browserName: string;
|
browserName: string;
|
||||||
platform?: string;
|
platform?: string;
|
||||||
wallTime?: number;
|
wallTime?: number;
|
||||||
|
sdkLanguage?: Language;
|
||||||
title?: string;
|
title?: string;
|
||||||
options: trace.BrowserContextEventOptions;
|
options: trace.BrowserContextEventOptions;
|
||||||
pages: PageEntry[];
|
pages: PageEntry[];
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ export class TraceModel {
|
||||||
this.contextEntry.title = event.title;
|
this.contextEntry.title = event.title;
|
||||||
this.contextEntry.platform = event.platform;
|
this.contextEntry.platform = event.platform;
|
||||||
this.contextEntry.wallTime = event.wallTime;
|
this.contextEntry.wallTime = event.wallTime;
|
||||||
|
this.contextEntry.sdkLanguage = event.sdkLanguage;
|
||||||
this.contextEntry.options = event.options;
|
this.contextEntry.options = event.options;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
[*]
|
[*]
|
||||||
|
@isomorphic/**
|
||||||
@web/**
|
@web/**
|
||||||
../entries.ts
|
../entries.ts
|
||||||
../geometry.ts
|
../geometry.ts
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,14 @@ import * as React from 'react';
|
||||||
import './actionList.css';
|
import './actionList.css';
|
||||||
import * as modelUtil from './modelUtil';
|
import * as modelUtil from './modelUtil';
|
||||||
import './tabbedPane.css';
|
import './tabbedPane.css';
|
||||||
|
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||||
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
|
|
||||||
export interface ActionListProps {
|
export interface ActionListProps {
|
||||||
actions: ActionTraceEvent[],
|
actions: ActionTraceEvent[],
|
||||||
selectedAction: ActionTraceEvent | undefined,
|
selectedAction: ActionTraceEvent | undefined,
|
||||||
highlightedAction: ActionTraceEvent | undefined,
|
highlightedAction: ActionTraceEvent | undefined,
|
||||||
|
sdkLanguage: Language | undefined;
|
||||||
onSelected: (action: ActionTraceEvent) => void,
|
onSelected: (action: ActionTraceEvent) => void,
|
||||||
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
||||||
setSelectedTab: (tab: string) => void,
|
setSelectedTab: (tab: string) => void,
|
||||||
|
|
@ -32,8 +35,9 @@ export interface ActionListProps {
|
||||||
|
|
||||||
export const ActionList: React.FC<ActionListProps> = ({
|
export const ActionList: React.FC<ActionListProps> = ({
|
||||||
actions = [],
|
actions = [],
|
||||||
selectedAction = undefined,
|
selectedAction,
|
||||||
highlightedAction = undefined,
|
highlightedAction,
|
||||||
|
sdkLanguage,
|
||||||
onSelected = () => {},
|
onSelected = () => {},
|
||||||
onHighlighted = () => {},
|
onHighlighted = () => {},
|
||||||
setSelectedTab = () => {},
|
setSelectedTab = () => {},
|
||||||
|
|
@ -83,6 +87,7 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||||
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
|
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
|
||||||
const error = metadata.error?.error?.message;
|
const error = metadata.error?.error?.message;
|
||||||
const { errors, warnings } = modelUtil.stats(action);
|
const { errors, warnings } = modelUtil.stats(action);
|
||||||
|
const locator = metadata.params.selector ? asLocator(sdkLanguage || 'javascript', metadata.params.selector) : undefined;
|
||||||
return <div
|
return <div
|
||||||
className={'action-entry' + selectedSuffix + highlightedSuffix}
|
className={'action-entry' + selectedSuffix + highlightedSuffix}
|
||||||
key={metadata.id}
|
key={metadata.id}
|
||||||
|
|
@ -92,7 +97,7 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||||
>
|
>
|
||||||
<div className='action-title'>
|
<div className='action-title'>
|
||||||
<span>{metadata.apiName}</span>
|
<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>}
|
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className='action-duration' style={{ flex: 'none' }}>{metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}</div>
|
<div className='action-duration' style={{ flex: 'none' }}>{metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}</div>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
import type { ResourceSnapshot } from '@trace/snapshot';
|
import type { ResourceSnapshot } from '@trace/snapshot';
|
||||||
import type * as trace from '@trace/trace';
|
import type * as trace from '@trace/trace';
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
import type { ActionTraceEvent } from '@trace/trace';
|
||||||
|
|
@ -36,11 +37,13 @@ export class MultiTraceModel {
|
||||||
readonly actions: trace.ActionTraceEvent[];
|
readonly actions: trace.ActionTraceEvent[];
|
||||||
readonly events: trace.ActionTraceEvent[];
|
readonly events: trace.ActionTraceEvent[];
|
||||||
readonly hasSource: boolean;
|
readonly hasSource: boolean;
|
||||||
|
readonly sdkLanguage: Language | undefined;
|
||||||
|
|
||||||
constructor(contexts: ContextEntry[]) {
|
constructor(contexts: ContextEntry[]) {
|
||||||
contexts.forEach(contextEntry => indexModel(contextEntry));
|
contexts.forEach(contextEntry => indexModel(contextEntry));
|
||||||
|
|
||||||
this.browserName = contexts[0]?.browserName || '';
|
this.browserName = contexts[0]?.browserName || '';
|
||||||
|
this.sdkLanguage = contexts[0]?.sdkLanguage;
|
||||||
this.platform = contexts[0]?.platform || '';
|
this.platform = contexts[0]?.platform || '';
|
||||||
this.title = contexts[0]?.title || '';
|
this.title = contexts[0]?.title || '';
|
||||||
this.options = contexts[0]?.options || {};
|
this.options = contexts[0]?.options || {};
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
<TabbedPane tabs={
|
<TabbedPane tabs={
|
||||||
[
|
[
|
||||||
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
|
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
|
||||||
|
sdkLanguage={model.sdkLanguage}
|
||||||
actions={model.actions}
|
actions={model.actions}
|
||||||
selectedAction={selectedAction}
|
selectedAction={selectedAction}
|
||||||
highlightedAction={highlightedAction}
|
highlightedAction={highlightedAction}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"@isomorphic/*": ["../playwright-core/src/server/isomorphic/*"],
|
||||||
"@protocol/*": ["../protocol/src/*"],
|
"@protocol/*": ["../protocol/src/*"],
|
||||||
"@recorder/*": ["../recorder/src/*"],
|
"@recorder/*": ["../recorder/src/*"],
|
||||||
"@trace/*": ["../trace/src/*"],
|
"@trace/*": ["../trace/src/*"],
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export default defineConfig({
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
'@isomorphic': path.resolve(__dirname, '../playwright-core/src/server/isomorphic'),
|
||||||
'@protocol': path.resolve(__dirname, '../protocol/src'),
|
'@protocol': path.resolve(__dirname, '../protocol/src'),
|
||||||
'@web': path.resolve(__dirname, '../web/src'),
|
'@web': path.resolve(__dirname, '../web/src'),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CallMetadata } from '@protocol/callMetadata';
|
import type { CallMetadata } from '@protocol/callMetadata';
|
||||||
|
import type { Language } from '../../playwright-core/src/server/isomorphic/locatorGenerators';
|
||||||
import type { FrameSnapshot, ResourceSnapshot } from './snapshot';
|
import type { FrameSnapshot, ResourceSnapshot } from './snapshot';
|
||||||
|
|
||||||
export type Size = { width: number, height: number };
|
export type Size = { width: number, height: number };
|
||||||
|
|
@ -36,7 +37,8 @@ export type ContextCreatedTraceEvent = {
|
||||||
platform: string,
|
platform: string,
|
||||||
wallTime: number,
|
wallTime: number,
|
||||||
title?: string,
|
title?: string,
|
||||||
options: BrowserContextEventOptions
|
options: BrowserContextEventOptions,
|
||||||
|
sdkLanguage?: Language,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScreencastFrameTraceEvent = {
|
export type ScreencastFrameTraceEvent = {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
|
||||||
await page.evaluate(() => 1 + 1, null);
|
await page.evaluate(() => 1 + 1, null);
|
||||||
|
|
||||||
async function doClick() {
|
async function doClick() {
|
||||||
await page.click('"Click"');
|
await page.getByText('Click').click();
|
||||||
}
|
}
|
||||||
await doClick();
|
await doClick();
|
||||||
|
|
||||||
|
|
@ -92,10 +92,10 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
/browserContext.newPage/,
|
/browserContext.newPage/,
|
||||||
/page.gotodata:text\/html,<html>Hello world<\/html>/,
|
/page.gotodata:text\/html,<html>Hello world<\/html>/,
|
||||||
/page.setContent/,
|
/page.setContent/,
|
||||||
/expect.toHaveTextbutton/,
|
/expect.toHaveTextlocator\('button'\)/,
|
||||||
/page.evaluate/,
|
/page.evaluate/,
|
||||||
/page.evaluate/,
|
/page.evaluate/,
|
||||||
/page.click"Click"/,
|
/locator.clickgetByText\('Click'\)/,
|
||||||
/page.waitForNavigation/,
|
/page.waitForNavigation/,
|
||||||
/page.waitForResponse/,
|
/page.waitForResponse/,
|
||||||
/page.waitForTimeout/,
|
/page.waitForTimeout/,
|
||||||
|
|
@ -106,7 +106,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
|
|
||||||
test('should contain action info', async ({ showTraceViewer }) => {
|
test('should contain action info', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('page.click');
|
await traceViewer.selectAction('locator.click');
|
||||||
const logLines = await traceViewer.callLines.allTextContents();
|
const logLines = await traceViewer.callLines.allTextContents();
|
||||||
expect(logLines.length).toBeGreaterThan(10);
|
expect(logLines.length).toBeGreaterThan(10);
|
||||||
expect(logLines).toContain('attempting click action');
|
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 }) => {
|
test('should have correct stack trace', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
|
|
||||||
await traceViewer.selectAction('page.click');
|
await traceViewer.selectAction('locator.click');
|
||||||
await traceViewer.showSourceTab();
|
await traceViewer.showSourceTab();
|
||||||
await expect(traceViewer.stackFrames).toContainText([
|
await expect(traceViewer.stackFrames).toContainText([
|
||||||
/doClick\s+trace-viewer.spec.ts\s+:\d+/,
|
/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 }) => {
|
test('should show action source', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('page.click');
|
await traceViewer.selectAction('locator.click');
|
||||||
const page = traceViewer.page;
|
const page = traceViewer.page;
|
||||||
|
|
||||||
await page.click('text=Source');
|
await page.click('text=Source');
|
||||||
|
|
@ -546,7 +546,7 @@ test('should show action source', async ({ showTraceViewer }) => {
|
||||||
/async.*function.*doClick/,
|
/async.*function.*doClick/,
|
||||||
/page\.click/
|
/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]+/);
|
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');
|
const response = await request.head(server.PREFIX + '/simplezip.json');
|
||||||
await expect(response).toBeOK();
|
await expect(response).toBeOK();
|
||||||
}
|
}
|
||||||
await page.click('button');
|
await page.locator('button').click();
|
||||||
await page.click('button');
|
await page.locator('button').click();
|
||||||
{
|
{
|
||||||
const response = await request.post(server.PREFIX + '/one-style.css');
|
const response = await request.post(server.PREFIX + '/one-style.css');
|
||||||
expect(response).toBeOK();
|
expect(response).toBeOK();
|
||||||
|
|
@ -623,8 +623,8 @@ test('should open two trace files', async ({ context, page, request, server, sho
|
||||||
`apiRequestContext.get`,
|
`apiRequestContext.get`,
|
||||||
`page.gotohttp://localhost:${server.PORT}/input/button.html`,
|
`page.gotohttp://localhost:${server.PORT}/input/button.html`,
|
||||||
`apiRequestContext.head`,
|
`apiRequestContext.head`,
|
||||||
`page.clickbutton`,
|
`locator.clicklocator('button')`,
|
||||||
`page.clickbutton`,
|
`locator.clicklocator('button')`,
|
||||||
`apiRequestContext.post`,
|
`apiRequestContext.post`,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -729,3 +729,15 @@ test('should display waitForLoadState even if did not wait for it', async ({ run
|
||||||
/page.waitForLoadState/,
|
/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"\)/,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue