diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index a48c2974b9..6b4bad5339 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -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) diff --git a/packages/trace-viewer/src/entries.ts b/packages/trace-viewer/src/entries.ts index 152e16f4d0..5faf5f7fea 100644 --- a/packages/trace-viewer/src/entries.ts +++ b/packages/trace-viewer/src/entries.ts @@ -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[]; diff --git a/packages/trace-viewer/src/traceModel.ts b/packages/trace-viewer/src/traceModel.ts index a9baa29baf..cfa1a1a705 100644 --- a/packages/trace-viewer/src/traceModel.ts +++ b/packages/trace-viewer/src/traceModel.ts @@ -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; } diff --git a/packages/trace-viewer/src/ui/DEPS.list b/packages/trace-viewer/src/ui/DEPS.list index a55f4b2e25..9eb15c9952 100644 --- a/packages/trace-viewer/src/ui/DEPS.list +++ b/packages/trace-viewer/src/ui/DEPS.list @@ -1,4 +1,5 @@ [*] +@isomorphic/** @web/** ../entries.ts ../geometry.ts diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index edf1c28c7c..fc203976d9 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -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 = ({ actions = [], - selectedAction = undefined, - highlightedAction = undefined, + selectedAction, + highlightedAction, + sdkLanguage, onSelected = () => {}, onHighlighted = () => {}, setSelectedTab = () => {}, @@ -83,6 +87,7 @@ export const ActionList: React.FC = ({ 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
= ({ >
{metadata.apiName} - {metadata.params.selector &&
{metadata.params.selector}
} + {locator &&
{locator}
} {metadata.method === 'goto' && metadata.params.url &&
{metadata.params.url}
}
{metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}
diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index f2c752d61b..4f5ab5bf15 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -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 || {}; diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 49292781f7..6d25212c8b 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -175,6 +175,7 @@ export const Workbench: React.FunctionComponent<{ 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,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(''); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + await expect(traceViewer.actionTitles).toHaveText([ + /page.setContent/, + /locator.clickget_by_role\("button", name="Submit"\)/, + ]); +});