chore: render typed locators in the trace viewer (#18166)
This commit is contained in:
Родитель
11eb719d13
Коммит
1b541c9932
|
@ -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)
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
[*]
|
||||
@isomorphic/**
|
||||
@web/**
|
||||
../entries.ts
|
||||
../geometry.ts
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 || {};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@isomorphic/*": ["../playwright-core/src/server/isomorphic/*"],
|
||||
"@protocol/*": ["../protocol/src/*"],
|
||||
"@recorder/*": ["../recorder/src/*"],
|
||||
"@trace/*": ["../trace/src/*"],
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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"\)/,
|
||||
]);
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче