diff --git a/docs/src/test-reporter-api/class-teststep.md b/docs/src/test-reporter-api/class-teststep.md index 8ff66f4be55d1..01a3962543d66 100644 --- a/docs/src/test-reporter-api/class-teststep.md +++ b/docs/src/test-reporter-api/class-teststep.md @@ -104,7 +104,24 @@ The list of files or buffers attached in the step execution through [`method: Te * since: v1.10 - type: <[string]> -User-friendly test step title. +User-friendly test step title, for example `Click` or `Navigate`. + +## property: TestStep.subtitle +* since: v1.63 +- type: ?<[string]> + +User-friendly test step subtitle that complements the title, when available. For Playwright API +calls, it is the target locator or the navigation url. For example, a `Click` step has the clicked +locator as a subtitle. User interfaces typically render the subtitle next to the title or on a +separate line. + +```js +// title `Click`, subtitle `getByRole('button')` +await page.getByRole('button').click(); + +// title `Navigate`, subtitle `/index.html` +await page.goto('https://example.com/index.html'); +``` ## method: TestStep.titlePath * since: v1.10 diff --git a/packages/html-reporter/src/testResultView.css b/packages/html-reporter/src/testResultView.css index fd1fb525b79ae..944c29977cbfa 100644 --- a/packages/html-reporter/src/testResultView.css +++ b/packages/html-reporter/src/testResultView.css @@ -69,6 +69,10 @@ background: var(--color-attention-subtle); } +.step-subtitle { + color: var(--color-fg-muted); +} + .step-spacer { flex: auto; } diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 867ac1d41ddfc..9844e2810384f 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -222,13 +222,29 @@ function pickDiffForError(error: string, diffs: ImageDiff[]): ImageDiff | undefi } function stepMatchesFilter(step: TestStep, filterText: string): boolean { - return step.title.toLowerCase().includes(filterText.toLowerCase()); + const text = step.subtitle ? `${step.title} ${step.subtitle}` : step.title; + return text.toLowerCase().includes(filterText.toLowerCase()); } function stepChildrenMatchFilter(step: TestStep, filterText: string): boolean { return step.steps.some(s => stepMatchesFilter(s, filterText) || stepChildrenMatchFilter(s, filterText)); } +function highlightFilterText(text: string, filterText: string): React.ReactNode[] { + const unmatched = text.toLowerCase().split(filterText.toLowerCase()); + const parts: React.ReactNode[] = []; + let index = 0; + for (let i = 0; i < unmatched.length; i++) { + if (i) { + parts.push({text.substring(index, index + filterText.length)}); + index += filterText.length; + } + parts.push(text.substring(index, index + unmatched[i].length)); + index += unmatched[i].length; + } + return parts; +} + function stepHasDescendantAttachments(step: TestStep): boolean { return step.steps.some(s => s.attachments.length > 0 || stepHasDescendantAttachments(s)); } @@ -252,6 +268,7 @@ const StepTreeItem: React.FC<{ let expandByDefault = false; let title: React.ReactNode = {step.title}; + let subtitle: React.ReactNode = step.subtitle; if (filterText) { const matchesFilter = !!filterText && stepMatchesFilter(step, filterText); @@ -260,25 +277,17 @@ const StepTreeItem: React.FC<{ return null; expandByDefault = childrenMatchFilter; if (matchesFilter) { - const unmatched = step.title.toLowerCase().split(filterText.toLowerCase()); - const parts: React.ReactNode[] = []; - let index = 0; - for (let i = 0; i < unmatched.length; i++) { - if (i) { - parts.push({step.title.substring(index, index + filterText.length)}); - index += filterText.length; - } - parts.push(unmatched[i]); - index += unmatched[i].length; - } - title = parts; + title = highlightFilterText(step.title, filterText); + if (step.subtitle) + subtitle = highlightFilterText(step.subtitle, filterText); } } - return + return {statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))} {title} + {step.subtitle && {subtitle}} {step.count > 1 && <> ✕ {step.count}} {step.location && — {step.location.file}:{step.location.line}} diff --git a/packages/html-reporter/src/types.d.ts b/packages/html-reporter/src/types.d.ts index 62bab6c0e3a4c..2c89a04690cc9 100644 --- a/packages/html-reporter/src/types.d.ts +++ b/packages/html-reporter/src/types.d.ts @@ -119,6 +119,7 @@ export type TestResult = { export type TestStep = { title: string; + subtitle?: string; startTime: string; duration: number; location?: Location; diff --git a/packages/isomorphic/protocolFormatter.ts b/packages/isomorphic/protocolFormatter.ts index 188d6690d3e05..84670af6bec9e 100644 --- a/packages/isomorphic/protocolFormatter.ts +++ b/packages/isomorphic/protocolFormatter.ts @@ -15,12 +15,24 @@ */ import { getMetainfo } from './protocolMetainfo'; +import { asLocatorDescription } from './locatorGenerators'; -export function formatProtocolParam(params: Record | undefined, alternatives: string): string | undefined { - return _formatProtocolParam(params, alternatives)?.replaceAll('\n', '\\n'); +import type { Language } from './locatorGenerators'; + +export type CallMetainfo = { + type: string; + method: string; + params?: Record; + // Pre-rendered overrides, take precedence over the protocol templates. + title?: string; + subtitle?: string; +}; + +export function formatProtocolParam(params: Record | undefined, alternatives: string, sdkLanguage?: Language): string | undefined { + return _formatProtocolParam(params, alternatives, sdkLanguage)?.replaceAll('\n', '\\n'); } -function _formatProtocolParam(params: Record | undefined, alternatives: string): string | undefined { +function _formatProtocolParam(params: Record | undefined, alternatives: string, sdkLanguage?: Language): string | undefined { if (!params) return undefined; @@ -44,8 +56,11 @@ function _formatProtocolParam(params: Record | undefined, altern } const value = deepParam(params, name); - if (value !== undefined) - return value; + if (value === undefined) + continue; + if (name === 'selector' || name.endsWith('.selector')) + return asLocatorDescription(sdkLanguage ?? 'javascript', value); + return value; } } @@ -62,11 +77,31 @@ function deepParam(params: Record, name: string): string | undefine return String(current); } -export function renderTitleForCall(metadata: { title?: string, type: string, method: string, params: Record | undefined }) { +export function renderTitleForCall(metadata: CallMetainfo, sdkLanguage?: Language): string { const titleFormat = metadata.title ?? getMetainfo(metadata)?.title ?? metadata.method; return titleFormat.replace(/\{([^}]+)\}/g, (fullMatch, p1) => { - return formatProtocolParam(metadata.params, p1) ?? fullMatch; + return formatProtocolParam(metadata.params, p1, sdkLanguage) ?? fullMatch; + }); +} + +export function renderSubtitleForCall(metadata: CallMetainfo, sdkLanguage?: Language): string | undefined { + const subtitleFormat = metadata.subtitle ?? getMetainfo(metadata)?.subtitle; + if (subtitleFormat === undefined) + return undefined; + let allParamsResolved = true; + const subtitle = subtitleFormat.replace(/\{([^}]+)\}/g, (fullMatch, p1) => { + const param = formatProtocolParam(metadata.params, p1, sdkLanguage); + if (param === undefined) + allParamsResolved = false; + return param ?? fullMatch; }); + return allParamsResolved ? subtitle : undefined; +} + +export function renderFullTitleForCall(metadata: CallMetainfo, sdkLanguage?: Language): string { + const title = renderTitleForCall(metadata, sdkLanguage); + const subtitle = renderSubtitleForCall(metadata, sdkLanguage); + return subtitle ? `${title} ${subtitle}` : title; } export type ActionGroup = 'configuration' | 'route' | 'getter'; @@ -74,3 +109,59 @@ export type ActionGroup = 'configuration' | 'route' | 'getter'; export function getActionGroup(metadata: { type: string, method: string }) { return getMetainfo(metadata)?.group as undefined | ActionGroup; } + +const kMaxParamLength = 200; + +// Curated per-call parameters, defined by the "renderParams" lists in protocol.yml. Only the +// arguments that say what the call actually did are reported, and only when they are +// bounded in size: page content, evaluated expressions, request bodies and the like are +// never reported, and neither are options that repeat their default on every call. +export function renderParamsForCall(metadata: CallMetainfo, sdkLanguage?: Language): Record | undefined { + if (!metadata.params) + return undefined; + const result: Record = {}; + const locator = renderLocator(metadata.params.selector, sdkLanguage); + if (locator !== undefined) + result.locator = locator; + for (const entry of getMetainfo(metadata)?.renderParams ?? []) { + const { key, value } = renderParamEntry(metadata.params, entry, sdkLanguage); + if (value !== undefined) + result[key] = value; + } + return Object.keys(result).length ? result : undefined; +} + +// Each entry is "[key=]path[:selector]": a dotted path into the call params, reported +// under the last path segment unless an explicit key is given, with selector values +// rendered as locators. +function renderParamEntry(params: Record, entry: string, sdkLanguage?: Language): { key: string, value: any } { + const [spec, format] = entry.split(':'); + const eqIndex = spec.indexOf('='); + const path = eqIndex === -1 ? spec : spec.slice(eqIndex + 1); + const key = eqIndex === -1 ? path.split('.').pop()! : spec.slice(0, eqIndex); + let value: any = params; + for (const token of path.split('.')) { + if (typeof value !== 'object' || value === null) { + value = undefined; + break; + } + value = value[token]; + } + if (value === undefined) + return { key, value }; + if (format === 'selector') + return { key, value: renderLocator(value, sdkLanguage) }; + if (typeof value === 'string') + return { key, value: truncateParam(value) }; + return { key, value }; +} + +function renderLocator(selector: any, sdkLanguage?: Language): string | undefined { + if (typeof selector !== 'string') + return undefined; + return truncateParam(asLocatorDescription(sdkLanguage ?? 'javascript', selector)); +} + +export function truncateParam(value: string): string { + return value.length > kMaxParamLength ? value.substring(0, kMaxParamLength) + '…' : value; +} diff --git a/packages/isomorphic/protocolMetainfo.ts b/packages/isomorphic/protocolMetainfo.ts index d8afde4be244d..5da32305cd43d 100644 --- a/packages/isomorphic/protocolMetainfo.ts +++ b/packages/isomorphic/protocolMetainfo.ts @@ -16,7 +16,7 @@ // This file is generated by generate_channels.js, do not edit manually. -export type MethodMetainfo = { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string }; +export type MethodMetainfo = { internal?: boolean, title?: string, subtitle?: string, renderParams?: string[], slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string }; export const methodMetainfo = new Map([ ['Android.devices', { internal: true, }], @@ -46,7 +46,7 @@ export const methodMetainfo = new Map([ ['AndroidDevice.push', { title: 'Push', }], ['AndroidDevice.connectToWebView', { title: 'Connect to Web View', }], ['AndroidDevice.close', { internal: true, }], - ['APIRequestContext.fetch', { title: '{method} "{url}"', }], + ['APIRequestContext.fetch', { title: '{method}', subtitle: '{url}', renderParams: ['url', 'method'], }], ['APIRequestContext.fetchResponseBody', { title: 'Get response body', group: 'getter', }], ['APIRequestContext.fetchLog', { internal: true, }], ['APIRequestContext.storageState', { title: 'Get storage state', group: 'configuration', }], @@ -90,7 +90,7 @@ export const methodMetainfo = new Map([ ['BrowserContext.setHTTPCredentials', { title: 'Set HTTP credentials', group: 'configuration', }], ['BrowserContext.setNetworkInterceptionPatterns', { title: 'Route requests', group: 'route', }], ['BrowserContext.setWebSocketInterceptionPatterns', { title: 'Route WebSockets', group: 'route', }], - ['BrowserContext.setOffline', { title: 'Set offline mode', }], + ['BrowserContext.setOffline', { title: 'Set offline mode', renderParams: ['offline'], }], ['BrowserContext.storageState', { title: 'Get storage state', group: 'configuration', }], ['BrowserContext.setStorageState', { title: 'Set storage state', group: 'configuration', }], ['BrowserContext.pause', { title: 'Pause', }], @@ -121,56 +121,56 @@ export const methodMetainfo = new Map([ ['ElectronApplication.evaluateExpression', { title: 'Evaluate', }], ['ElectronApplication.evaluateExpressionHandle', { title: 'Evaluate', }], ['ElectronApplication.updateSubscription', { internal: true, }], - ['Frame.evalOnSelector', { title: 'Evaluate', snapshot: true, pause: true, }], - ['Frame.evalOnSelectorAll', { title: 'Evaluate', snapshot: true, pause: true, }], - ['Frame.addScriptTag', { title: 'Add script tag', snapshot: true, pause: true, }], - ['Frame.addStyleTag', { title: 'Add style tag', snapshot: true, pause: true, }], - ['Frame.ariaSnapshot', { title: 'Aria snapshot', group: 'getter', }], - ['Frame.ariaSnapshotJSON', { title: 'Aria snapshot JSON', group: 'getter', }], - ['Frame.blur', { title: 'Blur', slowMo: true, snapshot: true, pause: true, }], - ['Frame.check', { title: 'Check', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.click', { title: 'Click', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.evalOnSelector', { title: 'Evaluate', subtitle: '{selector}', snapshot: true, pause: true, }], + ['Frame.evalOnSelectorAll', { title: 'Evaluate', subtitle: '{selector}', snapshot: true, pause: true, }], + ['Frame.addScriptTag', { title: 'Add script tag', renderParams: ['url'], snapshot: true, pause: true, }], + ['Frame.addStyleTag', { title: 'Add style tag', renderParams: ['url'], snapshot: true, pause: true, }], + ['Frame.ariaSnapshot', { title: 'Aria snapshot', subtitle: '{selector}', group: 'getter', }], + ['Frame.ariaSnapshotJSON', { title: 'Aria snapshot JSON', subtitle: '{selector}', group: 'getter', }], + ['Frame.blur', { title: 'Blur', subtitle: '{selector}', slowMo: true, snapshot: true, pause: true, }], + ['Frame.check', { title: 'Check', subtitle: '{selector}', renderParams: ['position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.click', { title: 'Click', subtitle: '{selector}', renderParams: ['button', 'clickCount', 'modifiers', 'position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['Frame.content', { title: 'Get content', snapshot: true, pause: true, }], - ['Frame.dragAndDrop', { title: 'Drag and drop', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.drop', { title: 'Drop files or data onto an element', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.dblclick', { title: 'Double click', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.dispatchEvent', { title: 'Dispatch "{type}"', slowMo: true, snapshot: true, pause: true, }], + ['Frame.dragAndDrop', { title: 'Drag and drop', renderParams: ['source:selector', 'target:selector'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.drop', { title: 'Drop files or data onto an element', subtitle: '{selector}', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.dblclick', { title: 'Double click', subtitle: '{selector}', renderParams: ['button', 'modifiers', 'position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.dispatchEvent', { title: 'Dispatch "{type}"', subtitle: '{selector}', renderParams: ['type'], slowMo: true, snapshot: true, pause: true, }], ['Frame.evaluateExpression', { title: 'Evaluate', snapshot: true, pause: true, }], ['Frame.evaluateExpressionHandle', { title: 'Evaluate', snapshot: true, pause: true, }], - ['Frame.fill', { title: 'Fill "{value}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.focus', { title: 'Focus', slowMo: true, snapshot: true, pause: true, }], + ['Frame.fill', { title: 'Fill "{value}"', subtitle: '{selector}', renderParams: ['value'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.focus', { title: 'Focus', subtitle: '{selector}', slowMo: true, snapshot: true, pause: true, }], ['Frame.frameElement', { title: 'Get frame element', group: 'getter', }], ['Frame.resolveSelector', { internal: true, }], ['Frame.highlight', { internal: true, }], ['Frame.hideHighlight', { internal: true, }], - ['Frame.getAttribute', { title: 'Get attribute "{name}"', snapshot: true, pause: true, group: 'getter', }], - ['Frame.goto', { title: 'Navigate to "{url}"', slowMo: true, snapshot: true, pause: true, }], - ['Frame.hover', { title: 'Hover', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.innerHTML', { title: 'Get HTML', snapshot: true, pause: true, group: 'getter', }], - ['Frame.innerText', { title: 'Get inner text', snapshot: true, pause: true, group: 'getter', }], - ['Frame.inputValue', { title: 'Get input value', snapshot: true, pause: true, group: 'getter', }], - ['Frame.isChecked', { title: 'Is checked', snapshot: true, pause: true, group: 'getter', }], - ['Frame.isDisabled', { title: 'Is disabled', snapshot: true, pause: true, group: 'getter', }], - ['Frame.isEnabled', { title: 'Is enabled', snapshot: true, pause: true, group: 'getter', }], - ['Frame.isHidden', { title: 'Is hidden', snapshot: true, pause: true, group: 'getter', }], - ['Frame.isVisible', { title: 'Is visible', snapshot: true, pause: true, group: 'getter', }], - ['Frame.isEditable', { title: 'Is editable', snapshot: true, pause: true, group: 'getter', }], - ['Frame.press', { title: 'Press "{key}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.querySelector', { title: 'Query selector', snapshot: true, }], - ['Frame.querySelectorAll', { title: 'Query selector all', snapshot: true, }], - ['Frame.queryCount', { title: 'Query count', snapshot: true, pause: true, }], - ['Frame.selectOption', { title: 'Select option', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.getAttribute', { title: 'Get attribute "{name}"', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.goto', { title: 'Navigate', subtitle: '{url}', renderParams: ['url'], slowMo: true, snapshot: true, pause: true, }], + ['Frame.hover', { title: 'Hover', subtitle: '{selector}', renderParams: ['modifiers', 'position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.innerHTML', { title: 'Get HTML', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.innerText', { title: 'Get inner text', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.inputValue', { title: 'Get input value', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isChecked', { title: 'Is checked', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isDisabled', { title: 'Is disabled', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isEnabled', { title: 'Is enabled', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isHidden', { title: 'Is hidden', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isVisible', { title: 'Is visible', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.isEditable', { title: 'Is editable', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], + ['Frame.press', { title: 'Press "{key}"', subtitle: '{selector}', renderParams: ['key'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.querySelector', { title: 'Query selector', subtitle: '{selector}', snapshot: true, }], + ['Frame.querySelectorAll', { title: 'Query selector all', subtitle: '{selector}', snapshot: true, }], + ['Frame.queryCount', { title: 'Query count', subtitle: '{selector}', snapshot: true, pause: true, }], + ['Frame.selectOption', { title: 'Select option', subtitle: '{selector}', renderParams: ['options'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['Frame.setContent', { title: 'Set content', snapshot: true, pause: true, }], - ['Frame.setInputFiles', { title: 'Set input files', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.tap', { title: 'Tap', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.textContent', { title: 'Get text content', snapshot: true, pause: true, group: 'getter', }], + ['Frame.setInputFiles', { title: 'Set input files', subtitle: '{selector}', renderParams: ['files=localPaths'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.tap', { title: 'Tap', subtitle: '{selector}', renderParams: ['modifiers', 'position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.textContent', { title: 'Get text content', subtitle: '{selector}', snapshot: true, pause: true, group: 'getter', }], ['Frame.title', { title: 'Get page title', group: 'getter', }], - ['Frame.type', { title: 'Type "{text}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.uncheck', { title: 'Uncheck', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['Frame.waitForTimeout', { title: 'Wait for timeout', snapshot: true, }], - ['Frame.waitForFunction', { title: 'Wait for function', snapshot: true, pause: true, }], - ['Frame.waitForSelector', { title: 'Wait for selector', snapshot: true, }], - ['Frame.expect', { title: 'Expect "{expression}"', snapshot: true, pause: true, }], + ['Frame.type', { title: 'Type "{text}"', subtitle: '{selector}', renderParams: ['text'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.uncheck', { title: 'Uncheck', subtitle: '{selector}', renderParams: ['position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['Frame.waitForTimeout', { title: 'Wait for timeout', renderParams: ['timeout=waitTimeout'], snapshot: true, }], + ['Frame.waitForFunction', { title: 'Wait for function', subtitle: '{selector}', snapshot: true, pause: true, }], + ['Frame.waitForSelector', { title: 'Wait for selector', subtitle: '{selector}', renderParams: ['state'], snapshot: true, }], + ['Frame.expect', { title: 'Expect "{expression}"', subtitle: '{selector}', snapshot: true, pause: true, }], ['JSHandle.dispose', { internal: true, }], ['ElementHandle.dispose', { internal: true, }], ['JSHandle.evaluateExpression', { title: 'Evaluate', snapshot: true, pause: true, }], @@ -183,18 +183,18 @@ export const methodMetainfo = new Map([ ['ElementHandle.getProperty', { title: 'Get JS property', group: 'getter', }], ['JSHandle.jsonValue', { title: 'Get JSON value', group: 'getter', }], ['ElementHandle.jsonValue', { title: 'Get JSON value', group: 'getter', }], - ['ElementHandle.evalOnSelector', { title: 'Evaluate', snapshot: true, pause: true, }], - ['ElementHandle.evalOnSelectorAll', { title: 'Evaluate', snapshot: true, pause: true, }], + ['ElementHandle.evalOnSelector', { title: 'Evaluate', subtitle: '{selector}', snapshot: true, pause: true, }], + ['ElementHandle.evalOnSelectorAll', { title: 'Evaluate', subtitle: '{selector}', snapshot: true, pause: true, }], ['ElementHandle.boundingBox', { title: 'Get bounding box', snapshot: true, pause: true, }], - ['ElementHandle.check', { title: 'Check', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['ElementHandle.click', { title: 'Click', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.check', { title: 'Check', renderParams: ['position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.click', { title: 'Click', renderParams: ['button', 'clickCount', 'modifiers', 'position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['ElementHandle.contentFrame', { title: 'Get content frame', group: 'getter', }], - ['ElementHandle.dblclick', { title: 'Double click', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['ElementHandle.dispatchEvent', { title: 'Dispatch event', slowMo: true, snapshot: true, pause: true, }], - ['ElementHandle.fill', { title: 'Fill "{value}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.dblclick', { title: 'Double click', renderParams: ['button', 'modifiers', 'position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.dispatchEvent', { title: 'Dispatch event', renderParams: ['type'], slowMo: true, snapshot: true, pause: true, }], + ['ElementHandle.fill', { title: 'Fill "{value}"', renderParams: ['value'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['ElementHandle.focus', { title: 'Focus', slowMo: true, snapshot: true, pause: true, }], ['ElementHandle.getAttribute', { title: 'Get attribute', snapshot: true, pause: true, group: 'getter', }], - ['ElementHandle.hover', { title: 'Hover', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.hover', { title: 'Hover', renderParams: ['modifiers', 'position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['ElementHandle.innerHTML', { title: 'Get HTML', snapshot: true, pause: true, group: 'getter', }], ['ElementHandle.innerText', { title: 'Get inner text', snapshot: true, pause: true, group: 'getter', }], ['ElementHandle.inputValue', { title: 'Get input value', snapshot: true, pause: true, group: 'getter', }], @@ -205,20 +205,20 @@ export const methodMetainfo = new Map([ ['ElementHandle.isHidden', { title: 'Is hidden', snapshot: true, pause: true, group: 'getter', }], ['ElementHandle.isVisible', { title: 'Is visible', snapshot: true, pause: true, group: 'getter', }], ['ElementHandle.ownerFrame', { title: 'Get owner frame', group: 'getter', }], - ['ElementHandle.press', { title: 'Press "{key}"', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['ElementHandle.querySelector', { title: 'Query selector', snapshot: true, }], - ['ElementHandle.querySelectorAll', { title: 'Query selector all', snapshot: true, }], - ['ElementHandle.screenshot', { title: 'Screenshot', snapshot: true, pause: true, }], + ['ElementHandle.press', { title: 'Press "{key}"', renderParams: ['key'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.querySelector', { title: 'Query selector', subtitle: '{selector}', snapshot: true, }], + ['ElementHandle.querySelectorAll', { title: 'Query selector all', subtitle: '{selector}', snapshot: true, }], + ['ElementHandle.screenshot', { title: 'Screenshot', renderParams: ['type'], snapshot: true, pause: true, }], ['ElementHandle.scrollIntoViewIfNeeded', { title: 'Scroll into view', slowMo: true, snapshot: true, pause: true, }], - ['ElementHandle.selectOption', { title: 'Select option', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.selectOption', { title: 'Select option', renderParams: ['options'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['ElementHandle.selectText', { title: 'Select text', slowMo: true, snapshot: true, pause: true, }], - ['ElementHandle.setInputFiles', { title: 'Set input files', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['ElementHandle.tap', { title: 'Tap', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.setInputFiles', { title: 'Set input files', renderParams: ['files=localPaths'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.tap', { title: 'Tap', renderParams: ['modifiers', 'position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], ['ElementHandle.textContent', { title: 'Get text content', snapshot: true, pause: true, group: 'getter', }], - ['ElementHandle.type', { title: 'Type', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['ElementHandle.uncheck', { title: 'Uncheck', slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], - ['ElementHandle.waitForElementState', { title: 'Wait for state', snapshot: true, pause: true, }], - ['ElementHandle.waitForSelector', { title: 'Wait for selector', snapshot: true, }], + ['ElementHandle.type', { title: 'Type', renderParams: ['text'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.uncheck', { title: 'Uncheck', renderParams: ['position'], slowMo: true, snapshot: true, pause: true, input: true, isAutoWaiting: true, }], + ['ElementHandle.waitForElementState', { title: 'Wait for state', renderParams: ['state'], snapshot: true, pause: true, }], + ['ElementHandle.waitForSelector', { title: 'Wait for selector', subtitle: '{selector}', renderParams: ['state'], snapshot: true, }], ['LocalUtils.zip', { internal: true, }], ['LocalUtils.harOpen', { internal: true, }], ['LocalUtils.harLookup', { internal: true, }], @@ -252,32 +252,32 @@ export const methodMetainfo = new Map([ ['Page.runBeforeUnload', { title: 'Run beforeunload', pause: true, }], ['Page.clearConsoleMessages', { title: 'Clear console messages', }], ['Page.consoleMessages', { title: 'Get console messages', group: 'getter', }], - ['Page.emulateMedia', { title: 'Emulate media', snapshot: true, pause: true, }], + ['Page.emulateMedia', { title: 'Emulate media', renderParams: ['media', 'colorScheme', 'reducedMotion', 'forcedColors', 'contrast'], snapshot: true, pause: true, }], ['Page.exposeBinding', { title: 'Expose binding', group: 'configuration', }], ['Page.goBack', { title: 'Go back', slowMo: true, snapshot: true, pause: true, }], ['Page.goForward', { title: 'Go forward', slowMo: true, snapshot: true, pause: true, }], ['Page.requestGC', { title: 'Request garbage collection', group: 'configuration', }], - ['Page.registerLocatorHandler', { title: 'Register locator handler', }], + ['Page.registerLocatorHandler', { title: 'Register locator handler', subtitle: '{selector}', }], ['Page.resolveLocatorHandlerNoReply', { internal: true, }], ['Page.unregisterLocatorHandler', { title: 'Unregister locator handler', }], ['Page.reload', { title: 'Reload', slowMo: true, snapshot: true, pause: true, }], - ['Page.expectScreenshot', { title: 'Expect screenshot', snapshot: true, pause: true, }], - ['Page.screenshot', { title: 'Screenshot', snapshot: true, pause: true, }], + ['Page.expectScreenshot', { title: 'Expect screenshot', subtitle: '{locator.selector}', snapshot: true, pause: true, }], + ['Page.screenshot', { title: 'Screenshot', renderParams: ['type', 'fullPage'], snapshot: true, pause: true, }], ['Page.setExtraHTTPHeaders', { title: 'Set extra HTTP headers', group: 'configuration', }], ['Page.setNetworkInterceptionPatterns', { title: 'Route requests', group: 'route', }], ['Page.setWebSocketInterceptionPatterns', { title: 'Route WebSockets', group: 'route', }], - ['Page.setViewportSize', { title: 'Set viewport size', snapshot: true, pause: true, }], - ['Page.keyboardDown', { title: 'Key down "{key}"', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.keyboardUp', { title: 'Key up "{key}"', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.keyboardInsertText', { title: 'Insert "{text}"', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.keyboardType', { title: 'Type "{text}"', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.keyboardPress', { title: 'Press "{key}"', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.mouseMove', { title: 'Mouse move', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.mouseDown', { title: 'Mouse down', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.mouseUp', { title: 'Mouse up', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.mouseClick', { title: 'Click', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.mouseWheel', { title: 'Mouse wheel', slowMo: true, snapshot: true, pause: true, input: true, }], - ['Page.touchscreenTap', { title: 'Tap', slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.setViewportSize', { title: 'Set viewport size', renderParams: ['viewportSize.width', 'viewportSize.height'], snapshot: true, pause: true, }], + ['Page.keyboardDown', { title: 'Key down "{key}"', renderParams: ['key'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.keyboardUp', { title: 'Key up "{key}"', renderParams: ['key'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.keyboardInsertText', { title: 'Insert "{text}"', renderParams: ['text'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.keyboardType', { title: 'Type "{text}"', renderParams: ['text'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.keyboardPress', { title: 'Press "{key}"', renderParams: ['key'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseMove', { title: 'Mouse move', renderParams: ['x', 'y'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseDown', { title: 'Mouse down', renderParams: ['button', 'clickCount'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseUp', { title: 'Mouse up', renderParams: ['button', 'clickCount'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseClick', { title: 'Click', renderParams: ['x', 'y', 'button', 'clickCount'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.mouseWheel', { title: 'Mouse wheel', renderParams: ['deltaX', 'deltaY'], slowMo: true, snapshot: true, pause: true, input: true, }], + ['Page.touchscreenTap', { title: 'Tap', renderParams: ['x', 'y'], slowMo: true, snapshot: true, pause: true, input: true, }], ['Page.clearPageErrors', { title: 'Clear page errors', }], ['Page.pageErrors', { title: 'Get page errors', group: 'getter', }], ['Page.pdf', { title: 'PDF', }], @@ -331,11 +331,11 @@ export const methodMetainfo = new Map([ ['Debugger.next', { title: 'Step to next call', group: 'configuration', }], ['Debugger.runTo', { title: 'Run to location', group: 'configuration', }], ['Debugger.enable', { internal: true, }], - ['Dialog.accept', { title: 'Accept dialog', }], + ['Dialog.accept', { title: 'Accept dialog', renderParams: ['promptText'], }], ['Dialog.dismiss', { title: 'Dismiss dialog', }], ['Tracing.tracingStart', { title: 'Start tracing', group: 'configuration', }], ['Tracing.tracingStartChunk', { title: 'Start tracing', group: 'configuration', }], - ['Tracing.tracingGroup', { title: 'Trace "{name}"', }], + ['Tracing.tracingGroup', { title: 'Trace "{name}"', renderParams: ['name'], }], ['Tracing.tracingGroupEnd', { title: 'Group end', }], ['Tracing.tracingStopChunk', { title: 'Stop tracing', group: 'configuration', }], ['Tracing.tracingStop', { title: 'Stop tracing', group: 'configuration', }], diff --git a/packages/isomorphic/trace/traceModel.ts b/packages/isomorphic/trace/traceModel.ts index eef67a59a6cec..b8aab18fc936b 100644 --- a/packages/isomorphic/trace/traceModel.ts +++ b/packages/isomorphic/trace/traceModel.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getActionGroup, renderTitleForCall } from '../protocolFormatter'; +import { getActionGroup, renderFullTitleForCall } from '../protocolFormatter'; import type { Language } from '../locatorGenerators'; import type { ResourceSnapshot } from '@trace/snapshot'; @@ -219,7 +219,7 @@ export class TraceModel { const { rootItem } = buildActionTree(actions); const actionTree: string[] = []; const visit = (actionItem: ActionTreeItem, indent: string) => { - const title = renderTitleForCall({ ...actionItem.action, type: actionItem.action.class }); + const title = renderFullTitleForCall({ ...actionItem.action, type: actionItem.action.class }); actionTree.push(`${indent}${title || actionItem.id}`); for (const child of actionItem.children) visit(child, indent + ' '); diff --git a/packages/playwright-core/src/server/debugger.ts b/packages/playwright-core/src/server/debugger.ts index d02555df4f82c..ef07123dfb582 100644 --- a/packages/playwright-core/src/server/debugger.ts +++ b/packages/playwright-core/src/server/debugger.ts @@ -15,7 +15,7 @@ */ import { getMetainfo } from '@isomorphic/protocolMetainfo'; -import { renderTitleForCall } from '@isomorphic/protocolFormatter'; +import { renderFullTitleForCall } from '@isomorphic/protocolFormatter'; import { monotonicTime } from '@isomorphic/time'; import { SdkObject } from './instrumentation'; import { BrowserContext } from './browserContext'; @@ -201,7 +201,7 @@ export class Debugger extends SdkObject implements Instrumenta continue; updates.push({ id: call.metadata.id, - title: renderTitleForCall(call.metadata) ?? '', + title: renderFullTitleForCall(call.metadata, this._context._browser.sdkLanguage()) ?? '', location: call.metadata.location, newLogEntries: call.metadata.log.slice(call.sentLogCount), actionPoint: call.actionPoint, diff --git a/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts index 1c9522415621f..f1ef39f29b85a 100644 --- a/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { renderTitleForCall } from '@isomorphic/protocolFormatter'; +import { renderFullTitleForCall } from '@isomorphic/protocolFormatter'; import { Dispatcher } from './dispatcher'; import { Debugger } from '../debugger'; @@ -46,14 +46,14 @@ export class DebuggerDispatcher extends Dispatcher', line: metadata.location?.line, column: metadata.location?.column, }, - title: renderTitleForCall(metadata), + title: renderFullTitleForCall(metadata, sdkObject.attribution.playwright.options.sdkLanguage), }; } diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 42352d12f2a7c..5a4cd31359f9b 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { renderTitleForCall } from '@isomorphic/protocolFormatter'; +import { renderFullTitleForCall } from '@isomorphic/protocolFormatter'; import { ExpectError, Frame } from '../frames'; import { Dispatcher } from './dispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; @@ -280,7 +280,7 @@ export class FrameDispatcher extends Dispatcher { - progress.log(`${renderTitleForCall(progress.metadata)}${progress.timeout ? ` with timeout ${progress.timeout}ms` : ''}`); + progress.log(`${renderFullTitleForCall(progress.metadata, this._frame._page.browserContext._browser.sdkLanguage())}${progress.timeout ? ` with timeout ${progress.timeout}ms` : ''}`); const expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined; try { await this._frame.expect(progress, params.selector, { ...params, expectedValue }); diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 69bb01f0a8d28..4b351777a85a5 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { renderTitleForCall } from '@isomorphic/protocolFormatter'; +import { renderFullTitleForCall } from '@isomorphic/protocolFormatter'; import { deserializeURLMatch, urlMatches } from '@isomorphic/urlMatch'; import { ManualPromise } from '@isomorphic/manualPromise'; import { Page, Worker } from '../page'; @@ -236,7 +236,7 @@ export class PageDispatcher extends Dispatcher implements Instrume status = 'in-progress'; if (this._debugger.isPaused(metadata)) status = 'paused'; - logs.push(metadataToCallLog(metadata, status)); + logs.push(metadataToCallLog(metadata, status, this._currentLanguage)); } this._callLogs = logs; this.emit(RecorderEvent.CallLogsUpdated, logs); diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index a29951ec63cf7..4fdbacb7c632c 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { renderTitleForCall } from '@isomorphic/protocolFormatter'; +import { renderSubtitleForCall, renderTitleForCall } from '@isomorphic/protocolFormatter'; import { raceAgainstDeadline } from '@isomorphic/timeoutRunner'; import { monotonicTime } from '@isomorphic/time'; import { quoteCSSAttributeValue } from '@isomorphic/stringUtils'; @@ -24,6 +24,7 @@ import { Frame } from '../frames'; import type { CallMetadata } from '../instrumentation'; import type { CallLog, CallLogStatus } from '@recorder/recorderTypes'; import type { Progress } from '../progress'; +import type { Language } from '@isomorphic/locatorGenerators'; function buildFullSelector(framePath: string[], selector: string) { return [...framePath, selector].join(' >> internal:control=enter-frame >> '); @@ -58,14 +59,11 @@ async function resolvesToFrame(progress: Progress, selector: string, frame: Fram } } -export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { - const title = renderTitleForCall(metadata); +export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus, sdkLanguage: Language): CallLog { + const title = renderTitleForCall(metadata, sdkLanguage); + const subtitle = renderSubtitleForCall(metadata, sdkLanguage); if (metadata.error) status = 'error'; - const params = { - url: metadata.params?.url, - selector: metadata.params?.selector, - }; let duration = metadata.endTime ? metadata.endTime - metadata.startTime : undefined; if (typeof duration === 'number' && metadata.pauseStartTime && metadata.pauseEndTime) { duration -= (metadata.pauseEndTime - metadata.pauseStartTime); @@ -75,9 +73,9 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus) id: metadata.id, messages: metadata.log, title: title ?? '', + subtitle, status, error: metadata.error?.error?.message, - params, duration, }; return callLog; diff --git a/packages/playwright-core/src/server/screencast.ts b/packages/playwright-core/src/server/screencast.ts index d38cafaf1d240..f1add3433a624 100644 --- a/packages/playwright-core/src/server/screencast.ts +++ b/packages/playwright-core/src/server/screencast.ts @@ -15,7 +15,7 @@ */ import { LongStandingScope } from '@isomorphic/manualPromise'; -import { renderTitleForCall } from '@isomorphic/protocolFormatter'; +import { renderFullTitleForCall } from '@isomorphic/protocolFormatter'; import { debugLogger } from '@utils/debugLogger'; import { Page } from './page'; import { nullProgress } from './progress'; @@ -169,7 +169,7 @@ export class Screencast implements InstrumentationListener { if (!box && (sdkObject instanceof ElementHandle)) box = await sdkObject.boundingBox(nullProgress) || undefined; - const actionTitle = renderTitleForCall(progress.metadata); + const actionTitle = renderFullTitleForCall(progress.metadata, this.page.browserContext._browser.sdkLanguage()); const utility = await progress.race(page.mainFrame().utilityContext()); // Run this outside of the progress timer. diff --git a/packages/playwright-core/src/tools/trace/traceActions.ts b/packages/playwright-core/src/tools/trace/traceActions.ts index 3b75d9c1bda2f..6332aa3f7dc21 100644 --- a/packages/playwright-core/src/tools/trace/traceActions.ts +++ b/packages/playwright-core/src/tools/trace/traceActions.ts @@ -17,12 +17,10 @@ /* eslint-disable no-console */ import { buildActionTree } from '@isomorphic/trace/traceModel'; -import { asLocatorDescription } from '@isomorphic/locatorGenerators'; import { msToString } from '@isomorphic/formatUtils'; -import { loadTrace, formatTimestamp, actionTitle } from './traceUtils'; +import { loadTrace, formatTimestamp, actionFullTitle, actionSubtitle, actionTitle } from './traceUtils'; import type { ActionEntry } from '@isomorphic/trace/entries'; -import type { Language } from '@isomorphic/locatorGenerators'; export async function traceActions(options: { grep?: string, errorsOnly?: boolean }) { const trace = await loadTrace(); @@ -38,12 +36,12 @@ export async function traceActions(options: { grep?: string, errorsOnly?: boolea const ts = formatTimestamp(action.startTime, trace.model.startTime); const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'running'; const title = actionTitle(action); - const locator = actionLocator(action); + const subtitle = actionSubtitle(action); const error = action.error ? ' ✗' : ''; const prefix = ` ${(ordinal + '.').padStart(4)} ${ts} ${indent}`; console.log(`${prefix}${title.padEnd(Math.max(1, 55 - indent.length))} ${duration.padStart(8)}${error}`); - if (locator) - console.log(`${' '.repeat(prefix.length)}${locator}`); + if (subtitle) + console.log(`${' '.repeat(prefix.length)}${subtitle}`); for (const child of item.children) visit(child, indent + ' '); }; @@ -55,17 +53,13 @@ function filterActions(actions: ActionEntry[], options: { grep?: string, errorsO let result = actions.filter(a => a.group !== 'configuration'); if (options.grep) { const pattern = new RegExp(options.grep, 'i'); - result = result.filter(a => pattern.test(actionTitle(a)) || pattern.test(actionLocator(a) || '')); + result = result.filter(a => pattern.test(actionFullTitle(a))); } if (options.errorsOnly) result = result.filter(a => !!a.error); return result; } -function actionLocator(action: ActionEntry, sdkLanguage?: Language): string | undefined { - return action.params.selector ? asLocatorDescription(sdkLanguage || 'javascript', action.params.selector) : undefined; -} - export async function traceAction(actionId: string) { const trace = await loadTrace(); const action = trace.resolveActionId(actionId); @@ -75,7 +69,7 @@ export async function traceAction(actionId: string) { return; } - const title = actionTitle(action); + const title = actionFullTitle(action); console.log(`\n ${title}\n`); // Time diff --git a/packages/playwright-core/src/tools/trace/traceErrors.ts b/packages/playwright-core/src/tools/trace/traceErrors.ts index f699de9d53d36..7fb17452900a7 100644 --- a/packages/playwright-core/src/tools/trace/traceErrors.ts +++ b/packages/playwright-core/src/tools/trace/traceErrors.ts @@ -16,7 +16,7 @@ /* eslint-disable no-console */ -import { loadTrace, actionTitle } from './traceUtils'; +import { loadTrace, actionFullTitle } from './traceUtils'; export async function traceErrors() { const trace = await loadTrace(); @@ -29,7 +29,7 @@ export async function traceErrors() { for (const error of model.errorDescriptors) { if (error.action) { - const title = actionTitle(error.action); + const title = actionFullTitle(error.action); console.log(`\n ✗ ${title}`); } else { console.log(`\n ✗ Error`); diff --git a/packages/playwright-core/src/tools/trace/traceUtils.ts b/packages/playwright-core/src/tools/trace/traceUtils.ts index 4a93dd3f121bd..2734a22069d73 100644 --- a/packages/playwright-core/src/tools/trace/traceUtils.ts +++ b/packages/playwright-core/src/tools/trace/traceUtils.ts @@ -19,7 +19,7 @@ import path from 'path'; import { TraceModel, buildActionTree } from '@isomorphic/trace/traceModel'; import { TraceLoader } from '@isomorphic/trace/traceLoader'; -import { renderTitleForCall } from '@isomorphic/protocolFormatter'; +import { renderFullTitleForCall, renderSubtitleForCall, renderTitleForCall } from '@isomorphic/protocolFormatter'; import { resolveWithinRoot } from '@utils/fileUtils'; import { DirTraceLoaderBackend, extractTrace } from './traceParser'; @@ -109,6 +109,14 @@ export function actionTitle(action: ActionEntry): string { return renderTitleForCall({ ...action, type: action.class }) || `${action.class}.${action.method}`; } +export function actionSubtitle(action: ActionEntry): string | undefined { + return renderSubtitleForCall({ ...action, type: action.class }); +} + +export function actionFullTitle(action: ActionEntry): string { + return renderFullTitleForCall({ ...action, type: action.class }) || `${action.class}.${action.method}`; +} + export async function saveOutputFile(fileName: string, content: string | Buffer, explicitOutput?: string): Promise { let outFile: string; if (explicitOutput) { diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index 04ca1ef854060..8ca7824f9ee0b 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -126,6 +126,7 @@ export type StepBeginPayload = { stepId: string; parentStepId: string | undefined; title: string; + subtitle?: string; category: string; params?: Record; wallTime: number; // milliseconds since unix epoch diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index eda97b3fa4d40..e18daf3cac2b0 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -18,8 +18,7 @@ import fs from 'fs'; import path from 'path'; import * as playwrightLibrary from 'playwright-core'; -import { asLocatorDescription } from '@isomorphic/locatorGenerators'; -import { getActionGroup, renderTitleForCall } from '@isomorphic/protocolFormatter'; +import { getActionGroup, renderParamsForCall, renderSubtitleForCall, renderTitleForCall, truncateParam } from '@isomorphic/protocolFormatter'; import { escapeHTML } from '@isomorphic/stringUtils'; import { jsonStringifyForceASCII } from '@utils/ascii'; import { createGuid } from '@utils/crypto'; @@ -112,11 +111,12 @@ const utilityFixtures: Fixtures = { } // In the general case, create a step for each api call and connect them through the stepId. - const params = renderParams(channel.type, channel.method, channel.params); + const params = renderParamsForCall({ type: channel.type, method: channel.method, params: channel.params }); const step = testInfo._addStep({ location: data.frames[0], category: 'pw:api', - title: renderTitle(channel.type, channel.method, channel.params, data.title, params?.locator), + title: renderTitle(channel.type, channel.method, channel.params, data.title), + subtitle: renderSubtitle(channel.type, channel.method, channel.params), params, group: getActionGroup({ type: channel.type, method: channel.method }), }, tracingGroupSteps[tracingGroupSteps.length - 1]); @@ -920,123 +920,13 @@ function createTestOverlay(parts: string[], position: string, fontSize: number) `; } -function renderTitle(type: string, method: string, params: Record | undefined, title: string | undefined, locator: string | undefined) { - const prefix = renderTitleForCall({ title, type, method, params }); - return prefix + (locator ? ` ${locator}` : ''); +function renderTitle(type: string, method: string, params: Record | undefined, title: string | undefined) { + return renderTitleForCall({ title, type, method, params }); } -const kMaxParamLength = 200; - -// Curated per-call parameters, keyed the same way as the protocol metainfo. Only the -// arguments that say what the call actually did are reported, and only when they are -// bounded in size: page content, evaluated expressions, request bodies and the like are -// never reported, and neither are options that repeat their default on every call. -function renderCallParams(type: string, method: string, params: Record): Record | undefined { - switch (`${type}.${method}`) { - case 'APIRequestContext.fetch': - return { url: params.url, method: params.method }; - case 'Frame.goto': - case 'Frame.addScriptTag': - case 'Frame.addStyleTag': - return { url: params.url }; - - case 'Frame.click': - case 'Frame.dblclick': - case 'ElementHandle.click': - case 'ElementHandle.dblclick': - return { button: params.button, clickCount: params.clickCount, modifiers: params.modifiers, position: params.position }; - case 'Frame.hover': - case 'Frame.tap': - case 'ElementHandle.hover': - case 'ElementHandle.tap': - return { modifiers: params.modifiers, position: params.position }; - case 'Frame.check': - case 'Frame.uncheck': - case 'ElementHandle.check': - case 'ElementHandle.uncheck': - return { position: params.position }; - case 'Frame.dragAndDrop': - return { source: renderLocator(params.source), target: renderLocator(params.target) }; - case 'Frame.fill': - case 'ElementHandle.fill': - return { value: params.value }; - case 'Frame.press': - case 'ElementHandle.press': - case 'Page.keyboardDown': - case 'Page.keyboardUp': - case 'Page.keyboardPress': - return { key: params.key }; - case 'Frame.type': - case 'ElementHandle.type': - case 'Page.keyboardType': - case 'Page.keyboardInsertText': - return { text: params.text }; - case 'Frame.dispatchEvent': - case 'ElementHandle.dispatchEvent': - return { type: params.type }; - case 'Frame.selectOption': - case 'ElementHandle.selectOption': - return { options: params.options }; - case 'Frame.setInputFiles': - case 'ElementHandle.setInputFiles': - return { files: params.localPaths }; - - case 'Page.mouseClick': - return { x: params.x, y: params.y, button: params.button, clickCount: params.clickCount }; - case 'Page.mouseMove': - case 'Page.touchscreenTap': - return { x: params.x, y: params.y }; - case 'Page.mouseDown': - case 'Page.mouseUp': - return { button: params.button, clickCount: params.clickCount }; - case 'Page.mouseWheel': - return { deltaX: params.deltaX, deltaY: params.deltaY }; - - case 'Frame.waitForSelector': - case 'ElementHandle.waitForSelector': - case 'ElementHandle.waitForElementState': - return { state: params.state }; - case 'Frame.waitForTimeout': - return { timeout: params.waitTimeout }; - - case 'Page.emulateMedia': - return { media: params.media, colorScheme: params.colorScheme, reducedMotion: params.reducedMotion, forcedColors: params.forcedColors, contrast: params.contrast }; - case 'Page.setViewportSize': - return params.viewportSize; - case 'Page.screenshot': - case 'ElementHandle.screenshot': - return { type: params.type, fullPage: params.fullPage }; - case 'BrowserContext.setOffline': - return { offline: params.offline }; - case 'Dialog.accept': - return { promptText: params.promptText }; - case 'Tracing.tracingGroup': - return { name: params.name }; - } -} - -function renderParams(type: string, method: string, params: Record | undefined): Record | undefined { - if (!params) - return undefined; - const result: Record = {}; - const locator = renderLocator(params.selector); - if (locator !== undefined) - result.locator = locator; - for (const [name, value] of Object.entries(renderCallParams(type, method, params) ?? {})) { - if (value !== undefined) - result[name] = typeof value === 'string' ? truncateParam(value) : value; - } - return Object.keys(result).length ? result : undefined; -} - -function renderLocator(selector: any): string | undefined { - if (typeof selector !== 'string') - return undefined; - return truncateParam(asLocatorDescription('javascript', selector)); -} - -function truncateParam(value: string): string { - return value.length > kMaxParamLength ? value.substring(0, kMaxParamLength) + '\u2026' : value; +function renderSubtitle(type: string, method: string, params: Record | undefined) { + const subtitle = renderSubtitleForCall({ type, method, params }); + return subtitle === undefined ? undefined : truncateParam(subtitle); } function tracing() { diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts index 441c6d24ac9ae..0c86049c5505b 100644 --- a/packages/playwright/src/isomorphic/teleReceiver.ts +++ b/packages/playwright/src/isomorphic/teleReceiver.ts @@ -108,6 +108,7 @@ export type JsonTestStepStart = { id: string; parentStepId?: string; title: string; + subtitle?: string; category: string, params?: Record; startTime: number; @@ -724,6 +725,7 @@ export class TeleTestCase implements reporterTypes.TestCase { class TeleTestStep implements reporterTypes.TestStep { title: string; + subtitle: string | undefined; category: string; params: Record | undefined; location: reporterTypes.Location | undefined; @@ -739,6 +741,7 @@ class TeleTestStep implements reporterTypes.TestStep { constructor(payload: JsonTestStepStart, parentStep: reporterTypes.TestStep | undefined, location: reporterTypes.Location | undefined, result: TeleTestResult) { this.title = payload.title; + this.subtitle = payload.subtitle; this.category = payload.category; this.params = payload.params; this.location = location; diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index c3879cd76af9c..40dbe6e0cd4f5 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -102,7 +102,7 @@ export interface ExpectTestInfo { _addStep(data: { category: 'expect'; title: string; - shortTitle: string; + subtitle?: string; params?: Record; }): ExpectStep; _deadline(): { deadline: number; timeout: number }; @@ -329,9 +329,8 @@ function callMatcherAsStep(matcherName: string, info: ExpectMetaInfo, actual: un const testInfo = expectConfig().testInfo; const customMessage = info.message || ''; const suffixes = computeMatcherTitleSuffix(matcherName, actual, args); - const defaultTitle = `${info.poll ? 'poll ' : ''}${info.isSoft ? 'soft ' : ''}${info.isNot ? 'not ' : ''}${matcherName}${suffixes.short || ''}`; - const shortTitle = customMessage || `Expect ${escapeWithQuotes(defaultTitle, '"')}`; - const longTitle = shortTitle + (suffixes.long || ''); + const defaultTitle = `${info.poll ? 'poll ' : ''}${info.isSoft ? 'soft ' : ''}${info.isNot ? 'not ' : ''}${matcherName}${suffixes.titleSuffix || ''}`; + const title = customMessage || `Expect ${escapeWithQuotes(defaultTitle, '"')}`; // This looks like it is unnecessary, but it isn't - we need to filter // out all the frames that belong to the test runner from caught runtime errors. @@ -341,8 +340,8 @@ function callMatcherAsStep(matcherName: string, info: ExpectMetaInfo, actual: un params.expected = args[0]; const stepData = { category: 'expect' as const, - title: longTitle, - shortTitle, + title, + subtitle: suffixes.subtitle, location: stackFrames[0], params: Object.keys(params).length ? params : undefined, }; @@ -370,7 +369,7 @@ function callMatcherAsStep(matcherName: string, info: ExpectMetaInfo, actual: un try { const invoke = () => info.poll ? invokePollMatcher(matcherName, info, matcher, actual, args, promise) - : invokeMatcher(matcherName, info, matcher, actual, args, promise, shortTitle); + : invokeMatcher(matcherName, info, matcher, actual, args, promise, title); const result = step ? currentZone().with('stepZone', step).run(invoke) : invoke(); if (result instanceof Promise) return result.then(finalizer, handleError); diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index e67724117207a..8018886c5b61f 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -529,15 +529,15 @@ export async function toPass( return { pass: !this.isNot, message: () => '' }; } -export function computeMatcherTitleSuffix(matcherName: string, receiver: any, args: any[]): { short?: string, long?: string, params?: Record } { +export function computeMatcherTitleSuffix(matcherName: string, receiver: any, args: any[]): { titleSuffix?: string, subtitle?: string, params?: Record } { if (matcherName === 'toHaveScreenshot') { const title = toHaveScreenshotStepTitle(...args); - return { short: title ? `(${title})` : '' }; + return { titleSuffix: title ? `(${title})` : '' }; } if (receiver && typeof receiver === 'object' && (receiver as any)._apiName === 'Locator') { try { const locator = asLocatorDescription('javascript', (receiver as LocatorEx)._selector); - return { long: ' ' + locator, params: { locator } }; + return { subtitle: locator, params: { locator } }; } catch { } } diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 83536e46341e8..cd49a6eb71b58 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -536,7 +536,9 @@ function relativeTestPath(screen: Screen, config: FullConfig, test: TestCase): s } export function stepSuffix(step: TestStep | undefined) { - const stepTitles = step ? step.titlePath() : []; + const stepTitles: string[] = []; + for (let current = step; current; current = current.parent) + stepTitles.unshift(current.subtitle ? `${current.title} ${current.subtitle}` : current.title); return stepTitles.map(t => t.split('\n')[0]).map(t => ' › ' + t).join(''); } diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 8fe90bcbbf822..30abcc8254e21 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -669,6 +669,7 @@ class HtmlBuilder { title = `${title} (skipped${skipped.description ? ': ' + skipped.description : ''})`; const testStep: TestStep = { title, + subtitle: step.subtitle, startTime: step.startTime.toISOString(), duration, steps: dedupeSteps(step.steps).map(s => this._createTestStep(s, result)), @@ -774,7 +775,7 @@ function dedupeSteps(steps: api.TestStep[]) { for (const step of steps) { const canDedupe = !step.error && step.duration >= 0 && step.location?.file && !step.steps.length; const lastStep = lastResult?.step; - if (canDedupe && lastResult && lastStep && step.category === lastStep.category && step.title === lastStep.title && step.location?.file === lastStep.location?.file && step.location?.line === lastStep.location?.line && step.location?.column === lastStep.location?.column) { + if (canDedupe && lastResult && lastStep && step.category === lastStep.category && step.title === lastStep.title && step.subtitle === lastStep.subtitle && step.location?.file === lastStep.location?.file && step.location?.line === lastStep.location?.line && step.location?.column === lastStep.location?.column) { ++lastResult.count; lastResult.duration += step.duration; continue; diff --git a/packages/playwright/src/reporters/json.ts b/packages/playwright/src/reporters/json.ts index a9e2401a1742b..406df9ba87d55 100644 --- a/packages/playwright/src/reporters/json.ts +++ b/packages/playwright/src/reporters/json.ts @@ -231,6 +231,7 @@ class JSONReporter implements ReporterV2 { const steps = step.steps.filter(s => s.category === 'test.step'); return { title: step.title, + subtitle: step.subtitle, duration: step.duration, error: step.error, steps: steps.length ? steps.map(s => this._serializeTestStep(s)) : undefined, diff --git a/packages/playwright/src/reporters/perfetto.ts b/packages/playwright/src/reporters/perfetto.ts index b633c44bed620..2f0f59e27ed6f 100644 --- a/packages/playwright/src/reporters/perfetto.ts +++ b/packages/playwright/src/reporters/perfetto.ts @@ -160,7 +160,7 @@ class PerfettoReporter implements ReporterV2 { const startMs = clamp(stepStart, parentStart, parentEnd); const endMs = clamp(step.duration >= 0 ? stepStart + step.duration : parentEnd, startMs, parentEnd); this._events.push({ - name: step.title, + name: step.subtitle ? `${step.title} ${step.subtitle}` : step.title, cat: step.category, ph: 'X', ts: startMs, diff --git a/packages/playwright/src/reporters/teleEmitter.ts b/packages/playwright/src/reporters/teleEmitter.ts index 424a8c203bbdb..71aa177bd6795 100644 --- a/packages/playwright/src/reporters/teleEmitter.ts +++ b/packages/playwright/src/reporters/teleEmitter.ts @@ -316,6 +316,7 @@ export class TeleReporterEmitter implements ReporterV2 { id: (step as any)[this._idSymbol], parentStepId: (step.parent as any)?.[this._idSymbol], title: step.title, + subtitle: step.subtitle, category: step.category, params: step.params, startTime: +step.startTime, diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index ba997fbffab96..4a9666184e029 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -399,6 +399,7 @@ class JobDispatcher { const parentStep = params.parentStepId ? steps.get(params.parentStepId) : undefined; const step: TestStep = { title: params.title, + subtitle: params.subtitle, titlePath: () => { const parentPath = parentStep?.titlePath() || []; return [...parentPath, params.title]; diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 0a043b2d61c33..1197dccc64c09 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -41,7 +41,7 @@ export type TestStepCategory = 'expect' | 'fixture' | 'hook' | 'pw:api' | 'test. interface TestStepData { title: string; - shortTitle?: string; + subtitle?: string; category: TestStepCategory; location?: Location; params?: Record; @@ -376,6 +376,7 @@ export class TestInfoImpl implements TestInfo { stepId, parentStepId: parentStep ? parentStep.stepId : undefined, title: step.title, + subtitle: step.subtitle, category: step.category, params: toReportedParams(step.params), wallTime: Date.now(), @@ -387,7 +388,8 @@ export class TestInfoImpl implements TestInfo { this._tracing.appendBeforeActionForStep({ stepId, parentId: parentStep?.stepId, - title: step.shortTitle ?? step.title, + title: step.title, + subtitle: step.subtitle, category: step.category, params: step.params, stack: step.location ? [step.location] : [], diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index 50a8ddb4740d3..f25923e64cdce 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -273,7 +273,7 @@ export class TestTracing { }); } - appendBeforeActionForStep(options: { stepId: string, parentId?: string, title: string, category: TestStepCategory, params?: Record, stack: StackFrame[], group?: string }) { + appendBeforeActionForStep(options: { stepId: string, parentId?: string, title: string, subtitle?: string, category: TestStepCategory, params?: Record, stack: StackFrame[], group?: string }) { this._appendTraceEvent({ type: 'before', callId: options.stepId, @@ -283,6 +283,7 @@ export class TestTracing { class: 'Test', method: options.category, title: options.title, + subtitle: options.subtitle, params: Object.fromEntries(Object.entries(options.params || {}).map(([name, value]) => [name, generatePreview(value)])), stack: options.stack, group: options.group, diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts index 0b84a322939e1..7d677656b88c6 100644 --- a/packages/playwright/types/testReporter.d.ts +++ b/packages/playwright/types/testReporter.d.ts @@ -351,6 +351,7 @@ export interface JSONReportTestResult { export interface JSONReportTestStep { title: string; + subtitle?: string; duration: number; error: TestError | undefined; steps?: JSONReportTestStep[]; @@ -919,7 +920,23 @@ export interface TestStep { steps: Array; /** - * User-friendly test step title. + * User-friendly test step subtitle that complements the title, when available. For Playwright API calls, it is the + * target locator or the navigation url. For example, a `Click` step has the clicked locator as a subtitle. User + * interfaces typically render the subtitle next to the title or on a separate line. + * + * ```js + * // title `Click`, subtitle `getByRole('button')` + * await page.getByRole('button').click(); + * + * // title `Navigate`, subtitle `/index.html` + * await page.goto('https://example.com/index.html'); + * ``` + * + */ + subtitle?: string; + + /** + * User-friendly test step title, for example `Click` or `Navigate`. */ title: string; } diff --git a/packages/protocol/spec/api.yml b/packages/protocol/spec/api.yml index 8528cb0098259..96988d00a5ffc 100644 --- a/packages/protocol/spec/api.yml +++ b/packages/protocol/spec/api.yml @@ -21,7 +21,11 @@ APIRequestContext: commands: fetch: - title: '{method} "{url}"' + title: '{method}' + subtitle: '{url}' + renderParams: + - url + - method parameters: url: string encodedParams: string? diff --git a/packages/protocol/spec/browserContext.yml b/packages/protocol/spec/browserContext.yml index f7d96dbe2e526..2a56d0f463f1b 100644 --- a/packages/protocol/spec/browserContext.yml +++ b/packages/protocol/spec/browserContext.yml @@ -169,6 +169,8 @@ BrowserContext: setOffline: title: Set offline mode + renderParams: + - offline parameters: offline: boolean diff --git a/packages/protocol/spec/frame.yml b/packages/protocol/spec/frame.yml index a8dd00d337918..d0c28ffc15ad2 100644 --- a/packages/protocol/spec/frame.yml +++ b/packages/protocol/spec/frame.yml @@ -27,6 +27,7 @@ Frame: evalOnSelector: title: Evaluate + subtitle: '{selector}' parameters: selector: string strict: boolean? @@ -41,6 +42,7 @@ Frame: evalOnSelectorAll: title: Evaluate + subtitle: '{selector}' parameters: selector: string expression: string @@ -54,6 +56,8 @@ Frame: addScriptTag: title: Add script tag + renderParams: + - url parameters: url: string? content: string? @@ -66,6 +70,8 @@ Frame: addStyleTag: title: Add style tag + renderParams: + - url parameters: url: string? content: string? @@ -77,6 +83,7 @@ Frame: ariaSnapshot: title: Aria snapshot + subtitle: '{selector}' group: getter parameters: mode: @@ -92,6 +99,7 @@ Frame: ariaSnapshotJSON: title: Aria snapshot JSON + subtitle: '{selector}' group: getter parameters: mode: @@ -107,6 +115,7 @@ Frame: blur: title: Blur + subtitle: '{selector}' parameters: selector: string strict: boolean? @@ -117,6 +126,9 @@ Frame: check: title: Check + subtitle: '{selector}' + renderParams: + - position parameters: selector: string strict: boolean? @@ -137,6 +149,12 @@ Frame: click: title: Click + subtitle: '{selector}' + renderParams: + - button + - clickCount + - modifiers + - position parameters: selector: string strict: boolean? @@ -185,6 +203,9 @@ Frame: dragAndDrop: title: Drag and drop + renderParams: + - source:selector + - target:selector parameters: source: string target: string @@ -208,6 +229,7 @@ Frame: drop: title: Drop files or data onto an element + subtitle: '{selector}' parameters: selector: string strict: boolean? @@ -243,6 +265,11 @@ Frame: dblclick: title: Double click + subtitle: '{selector}' + renderParams: + - button + - modifiers + - position parameters: selector: string strict: boolean? @@ -281,6 +308,9 @@ Frame: dispatchEvent: title: Dispatch "{type}" + subtitle: '{selector}' + renderParams: + - type parameters: selector: string strict: boolean? @@ -317,6 +347,9 @@ Frame: fill: title: Fill "{value}" + subtitle: '{selector}' + renderParams: + - value parameters: selector: string strict: boolean? @@ -331,6 +364,7 @@ Frame: focus: title: Focus + subtitle: '{selector}' parameters: selector: string strict: boolean? @@ -365,6 +399,7 @@ Frame: getAttribute: title: Get attribute "{name}" + subtitle: '{selector}' group: getter parameters: selector: string @@ -377,7 +412,10 @@ Frame: pause: true goto: - title: Navigate to "{url}" + title: Navigate + subtitle: '{url}' + renderParams: + - url parameters: url: string waitUntil: LifecycleEvent? @@ -391,6 +429,10 @@ Frame: hover: title: Hover + subtitle: '{selector}' + renderParams: + - modifiers + - position parameters: selector: string strict: boolean? @@ -421,6 +463,7 @@ Frame: innerHTML: title: Get HTML + subtitle: '{selector}' group: getter parameters: selector: string @@ -433,6 +476,7 @@ Frame: innerText: title: Get inner text + subtitle: '{selector}' group: getter parameters: selector: string @@ -445,6 +489,7 @@ Frame: inputValue: title: Get input value + subtitle: '{selector}' group: getter parameters: selector: string @@ -457,6 +502,7 @@ Frame: isChecked: title: Is checked + subtitle: '{selector}' group: getter parameters: selector: string @@ -469,6 +515,7 @@ Frame: isDisabled: title: Is disabled + subtitle: '{selector}' group: getter parameters: selector: string @@ -481,6 +528,7 @@ Frame: isEnabled: title: Is enabled + subtitle: '{selector}' group: getter parameters: selector: string @@ -493,6 +541,7 @@ Frame: isHidden: title: Is hidden + subtitle: '{selector}' group: getter parameters: # Note: compared to other isFoo methods, isHidden is a one-shot operation without a timeout. @@ -506,6 +555,7 @@ Frame: isVisible: title: Is visible + subtitle: '{selector}' group: getter parameters: # Note: compared to other isFoo methods, isVisible is a one-shot operation without a timeout. @@ -519,6 +569,7 @@ Frame: isEditable: title: Is editable + subtitle: '{selector}' group: getter parameters: selector: string @@ -531,6 +582,9 @@ Frame: press: title: Press "{key}" + subtitle: '{selector}' + renderParams: + - key parameters: selector: string strict: boolean? @@ -546,6 +600,7 @@ Frame: querySelector: title: Query selector + subtitle: '{selector}' parameters: selector: string strict: boolean? @@ -556,6 +611,7 @@ Frame: querySelectorAll: title: Query selector all + subtitle: '{selector}' parameters: selector: string returns: @@ -567,6 +623,7 @@ Frame: queryCount: title: Query count + subtitle: '{selector}' parameters: selector: string returns: @@ -577,6 +634,9 @@ Frame: selectOption: title: Select option + subtitle: '{selector}' + renderParams: + - options parameters: selector: string strict: boolean? @@ -615,6 +675,9 @@ Frame: setInputFiles: title: Set input files + subtitle: '{selector}' + renderParams: + - files=localPaths parameters: selector: string strict: boolean? @@ -644,6 +707,10 @@ Frame: tap: title: Tap + subtitle: '{selector}' + renderParams: + - modifiers + - position parameters: selector: string strict: boolean? @@ -674,6 +741,7 @@ Frame: textContent: title: Get text content + subtitle: '{selector}' group: getter parameters: selector: string @@ -692,6 +760,9 @@ Frame: type: title: Type "{text}" + subtitle: '{selector}' + renderParams: + - text parameters: selector: string strict: boolean? @@ -706,6 +777,9 @@ Frame: uncheck: title: Uncheck + subtitle: '{selector}' + renderParams: + - position parameters: selector: string strict: boolean? @@ -726,6 +800,8 @@ Frame: waitForTimeout: title: Wait for timeout + renderParams: + - timeout=waitTimeout parameters: waitTimeout: float flags: @@ -733,6 +809,7 @@ Frame: waitForFunction: title: Wait for function + subtitle: '{selector}' parameters: expression: string isFunction: boolean? @@ -751,6 +828,9 @@ Frame: waitForSelector: title: Wait for selector + subtitle: '{selector}' + renderParams: + - state parameters: selector: string strict: boolean? @@ -769,6 +849,7 @@ Frame: expect: title: Expect "{expression}" + subtitle: '{selector}' parameters: selector: string? expression: string diff --git a/packages/protocol/spec/handles.yml b/packages/protocol/spec/handles.yml index fb38ee8a18dd4..9a358546f7809 100644 --- a/packages/protocol/spec/handles.yml +++ b/packages/protocol/spec/handles.yml @@ -88,6 +88,7 @@ ElementHandle: evalOnSelector: title: Evaluate + subtitle: '{selector}' parameters: selector: string strict: boolean? @@ -102,6 +103,7 @@ ElementHandle: evalOnSelectorAll: title: Evaluate + subtitle: '{selector}' parameters: selector: string expression: string @@ -123,6 +125,8 @@ ElementHandle: check: title: Check + renderParams: + - position parameters: force: boolean? scroll: @@ -141,6 +145,11 @@ ElementHandle: click: title: Click + renderParams: + - button + - clickCount + - modifiers + - position parameters: force: boolean? scroll: @@ -185,6 +194,10 @@ ElementHandle: dblclick: title: Double click + renderParams: + - button + - modifiers + - position parameters: force: boolean? scroll: @@ -221,6 +234,8 @@ ElementHandle: dispatchEvent: title: Dispatch event + renderParams: + - type parameters: type: string eventInit: SerializedArgument @@ -231,6 +246,8 @@ ElementHandle: fill: title: Fill "{value}" + renderParams: + - value parameters: value: string force: boolean? @@ -261,6 +278,9 @@ ElementHandle: hover: title: Hover + renderParams: + - modifiers + - position parameters: force: boolean? scroll: @@ -376,6 +396,8 @@ ElementHandle: press: title: Press "{key}" + renderParams: + - key parameters: key: string delay: float? @@ -389,6 +411,7 @@ ElementHandle: querySelector: title: Query selector + subtitle: '{selector}' parameters: selector: string strict: boolean? @@ -399,6 +422,7 @@ ElementHandle: querySelectorAll: title: Query selector all + subtitle: '{selector}' parameters: selector: string returns: @@ -410,6 +434,8 @@ ElementHandle: screenshot: title: Screenshot + renderParams: + - type parameters: type: type: enum? @@ -434,6 +460,8 @@ ElementHandle: selectOption: title: Select option + renderParams: + - options parameters: elements: type: array? @@ -470,6 +498,8 @@ ElementHandle: setInputFiles: title: Set input files + renderParams: + - files=localPaths parameters: # Only one of payloads, localPaths and streams should be present. payloads: @@ -497,6 +527,9 @@ ElementHandle: tap: title: Tap + renderParams: + - modifiers + - position parameters: force: boolean? scroll: @@ -534,6 +567,8 @@ ElementHandle: type: title: Type + renderParams: + - text parameters: text: string delay: float? @@ -546,6 +581,8 @@ ElementHandle: uncheck: title: Uncheck + renderParams: + - position parameters: force: boolean? scroll: @@ -564,6 +601,8 @@ ElementHandle: waitForElementState: title: Wait for state + renderParams: + - state parameters: state: type: enum @@ -580,6 +619,9 @@ ElementHandle: waitForSelector: title: Wait for selector + subtitle: '{selector}' + renderParams: + - state parameters: selector: string strict: boolean? diff --git a/packages/protocol/spec/page.yml b/packages/protocol/spec/page.yml index d4d41e5c1fecb..d5bbff375c60b 100644 --- a/packages/protocol/spec/page.yml +++ b/packages/protocol/spec/page.yml @@ -66,6 +66,12 @@ Page: emulateMedia: title: Emulate media + renderParams: + - media + - colorScheme + - reducedMotion + - forcedColors + - contrast parameters: media: type: enum? @@ -139,6 +145,7 @@ Page: registerLocatorHandler: title: Register locator handler + subtitle: '{selector}' parameters: selector: string noWaitAfter: boolean? @@ -169,6 +176,7 @@ Page: expectScreenshot: title: Expect screenshot + subtitle: '{locator.selector}' parameters: expected: binary? isNot: boolean @@ -206,6 +214,9 @@ Page: screenshot: title: Screenshot + renderParams: + - type + - fullPage parameters: type: type: enum? @@ -261,6 +272,9 @@ Page: setViewportSize: title: Set viewport size + renderParams: + - viewportSize.width + - viewportSize.height parameters: viewportSize: type: object @@ -273,6 +287,8 @@ Page: keyboardDown: title: Key down "{key}" + renderParams: + - key parameters: key: string flags: @@ -283,6 +299,8 @@ Page: keyboardUp: title: Key up "{key}" + renderParams: + - key parameters: key: string flags: @@ -293,6 +311,8 @@ Page: keyboardInsertText: title: Insert "{text}" + renderParams: + - text parameters: text: string flags: @@ -303,6 +323,8 @@ Page: keyboardType: title: Type "{text}" + renderParams: + - text parameters: text: string delay: float? @@ -314,6 +336,8 @@ Page: keyboardPress: title: Press "{key}" + renderParams: + - key parameters: key: string delay: float? @@ -325,6 +349,9 @@ Page: mouseMove: title: Mouse move + renderParams: + - x + - y parameters: x: float y: float @@ -337,6 +364,9 @@ Page: mouseDown: title: Mouse down + renderParams: + - button + - clickCount parameters: button: type: enum? @@ -353,6 +383,9 @@ Page: mouseUp: title: Mouse up + renderParams: + - button + - clickCount parameters: button: type: enum? @@ -369,6 +402,11 @@ Page: mouseClick: title: Click + renderParams: + - x + - y + - button + - clickCount parameters: x: float y: float @@ -388,6 +426,9 @@ Page: mouseWheel: title: Mouse wheel + renderParams: + - deltaX + - deltaY parameters: deltaX: float deltaY: float @@ -399,6 +440,9 @@ Page: touchscreenTap: title: Tap + renderParams: + - x + - y parameters: x: float y: float diff --git a/packages/protocol/spec/playwright.yml b/packages/protocol/spec/playwright.yml index 7eac6215db960..401a9cf153f23 100644 --- a/packages/protocol/spec/playwright.yml +++ b/packages/protocol/spec/playwright.yml @@ -507,6 +507,8 @@ Dialog: accept: title: Accept dialog + renderParams: + - promptText parameters: promptText: string? diff --git a/packages/protocol/spec/tracing.yml b/packages/protocol/spec/tracing.yml index a33e659cce54a..de9a644d9d287 100644 --- a/packages/protocol/spec/tracing.yml +++ b/packages/protocol/spec/tracing.yml @@ -39,6 +39,8 @@ Tracing: tracingGroup: title: Trace "{name}" + renderParams: + - name parameters: name: string location: diff --git a/packages/recorder/src/callLog.css b/packages/recorder/src/callLog.css index e519217f96c03..7a17277473554 100644 --- a/packages/recorder/src/callLog.css +++ b/packages/recorder/src/callLog.css @@ -72,11 +72,7 @@ text-overflow: ellipsis; } -.call-log-url { - color: var(--vscode-charts-blue); -} - -.call-log-selector { +.call-log-subtitle { color: var(--vscode-charts-orange); white-space: nowrap; } diff --git a/packages/recorder/src/callLog.tsx b/packages/recorder/src/callLog.tsx index bc28d82a673f5..92a46f3a4ac0b 100644 --- a/packages/recorder/src/callLog.tsx +++ b/packages/recorder/src/callLog.tsx @@ -19,16 +19,12 @@ import * as React from 'react'; import type { CallLog } from './recorderTypes'; import { clsx } from '@web/uiUtils'; import { msToString } from '@isomorphic/formatUtils'; -import { asLocator } from '@isomorphic/locatorGenerators'; -import type { Language } from '@isomorphic/locatorGenerators'; export type CallLogProps = { - language: Language; log: CallLog[]; }; export const CallLogView: React.FC = ({ - language, log, }) => { const messagesEndRef = React.useRef(null); @@ -41,19 +37,6 @@ export const CallLogView: React.FC = ({ {log.map(callLog => { const expandOverride = expandOverrides.get(callLog.id); const isExpanded = typeof expandOverride === 'boolean' ? expandOverride : callLog.status !== 'done'; - const locator = callLog.params.selector ? asLocator(language, callLog.params.selector) : null; - let titlePrefix = callLog.title; - let titleSuffix = ''; - if (callLog.title.startsWith('expect.to') || callLog.title.startsWith('expect.not.to')) { - titlePrefix = 'expect('; - titleSuffix = `).${callLog.title.substring('expect.'.length)}()`; - } else if (callLog.title.startsWith('locator.')) { - titlePrefix = ''; - titleSuffix = `.${callLog.title.substring('locator.'.length)}()`; - } else if (locator || callLog.params.url) { - titlePrefix = callLog.title + '('; - titleSuffix = ')'; - } return
{ @@ -61,10 +44,8 @@ export const CallLogView: React.FC = ({ newOverrides.set(callLog.id, !isExpanded); setExpandOverrides(newOverrides); }}> - { titlePrefix } - { callLog.params.url ? {callLog.params.url} : undefined } - { locator ? {`page.${locator}`} : undefined } - { titleSuffix } + { callLog.subtitle ? callLog.title + ' ' : callLog.title } + { callLog.subtitle ? {callLog.subtitle} : undefined } { typeof callLog.duration === 'number' ? — {msToString(callLog.duration)} : undefined}
diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index 548649d65bb4b..b4154039fdc84 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -256,7 +256,7 @@ export const Recorder: React.FC = ({}) => { { id: 'log', title: 'Log', - render: () => + render: () => }, { id: 'aria', diff --git a/packages/recorder/src/recorderTypes.d.ts b/packages/recorder/src/recorderTypes.d.ts index 69dea99fb7985..920a0baac0e9f 100644 --- a/packages/recorder/src/recorderTypes.d.ts +++ b/packages/recorder/src/recorderTypes.d.ts @@ -64,15 +64,12 @@ export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused'; export type CallLog = { id: string; title: string; + subtitle?: string; messages: string[]; status: CallLogStatus; error?: string; reveal?: boolean; duration?: number; - params: { - url?: string; - selector?: string; - }; }; export type SourceHighlight = { diff --git a/packages/trace-viewer/src/ui/actionList.css b/packages/trace-viewer/src/ui/actionList.css index f600b25cceffb..7161559c62af6 100644 --- a/packages/trace-viewer/src/ui/actionList.css +++ b/packages/trace-viewer/src/ui/actionList.css @@ -24,7 +24,7 @@ text-overflow: ellipsis; } -.action-title-selector { +.action-title-subtitle { text-overflow: ellipsis; overflow: hidden; color: var(--vscode-tab-inactiveForeground); diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index bf6e2ff314ca1..17ed0cb07ee37 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -20,7 +20,7 @@ import { msToString } from '@isomorphic/formatUtils'; import * as React from 'react'; import './actionList.css'; import { buildActionTree } from '@isomorphic/trace/traceModel'; -import { asLocatorDescription, type Language } from '@isomorphic/locatorGenerators'; +import { type Language } from '@isomorphic/locatorGenerators'; import type { TreeState } from '@web/components/treeView'; import { TreeView } from '@web/components/treeView'; import type { ActionTreeItem, TraceModel } from '@isomorphic/trace/traceModel'; @@ -30,7 +30,7 @@ import type { Boundaries } from './geometry'; import { ToolbarButton } from '@web/components/toolbarButton'; import { testStatusIcon } from './testUtils'; import { getMetainfo } from '@isomorphic/protocolMetainfo'; -import { formatProtocolParam } from '@isomorphic/protocolFormatter'; +import { formatProtocolParam, renderSubtitleForCall } from '@isomorphic/protocolFormatter'; export interface ActionListProps { actions: ActionEntry[], @@ -90,12 +90,13 @@ export const ActionList: React.FC = ({ const timeVisible = !selectedTime || !item.action || (item.action.startTime <= selectedTime.maximum && item.action.endTime >= selectedTime.minimum); if (!timeVisible) return false; - const title = renderTitleForCall(item.action).title; if (!actionFilterText) return true; - const isIncluded = title.toLowerCase().includes(actionFilterText.toLowerCase()); + const { title, subtitle } = renderTitleForCall(item.action, sdkLanguage); + const text = subtitle ? `${title} ${subtitle}` : title; + const isIncluded = text.toLowerCase().includes(actionFilterText.toLowerCase()); return isIncluded ? true : 'if-needed'; - }, [selectedTime, actionFilterText]); + }, [selectedTime, actionFilterText, sdkLanguage]); const onSelectedAction = React.useCallback((item: ActionTreeItem) => { onSelected?.(item.action); @@ -147,8 +148,6 @@ export const renderAction = ( const { errors, warnings } = model?.stats(action) ?? { errors: 0, warnings: 0 }; const badgeLabel = [pluralize(errors, 'error'), pluralize(warnings, 'warning')].filter(Boolean).join(', '); - const locator = action.params.selector ? asLocatorDescription(sdkLanguage || 'javascript', action.params.selector) : undefined; - const isSkipped = action.class === 'Test' && action.method === 'test.step' && action.annotations?.some(a => a.type === 'skip'); let time: string = ''; if (action.endTime) @@ -157,7 +156,7 @@ export const renderAction = ( time = 'Timed out'; else if (!isLive) time = '-'; - const { elements, title } = renderTitleForCall(action); + const { elements, title, subtitle } = renderTitleForCall(action, sdkLanguage); return
{elements} @@ -174,11 +173,11 @@ export const renderAction = ( {!!warnings && {warnings}} }
- {locator &&
{locator}
} + {subtitle &&
{subtitle}
}
; }; -export function renderTitleForCall(action: ActionTraceEvent, sdkLanguage?: Language): { elements: React.ReactNode[], title: string } { +export function renderTitleForCall(action: ActionTraceEvent, sdkLanguage?: Language): { elements: React.ReactNode[], title: string, subtitle?: string } { let titleFormat = action.title ?? getMetainfo({ type: action.class, method: action.method })?.title ?? action.method; titleFormat = titleFormat.replace(/\n/g, ' '); @@ -195,7 +194,7 @@ export function renderTitleForCall(action: ActionTraceEvent, sdkLanguage?: Langu elements.push(chunk); title.push(chunk); - const param = formatProtocolParam(action.params, quotedText); + const param = formatProtocolParam(action.params, quotedText, sdkLanguage); if (param === undefined) { elements.push(fullMatch); title.push(fullMatch); @@ -215,12 +214,8 @@ export function renderTitleForCall(action: ActionTraceEvent, sdkLanguage?: Langu title.push(chunk); } - const locator = action.params.selector ? asLocatorDescription(sdkLanguage || 'javascript', action.params.selector) : undefined; - if (locator) { - title.push(' '); - title.push(locator); - } - return { elements, title: title.join('') }; + const subtitle = renderSubtitleForCall({ type: action.class, method: action.method, params: action.params, subtitle: action.subtitle }, sdkLanguage); + return { elements, title: title.join(''), subtitle }; } function pluralize(count: number, noun: string): string { diff --git a/packages/trace-viewer/src/ui/callTab.tsx b/packages/trace-viewer/src/ui/callTab.tsx index 2465f90a72330..1e49091ac1a87 100644 --- a/packages/trace-viewer/src/ui/callTab.tsx +++ b/packages/trace-viewer/src/ui/callTab.tsx @@ -41,11 +41,11 @@ export const CallTab: React.FunctionComponent<{ const startTimeMillis = action.startTime - startTimeOffset; const startTime = msToString(startTimeMillis); - const { title } = renderTitleForCall(action); + const { title, subtitle } = renderTitleForCall(action, sdkLanguage); return (
-
{title}
+
{subtitle ? `${title} ${subtitle}` : title}
Time
{renderProperty({ name: 'start', type: 'literal', text: startTime })} {renderProperty({ name: 'duration', type: 'literal', text: renderDuration(action) })} diff --git a/packages/trace/src/trace.ts b/packages/trace/src/trace.ts index a0ef448183be5..4ef8f0c6e21c6 100644 --- a/packages/trace/src/trace.ts +++ b/packages/trace/src/trace.ts @@ -135,6 +135,7 @@ export type BeforeActionTraceEvent = { callId: string; startTime: number; title?: string; + subtitle?: string; class: string; method: string; params: Record; diff --git a/tests/config/utils.ts b/tests/config/utils.ts index eca03a8006092..4f96e4d811b8d 100644 --- a/tests/config/utils.ts +++ b/tests/config/utils.ts @@ -159,7 +159,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso return { events, resources, - actions: actionObjects.map(a => iso.renderTitleForCall({ ...a, type: a.class })), + actions: actionObjects.map(a => iso.renderFullTitleForCall({ ...a, type: a.class })), actionObjects, stacks, }; diff --git a/tests/library/inspector/pause.spec.ts b/tests/library/inspector/pause.spec.ts index 0f7c3bd280532..3e79cdc400a6f 100644 --- a/tests/library/inspector/pause.spec.ts +++ b/tests/library/inspector/pause.spec.ts @@ -276,7 +276,7 @@ it.describe('pause', () => { await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")'); expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', - 'Click(page.locator(\'button\'))- XXms', + 'Click locator(\'button\')- XXms', 'Pause', ]); await recorderPage.click('[title="Resume (F8)"]'); @@ -324,8 +324,8 @@ it.describe('pause', () => { await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")'); expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', - 'Expect "toHaveText"(page.locator(\'button\'))- XXms', - 'Expect "not toHaveText"(page.locator(\'button\'))- XXms', + 'Expect "toHaveText" locator(\'button\')- XXms', + 'Expect "not toHaveText" locator(\'button\')- XXms', 'Pause', ]); await recorderPage.click('[title="Resume (F8)"]'); @@ -368,7 +368,7 @@ it.describe('pause', () => { expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', 'Wait for event "console"- XXms', - 'Click(page.getByRole(\'button\', { name: \'Submit\' }))- XXms', + 'Click getByRole(\'button\', { name: \'Submit\' })- XXms', 'Pause', ]); await recorderPage.click('[title="Resume (F8)"]'); @@ -387,7 +387,7 @@ it.describe('pause', () => { await recorderPage.waitForSelector('.source-line-error-underline'); expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', - 'Is checked(page.getByRole(\'button\'))- XXms', + 'Is checked getByRole(\'button\')- XXms', 'waiting for getByRole(\'button\')', 'error: Error: Not a checkbox or radio button', ]); @@ -407,8 +407,8 @@ it.describe('pause', () => { await recorderPage.waitForSelector('.source-line-error-underline'); expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', - 'Expect "toHaveText"(page.getByRole(\'button\'))- XXms', - 'Expect "toHaveText" with timeout 1ms', + 'Expect "toHaveText" getByRole(\'button\')- XXms', + 'Expect "toHaveText" getByRole(\'button\') with timeout 1ms', 'waiting for getByRole(\'button\')', 'error: Expect failed', ]); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index d9a7a02f15f8d..7c7d855c88377 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -178,7 +178,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => { const traceViewer = await showTraceViewer(traceFile); await expect(traceViewer.actionTitles).toHaveText([ /Create page/, - /Navigate to "data:"/, + /Navigate.*data:/, /^Expect "toHaveTitle"[\d]+ms$/, /^Expect "toHaveURL"[\d]+ms$/, /Set content/, @@ -191,7 +191,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => { /Wait for navigation/, /Wait for event "response"/, /Wait for timeout/, - /Navigate to "\/frames\/frame.html"/, + /Navigate.*\/frames\/frame.html/, /Set viewport size/, /Hover/, /Close page/, @@ -258,7 +258,7 @@ test('should open uncompressed trace directory', async ({ showTraceViewer }) => const traceViewer = await showTraceViewer(traceDir); await expect(traceViewer.actionTitles).toHaveText([ /Create page/, - /Navigate to "data:"/, + /Navigate.*data:/, /^Expect "toHaveTitle"[\d]+ms$/, /^Expect "toHaveURL"[\d]+ms$/, /Set content/, @@ -271,7 +271,7 @@ test('should open uncompressed trace directory', async ({ showTraceViewer }) => /Wait for navigation/, /Wait for event "response"/, /Wait for timeout/, - /Navigate to "\/frames\/frame.html"/, + /Navigate.*\/frames\/frame.html/, /Set viewport size/, /Hover/, /Close page/, @@ -1855,7 +1855,7 @@ test('should not record route actions', { }); await expect(traceViewer.actionTitles).toHaveText([ - /Navigate to "\/empty.html"/, + /Navigate.*\/empty.html/, ]); }); @@ -2256,7 +2256,7 @@ test('should render blob trace received from message', async ({ showTraceViewer await expect(traceViewer.page.locator('.drop-target')).not.toBeVisible(); await expect(traceViewer.actionTitles).toHaveText([ /Create page/, - /Navigate to "data:"/, + /Navigate.*data:/, /toHaveTitle/, /toHaveURL/, /Set content/, @@ -2269,7 +2269,7 @@ test('should render blob trace received from message', async ({ showTraceViewer /Wait for navigation/, /Wait for event "response"/, /Wait for timeout/, - /Navigate to "\/frames\/frame.html"/, + /Navigate.*\/frames\/frame.html/, /Set viewport size/, /Hover/, /Close page/, @@ -2378,7 +2378,7 @@ test('should filter actions', async ({ runAndTrace, page }) => { }); await expect(traceViewer.actionTitles).toHaveText([ - /Navigate to/, + /Navigate/, /Expect "toBeChecked"/, ]); await expect(traceViewer.page.getByText('3 hidden', { exact: true })).toBeVisible(); @@ -2393,7 +2393,7 @@ test('should filter actions', async ({ runAndTrace, page }) => { await traceViewer.page.locator('.setting').getByText('Getters').click(); await expect(traceViewer.actionTitles).toHaveText([ - /Navigate to/, + /Navigate/, /Get attribute "checked"/, /Expect "toBeChecked"/, ]); @@ -2402,7 +2402,7 @@ test('should filter actions', async ({ runAndTrace, page }) => { await traceViewer.page.locator('.setting').getByText('Network routes').click(); await expect(traceViewer.actionTitles).toHaveText([ /Route requests/, - /Navigate to/, + /Navigate/, /Fulfill request/, /Get attribute "checked"/, /Expect "toBeChecked"/, diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index 57e24c2baaa8c..898379927a7ea 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -42,14 +42,14 @@ test('should collect trace with resources, but no js', async ({ context, page, s const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.zip')); expect(events[0].type).toBe('context-options'); expect(actions).toEqual([ - 'Navigate to "/frames/frame.html"', + 'Navigate /frames/frame.html', 'Set content', - 'Click', + `Click locator('text="Click"')`, 'Mouse move', 'Double click', 'Insert "abc"', - 'Navigate to "/input/fileupload.html"', - 'Set input files', + 'Navigate /input/fileupload.html', + `Set input files locator('input[type="file"]')`, 'Wait for timeout', 'Close page', ]); @@ -85,9 +85,9 @@ test('should use the correct title for event driven callbacks', async ({ context expect(events[0].type).toBe('context-options'); expect(actions).toEqual([ 'Route requests', - 'Navigate to "/empty.html"', + 'Navigate /empty.html', 'Continue request', - 'Navigate to "/grid.html"', + 'Navigate /grid.html', 'Evaluate', 'Reload', 'Evaluate', @@ -222,14 +222,14 @@ test('should record context API request trace independently', async ({ context, await context.request.tracing.stop({ path: apiTracePath }); const browserTrace = await parseTraceRaw(browserTracePath); - expect(browserTrace.actions).toContain('Navigate to "/one-style.html"'); - expect(browserTrace.actions).not.toContain('POST "/simple.json"'); + expect(browserTrace.actions).toContain('Navigate /one-style.html'); + expect(browserTrace.actions).not.toContain('POST /simple.json'); expect(browserTrace.events.some(event => event.type === 'resource-snapshot' && event.snapshot.request.url.endsWith('/simple.json'))).toBe(false); expect(browserTrace.events.some(event => event.type === 'resource-snapshot' && event.snapshot.request.url.endsWith('/one-style.html'))).toBe(true); const apiTrace = await parseTraceRaw(apiTracePath); - expect(apiTrace.actions).toContain('POST "/simple.json"'); - expect(apiTrace.actions).not.toContain('Navigate to "/one-style.html"'); + expect(apiTrace.actions).toContain('POST /simple.json'); + expect(apiTrace.actions).not.toContain('Navigate /one-style.html'); const apiAction = apiTrace.actionObjects.find(action => action.class === 'APIRequestContext' && action.method === 'fetch')!; expect(relativeStack(apiAction, apiTrace.stacks)).toEqual(['tracing.spec.ts']); expect(apiTrace.events.filter(event => event.type === 'resource-snapshot').map(event => event.snapshot.request.url)).toEqual([apiURL]); @@ -252,9 +252,9 @@ test('should collect two traces', async ({ context, page, server }, testInfo) => const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace1.zip')); expect(events[0].type).toBe('context-options'); expect(actions).toEqual([ - 'Navigate to "/empty.html"', + 'Navigate /empty.html', 'Set content', - 'Click', + `Click locator('text="Click"')`, ]); } @@ -262,7 +262,7 @@ test('should collect two traces', async ({ context, page, server }, testInfo) => const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace2.zip')); expect(events[0].type).toBe('context-options'); expect(actions).toEqual([ - 'Double click', + `Double click locator('text="Click"')`, 'Close page', ]); } @@ -296,7 +296,7 @@ test('should respect tracesDir and name', async ({ browserType, server, mode }, { const { resources, actions } = await parseTraceRaw(testInfo.outputPath('trace1.zip')); - expect(actions).toEqual(['Navigate to "/one-style.html"']); + expect(actions).toEqual(['Navigate /one-style.html']); expect(resourceNames(resources)).toEqual([ 'resources/XXX.css', 'resources/XXX.html', @@ -308,7 +308,7 @@ test('should respect tracesDir and name', async ({ browserType, server, mode }, { const { resources, actions } = await parseTraceRaw(testInfo.outputPath('trace2.zip')); - expect(actions).toEqual(['Navigate to "/har.html"']); + expect(actions).toEqual(['Navigate /har.html']); expect(resourceNames(resources)).toEqual([ 'resources/XXX.css', 'resources/XXX.html', @@ -549,7 +549,7 @@ test('should include interrupted actions', async ({ context, page, server }, tes await context.close(); const { actions } = await parseTraceRaw(testInfo.outputPath('trace.zip')); - expect(actions).toContain('Click'); + expect(actions).toContain(`Click locator('text="ClickNoButton"')`); }); test('should throw when starting with different options', async ({ context }) => { @@ -590,8 +590,8 @@ test('should work with multiple chunks', async ({ context, page, server }, testI expect(trace1.events[0].type).toBe('context-options'); expect(trace1.actions).toEqual([ 'Set content', - 'Click', - 'Click', + `Click locator('text="Click"')`, + `Click locator('text="ClickNoButton"')`, 'Evaluate', ]); expect(trace1.events.some(e => e.type === 'frame-snapshot')).toBeTruthy(); @@ -600,7 +600,7 @@ test('should work with multiple chunks', async ({ context, page, server }, testI const trace2 = await parseTraceRaw(testInfo.outputPath('trace2.zip')); expect(trace2.events[0].type).toBe('context-options'); expect(trace2.actions).toEqual([ - 'Hover', + `Hover locator('text="Click"')`, ]); expect(trace2.events.some(e => e.type === 'frame-snapshot')).toBeTruthy(); expect(trace2.events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css'))).toBeTruthy(); @@ -648,7 +648,7 @@ test('should ignore iframes in head', async ({ context, page, server }, testInfo await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') }); const trace = await parseTraceRaw(testInfo.outputPath('trace.zip')); - expect(trace.actions).toEqual(['Click']); + expect(trace.actions).toEqual([`Click locator('button')`]); expect(trace.events.find(e => e.type === 'frame-snapshot')).toBeTruthy(); expect(trace.events.find(e => e.type === 'frame-snapshot' && JSON.stringify(e.snapshot.html).includes('IFRAME'))).toBeFalsy(); }); diff --git a/tests/mcp/cli-test.spec.ts b/tests/mcp/cli-test.spec.ts index 7a246c7d05f8d..aba99d794e582 100644 --- a/tests/mcp/cli-test.spec.ts +++ b/tests/mcp/cli-test.spec.ts @@ -58,14 +58,14 @@ test('debug test and snapshot', async ({ cliEnv, cli, childProcess }) => { const { output: stepOutput } = await cli(`--session=${session}`, 'step-over'); expect(stepOutput).toContain('### Paused'); - expect(stepOutput).toContain(`- Expect "toBeVisible" at subdir${path.sep}a.test.ts:5`); + expect(stepOutput).toContain(`- Expect "toBeVisible" getByRole('button', { name: 'Submit' }) at subdir${path.sep}a.test.ts:5`); const snapshotResult = await cli(`--session=${session}`, 'snapshot'); expect(snapshotResult.inlineSnapshot).toContain('button "Submit"'); const { output: pauseAtOutput } = await cli(`--session=${session}`, 'pause-at', 'a.test.ts:7'); expect(pauseAtOutput).toContain('### Paused'); - expect(pauseAtOutput).toContain(`- Expect "toBeVisible" at subdir${path.sep}a.test.ts:7`); + expect(pauseAtOutput).toContain(`- Expect "toBeVisible" getByRole('button', { name: 'Close' }) at subdir${path.sep}a.test.ts:7`); await cli(`--session=${session}`, 'resume'); diff --git a/tests/mcp/test-debug.spec.ts b/tests/mcp/test-debug.spec.ts index 880d73f3f5ae9..4389d0443482c 100644 --- a/tests/mcp/test-debug.spec.ts +++ b/tests/mcp/test-debug.spec.ts @@ -64,7 +64,7 @@ Timeout: 1000ms Error: element(s) not found Call log: - - Expect "toBeVisible" with timeout 1000ms + - Expect "toBeVisible" getByRole('button', { name: 'Missing' }) with timeout 1000ms - waiting for getByRole('button', { name: 'Missing' }) @@ -183,7 +183,7 @@ Timeout: 1000ms Error: element(s) not found Call log: - - Expect "toBeVisible" with timeout 1000ms + - Expect "toBeVisible" getByRole('button', { name: 'Missing' }) with timeout 1000ms - waiting for getByRole('button', { name: 'Missing' }) diff --git a/tests/page/expect-boolean.spec.ts b/tests/page/expect-boolean.spec.ts index f2eddbc3f1b6e..043c55f695d9f 100644 --- a/tests/page/expect-boolean.spec.ts +++ b/tests/page/expect-boolean.spec.ts @@ -61,7 +61,7 @@ Locator: locator('input') Expected: checked Received: unchecked Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" locator('input') with timeout 1000ms`); }); test('with not', async ({ page }) => { @@ -86,7 +86,7 @@ Locator: locator('input') Expected: not checked Received: checked Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" locator('input') with timeout 1000ms`); expect(stripAnsi(error.message)).toContain(`locator resolved to `); }); @@ -100,7 +100,7 @@ Locator: locator('input') Expected: unchecked Received: checked Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" locator('input') with timeout 1000ms`); expect(stripAnsi(error.message)).toContain(`locator resolved to `); }); @@ -114,7 +114,7 @@ Locator: locator('input') Expected: indeterminate Received: unchecked Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" locator('input') with timeout 1000ms`); }); test('fail missing', async ({ page }) => { @@ -129,7 +129,7 @@ Timeout: 1000ms Error: element(s) not found Call log: - - Expect "not toBeChecked" with timeout 1000ms + - Expect "not toBeChecked" locator('input2') with timeout 1000ms - waiting for locator('input2') `); }); @@ -475,7 +475,7 @@ Timeout: 1000ms Error: element(s) not found Call log: - - Expect "not toBeHidden" with timeout 1000ms + - Expect "not toBeHidden" locator('button') with timeout 1000ms `); }); diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index a118d402717b2..76d66d5287feb 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -73,7 +73,7 @@ Locator: locator('span') Expected: 0 Received: 1 Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveCount" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveCount" locator('span') with timeout 1000ms`); }); test('fail zero 2', async ({ page }) => { @@ -86,7 +86,7 @@ Locator: locator('span') Expected: not 1 Received: 1 Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveCount" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveCount" locator('span') with timeout 1000ms`); }); }); @@ -124,7 +124,7 @@ Locator: locator('div') Expected: "error" Received: "string" Timeout: 200ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`); }); test('pass number', async ({ page }) => { @@ -145,7 +145,7 @@ Locator: locator('div') Expected: 1 Received: 2021 Timeout: 200ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`); }); test('pass boolean', async ({ page }) => { @@ -166,7 +166,7 @@ Locator: locator('div') Expected: true Received: false Timeout: 200ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`); }); test('pass boolean 2', async ({ page }) => { @@ -187,7 +187,7 @@ Locator: locator('div') Expected: true Received: false Timeout: 200ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`); }); test('pass undefined', async ({ page }) => { @@ -249,7 +249,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" locator('div') with timeout 1000ms`); }); test('pass with array', async ({ page }) => { @@ -264,7 +264,7 @@ Call log: const error = await expect(locator).toHaveClass(['foo', 'bar', /[a-z]az/], { timeout: 1000 }).catch(e => e); expect(stripAnsi(error.message)).toContain(`expect(locator).toHaveClass(expected) failed`); expect(stripAnsi(error.message)).toContain(`Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" locator('div') with timeout 1000ms`); }); }); @@ -298,7 +298,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" locator('div') with timeout 1000ms`); }); test('pass with array', async ({ page }) => { @@ -316,7 +316,7 @@ Call log: const error = await expect(locator).toContainClass(['foo', 'bar', 'baz'], { timeout: 1000 }).catch(e => e); expect(stripAnsi(error.message)).toContain(`expect(locator).toContainClass(expected) failed`); expect(stripAnsi(error.message)).toContain(`Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" locator('div') with timeout 1000ms`); }); }); @@ -442,7 +442,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" locator('#node') with timeout 1000ms`); } { const error = await expect(locator).toHaveAttribute('disabled', /.*/, { timeout: 1000 }).catch(e => e); @@ -455,7 +455,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" locator('#node') with timeout 1000ms`); } await expect(locator).not.toHaveAttribute('disabled', ''); await expect(locator).not.toHaveAttribute('disabled', /.*/); @@ -477,7 +477,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" locator('#node') with timeout 1000ms`); } { const error = await expect(locator).not.toHaveAttribute('checked', /.*/, { timeout: 1000 }).catch(e => e); @@ -487,7 +487,7 @@ Locator: locator('#node') Expected pattern: not /.*/ Received string: "" Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" locator('#node') with timeout 1000ms`); } }); diff --git a/tests/page/expect-timeout.spec.ts b/tests/page/expect-timeout.spec.ts index 6753716a1c25b..04f8b6544d61c 100644 --- a/tests/page/expect-timeout.spec.ts +++ b/tests/page/expect-timeout.spec.ts @@ -136,7 +136,7 @@ Expected: visible Error: element(s) not found Call log: - - Expect "toBeVisible" with timeout 5000ms + - Expect "toBeVisible" locator('span') with timeout 5000ms - waiting for locator('span') - operation was aborted: stop it`); }); diff --git a/tests/page/expect-to-have-text.spec.ts b/tests/page/expect-to-have-text.spec.ts index 081e9a2ecafb7..e11e7d813c739 100644 --- a/tests/page/expect-to-have-text.spec.ts +++ b/tests/page/expect-to-have-text.spec.ts @@ -259,7 +259,7 @@ test.describe('toHaveText with array', () => { const error = await expect(locator).not.toHaveText([], { timeout: 1000 }).catch(e => e); expect(stripAnsi(error.message)).toContain(`expect(locator).not.toHaveText(expected)`); expect(stripAnsi(error.message)).toContain(`Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveText" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveText" locator('p') with timeout 1000ms`); }); test('pass eventually empty', async ({ page }) => { @@ -289,7 +289,7 @@ Timeout: 1000ms ] Call log: - - Expect \"toHaveText\" with timeout 1000ms + - Expect \"toHaveText\" locator('div') with timeout 1000ms - waiting for locator('div') `); expect(stripAnsi(error.message)).toContain('locator resolved to 2 elements'); diff --git a/tests/playwright-test/playwright.reuse.browser.spec.ts b/tests/playwright-test/playwright.reuse.browser.spec.ts index 3f8ad95789ddc..5a3af6921abbb 100644 --- a/tests/playwright-test/playwright.reuse.browser.spec.ts +++ b/tests/playwright-test/playwright.reuse.browser.spec.ts @@ -104,11 +104,14 @@ test('should produce correct test steps', async ({ runInlineTest, runServer }) = const result = await runInlineTest({ 'reporter.ts': ` class Reporter { + stepTitle(step) { + return step.subtitle ? step.title + ' ' + step.subtitle : step.title; + } onStepBegin(test, result, step) { - console.log('%% onStepBegin [' + step.category + '] ' + step.title); + console.log('%% onStepBegin [' + step.category + '] ' + this.stepTitle(step)); } onStepEnd(test, result, step) { - console.log('%% onStepEnd [' + step.category + '] ' + step.title); + console.log('%% onStepEnd [' + step.category + '] ' + this.stepTitle(step)); } } module.exports = Reporter; @@ -137,8 +140,8 @@ test('should produce correct test steps', async ({ runInlineTest, runServer }) = 'onStepEnd [pw:api] Create page', 'onStepEnd [fixture] Fixture "page"', 'onStepEnd [hook] Before Hooks', - 'onStepBegin [pw:api] Navigate to "about:blank"', - 'onStepEnd [pw:api] Navigate to "about:blank"', + 'onStepBegin [pw:api] Navigate about:blank', + 'onStepEnd [pw:api] Navigate about:blank', 'onStepBegin [pw:api] Evaluate', 'onStepEnd [pw:api] Evaluate', 'onStepBegin [hook] After Hooks', diff --git a/tests/playwright-test/playwright.reuse.spec.ts b/tests/playwright-test/playwright.reuse.spec.ts index 047ec35d2952c..b6bae8cab2b45 100644 --- a/tests/playwright-test/playwright.reuse.spec.ts +++ b/tests/playwright-test/playwright.reuse.spec.ts @@ -139,7 +139,7 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline ' Fixture "page"', ' Create page', 'Set content', - 'Click', + `Click locator('button')`, 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -154,8 +154,8 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline ' Fixture "page"', 'Expect "toBe"', 'Set content', - 'Fill "value"', - 'Click', + `Fill "value" locator('input')`, + `Click locator('input')`, 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -485,15 +485,15 @@ test('should reset tracing', async ({ runInlineTest }, testInfo) => { const trace1 = await parseTrace(traceFile1); expect(trace1.model.renderActionTree()).toEqual([ 'Set content', - 'Click', + `Click locator('button')`, ]); expect(trace1.snapshots.snapshotsForTest().length).toBeGreaterThan(0); const trace2 = await parseTrace(traceFile2); expect(trace2.model.renderActionTree()).toEqual([ 'Set content', - 'Fill "value"', - 'Click', + `Fill "value" locator('input')`, + `Click locator('input')`, ]); expect(trace1.snapshots.snapshotsForTest().length).toBeGreaterThan(0); }); diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts index 4ed1a3874a161..31319a666793a 100644 --- a/tests/playwright-test/playwright.trace.spec.ts +++ b/tests/playwright-test/playwright.trace.spec.ts @@ -97,8 +97,8 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => { ' Create context', ' Fixture "page"', ' Create page', - 'Navigate to "about:blank"', - 'GET "/empty.html"', + 'Navigate about:blank', + 'GET /empty.html', 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -109,7 +109,7 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => { expect(trace2.model.renderActionTree()).toEqual([ 'Before Hooks', 'Create request context', - 'GET "/empty.html"', + 'GET /empty.html', 'After Hooks', ]); const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip')); @@ -121,8 +121,8 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => { ' Create context', ' Fixture "page"', ' Create page', - 'Navigate to "about:blank"', - 'GET "/empty.html"', + 'Navigate about:blank', + 'GET /empty.html', 'Expect "toBe"', 'After Hooks', ' Fixture "page"', @@ -412,7 +412,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve ' Create context', ' Fixture "page"', ' Create page', - 'Navigate to "about:blank"', + 'Navigate about:blank', 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -420,7 +420,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve ' afterAll hook', ' Fixture "request"', ' Create request context', - ' GET "/empty.html"', + ' GET /empty.html', ' Fixture "request"', 'Worker Cleanup', ' Fixture "browser"', @@ -538,7 +538,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa }, { trace: 'retain-on-failure' }); const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain('Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -560,7 +560,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa }, { trace: 'retain-on-failure' }); const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain(' Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain(' Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -580,7 +580,7 @@ test(`trace:retain-on-failure should create trace if request context is disposed }, { trace: 'retain-on-failure' }); const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('GET "/empty.html"'); + expect(trace.model.renderActionTree()).toContain('GET /empty.html'); expect(result.failed).toBe(1); }); @@ -702,11 +702,11 @@ test('should expand expect.toPass', async ({ runInlineTest }, testInfo) => { ' Fixture "page"', ' Create page', 'Expect "toPass"', - ' Navigate to "data:"', + ' Navigate data:', ' Expect "toBe"', - ' Navigate to "data:"', + ' Navigate data:', ' Expect "toBe"', - ' Navigate to "data:"', + ' Navigate data:', ' Expect "toBe"', 'After Hooks', ' Fixture "page"', @@ -871,7 +871,7 @@ test('should use custom expect message in trace', async ({ runInlineTest }, test ' Create context', ' Fixture "page"', ' Create page', - 'expect to have text: find a hotel', + `expect to have text: find a hotel getByRole('button', { name: 'Find a hotel' })`, 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -1187,7 +1187,7 @@ test('trace:retain-on-first-failure should create trace but only on first failur const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain('Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -1204,7 +1204,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef }, { trace: 'retain-on-first-failure' }); const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain('Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -1223,7 +1223,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef }, { trace: 'retain-on-first-failure' }); const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain(' Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain(' Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -1240,7 +1240,7 @@ test('trace:retain-on-first-failure should create trace if request context is di }, { trace: 'retain-on-first-failure' }); const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('GET "/empty.html"'); + expect(trace.model.renderActionTree()).toContain('GET /empty.html'); expect(result.failed).toBe(1); }); @@ -1387,9 +1387,9 @@ test('should not nest top level expect into unfinished api calls ', { ' Create context', ' Fixture "page"', ' Create page', - 'Navigate to "/index"', - 'GET "/hang"', - 'Expect "toBeVisible"', + 'Navigate /index', + 'GET /hang', + `Expect "toBeVisible" getByText('Hello!')`, 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -1464,9 +1464,9 @@ test('should record trace snapshot for more obscure commands', async ({ runInlin ' Launch browser', 'Create page', 'Set content', - 'Query count', + `Query count locator('div')`, 'Expect "toBe"', - 'Bounding box', + `Bounding box locator('div')`, 'After Hooks', ]); diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 3a6912791a597..25a6c659aa4fd 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -1263,7 +1263,7 @@ test('preserve step params', async ({ runInlineTest, mergeReports }) => { class ParamsReporter { onStepEnd(test, result, step) { if (step.category === 'test.step' || step.title.startsWith('Navigate')) - console.log('%%' + step.title + ' | ' + JSON.stringify(step.params)); + console.log('%%' + (step.subtitle ? step.title + ' ' + step.subtitle : step.title) + ' | ' + JSON.stringify(step.params)); } } module.exports = ParamsReporter; @@ -1285,7 +1285,7 @@ test('preserve step params', async ({ runInlineTest, mergeReports }) => { const { exitCode, outputLines } = await mergeReports(reportDir, undefined, { additionalArgs: ['--reporter', './params-reporter.js'] }); expect(exitCode).toBe(0); expect(outputLines).toEqual([ - `Navigate to "about:blank" | {"url":"about:blank"}`, + `Navigate about:blank | {"url":"about:blank"}`, `my step | {"foo":"bar","count":7}`, ]); }); diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index d282e54708301..b4e06167548a6 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -779,10 +779,12 @@ for (const useIntermediateMergeReport of [true, false] as const) { await page.getByRole('link', { name: 'View Trace' }).click(); // Trace viewer should not hang here when displaying parallal requests. - await expect(page.getByTestId('actions-tree')).toContainText('GET'); - await page.getByText('GET "/empty.html"').nth(2).click(); - await page.getByText('GET "/empty.html"').nth(1).click(); - await page.getByText('GET "/empty.html"').nth(0).click(); + const getActions = page.getByTestId('actions-tree').getByRole('treeitem', { name: /GET/ }); + await expect(getActions).toHaveCount(4); + await expect(getActions.first()).toContainText('/empty.html'); + await getActions.nth(2).click(); + await getActions.nth(1).click(); + await getActions.nth(0).click(); }); test('should warn user when viewing via file:// protocol', async ({ runInlineTest, page, showReport }, testInfo) => { @@ -934,6 +936,30 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('.tree-item-title', { hasText: 'fill password' })).toBeHidden(); }); + test('should highlight filter matches in step title and subtitle', async ({ runInlineTest, page, showReport }) => { + const result = await runInlineTest({ + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('has steps', async ({ page }) => { + await page.setContent(''); + await page.click('#target'); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + await page.getByRole('link', { name: 'has steps' }).click(); + + const filterInput = page.getByLabel('Filter steps'); + await filterInput.fill('click'); + await expect(page.locator('.step-title-highlight')).toHaveText(['Click']); + + await filterInput.fill('#target'); + await expect(page.locator('.step-subtitle .step-title-highlight')).toHaveText(['#target']); + }); + test('should show step snippets from non-root', async ({ runInlineTest, page, showReport }) => { const result = await runInlineTest({ 'playwright.config.js': ` diff --git a/tests/playwright-test/reporter-perfetto.spec.ts b/tests/playwright-test/reporter-perfetto.spec.ts index cd89c21d2389b..15d230405e996 100644 --- a/tests/playwright-test/reporter-perfetto.spec.ts +++ b/tests/playwright-test/reporter-perfetto.spec.ts @@ -208,7 +208,7 @@ test('should report step params', async ({ runInlineTest }, testInfo) => { expect(result.exitCode).toBe(0); const events = slices(readTrace(testInfo.outputPath()).traceEvents); - expect(findSlice(events, 'Navigate to "about:blank"')!.args.params).toEqual({ url: 'about:blank' }); + expect(findSlice(events, 'Navigate about:blank')!.args.params).toEqual({ url: 'about:blank' }); expect(findSlice(events, 'Set content')!.args.params).toBe(undefined); expect(findSlice(events, `Click getByRole('button')`)!.args.params).toEqual({ locator: `getByRole('button')` }); expect(findSlice(events, `Expect "toBeVisible" getByRole('button')`)!.args.params).toEqual({ locator: `getByRole('button')` }); diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index da821b6f25109..559e00323fb00 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -77,7 +77,8 @@ export default class MyReporter implements Reporter { location = formatLocation(step.location); const skip = step.annotations?.find(a => a.type === 'skip'); const skipped = skip?.description ? ' (skipped: ' + skip.description + ')' : skip ? ' (skipped)' : ''; - console.log(formatPrefix(step.category) + indent + step.title + location + skipped); + const title = step.subtitle ? step.title + ' ' + step.subtitle : step.title; + console.log(formatPrefix(step.category) + indent + title + location + skipped); if (step.error) { const errorLocation = this.printErrorLocation ? formatLocation(step.error.location) : ''; console.log(formatPrefix(step.category) + indent + '↪ error: ' + this.trimError(step.error.message!) + errorLocation); @@ -1001,12 +1002,12 @@ pw:api | Create context fixture | Fixture "page" pw:api | Create page expect |Expect "toPass" @ a.test.ts:11 -pw:api | Navigate to "about:blank" @ a.test.ts:6 +pw:api | Navigate about:blank @ a.test.ts:6 test.step | inner step attempt: 0 @ a.test.ts:7 test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | Expect "toBe" @ a.test.ts:9 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -pw:api | Navigate to "about:blank" @ a.test.ts:6 +pw:api | Navigate about:blank @ a.test.ts:6 test.step | inner step attempt: 1 @ a.test.ts:7 expect | Expect "toBe" @ a.test.ts:9 hook |After Hooks @@ -1053,12 +1054,12 @@ pw:api | Create context fixture | Fixture "page" pw:api | Create page expect |Expect "poll toHaveLength" @ a.test.ts:14 -pw:api | Navigate to "about:blank" @ a.test.ts:7 +pw:api | Navigate about:blank @ a.test.ts:7 test.step | inner step attempt: 0 @ a.test.ts:8 expect | Expect "toBe" @ a.test.ts:10 expect | Expect "toHaveLength" @ a.test.ts:6 expect | ↪ error: Error: expect(received).toHaveLength(expected) -pw:api | Navigate to "about:blank" @ a.test.ts:7 +pw:api | Navigate about:blank @ a.test.ts:7 test.step | inner step attempt: 1 @ a.test.ts:8 expect | Expect "toBe" @ a.test.ts:10 expect | Expect "toHaveLength" @ a.test.ts:6 @@ -1246,12 +1247,12 @@ pw:api | Create page fixture | Fixture "request" pw:api | Create request context pw:api |Wait for navigation @ a.test.ts:5 -pw:api |Navigate to "data:" @ a.test.ts:6 +pw:api |Navigate data: @ a.test.ts:6 pw:api |Click locator('button') @ a.test.ts:8 pw:api |Click getByRole('button') @ a.test.ts:9 -pw:api |GET "/empty.html" @ a.test.ts:10 +pw:api |GET /empty.html @ a.test.ts:10 pw:api |↪ error: -pw:api |GET "/empty.html" @ a.test.ts:11 +pw:api |GET /empty.html @ a.test.ts:11 pw:api |↪ error: hook |After Hooks fixture | Fixture "request" @@ -1463,7 +1464,7 @@ fixture | Fixture "context" pw:api | Create context fixture | Fixture "page" pw:api | Create page -pw:api |Navigate to "/empty.html" @ a.test.ts:4 +pw:api |Navigate /empty.html @ a.test.ts:4 pw:api |Set content @ a.test.ts:5 test.step |custom step @ a.test.ts:6 pw:api | Wait for event "response" @ a.test.ts:7 @@ -1510,7 +1511,7 @@ fixture | Fixture "page" pw:api | Create page pw:api |Wait for event "request" @ a.test.ts:5 pw:api |Wait for event "response" @ a.test.ts:6 -pw:api |Navigate to "/empty.html" @ a.test.ts:7 +pw:api |Navigate /empty.html @ a.test.ts:7 hook |After Hooks fixture | Fixture "page" fixture | Fixture "context" @@ -1553,8 +1554,8 @@ pw:api | Create context fixture | Fixture "page" pw:api | Create page test.step |custom step @ a.test.ts:4 -pw:api | Navigate to "/empty.html" @ a.test.ts:12 -pw:api | GET "/empty.html" @ a.test.ts:6 +pw:api | Navigate /empty.html @ a.test.ts:12 +pw:api | GET /empty.html @ a.test.ts:6 expect | Expect "toBe" @ a.test.ts:8 hook |After Hooks fixture | Fixture "page" @@ -1844,10 +1845,10 @@ pw:api | Create page fixture | Fixture "bar" @ a.test.ts:4 pw:api | Set content @ a.test.ts:14 test.step | inner step @ a.test.ts:15 -pw:api | Navigate to "data:" @ a.test.ts:16 +pw:api | Navigate data: @ a.test.ts:16 pw:api | Set content @ a.test.ts:22 test.step | inner step @ a.test.ts:23 -pw:api | Navigate to "data:" @ a.test.ts:24 +pw:api | Navigate data: @ a.test.ts:24 expect |Expect "toBeVisible" locator('body') @ a.test.ts:32 expect |Expect "toBe" @ a.test.ts:33 expect |Expect "toBe" @ a.test.ts:34 @@ -1869,7 +1870,7 @@ test('should report step params', async ({ runInlineTest }) => { export default class MyReporter implements Reporter { onStepEnd(test: TestCase, result: TestResult, step: TestStep) { if (step.location?.file.endsWith('a.test.ts')) - console.log('%%' + step.category + ' | ' + step.title + ' | ' + JSON.stringify(step.params)); + console.log('%%' + step.category + ' | ' + step.title + ' | ' + step.subtitle + ' | ' + JSON.stringify(step.params)); } } `, @@ -1890,11 +1891,11 @@ test('should report step params', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); expect(result.outputLines).toEqual([ - `pw:api | Navigate to "about:blank" | {"url":"about:blank"}`, - `pw:api | Set content | undefined`, - `pw:api | Click getByRole('button') | {"locator":"getByRole('button')"}`, - `expect | Expect "toBeVisible" getByRole('button') | {"locator":"getByRole('button')"}`, - `test.step | my step | {"foo":"bar","count":7}`, + `pw:api | Navigate | about:blank | {"url":"about:blank"}`, + `pw:api | Set content | undefined | undefined`, + `pw:api | Click | getByRole('button') | {"locator":"getByRole('button')"}`, + `expect | Expect "toBeVisible" | getByRole('button') | {"locator":"getByRole('button')"}`, + `test.step | my step | undefined | {"foo":"bar","count":7}`, ]); }); @@ -1905,7 +1906,7 @@ test('should report input step params', async ({ runInlineTest }) => { export default class MyReporter implements Reporter { onStepEnd(test: TestCase, result: TestResult, step: TestStep) { if (step.location?.file.endsWith('a.test.ts')) - console.log('%%' + step.title + ' | ' + JSON.stringify(step.params)); + console.log('%%' + (step.subtitle ? step.title + ' ' + step.subtitle : step.title) + ' | ' + JSON.stringify(step.params)); } } `, diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 22551fa95d1c8..9e83293d77c4e 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -478,7 +478,7 @@ test('should fail to screenshot an element with infinite animation', async ({ ru }); expect(result.exitCode).toBe(1); expect(result.output).toContain(`Timeout 2000ms exceeded`); - expect(result.output).toContain(`Expect "toHaveScreenshot" with timeout 2000ms`); + expect(result.output).toContain(`Expect "toHaveScreenshot" locator('body') with timeout 2000ms`); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-previous.png'))).toBe(true); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(false); diff --git a/tests/playwright-test/ui-mode-test-progress.spec.ts b/tests/playwright-test/ui-mode-test-progress.spec.ts index 932f3c5cf1303..e144c5e929ca7 100644 --- a/tests/playwright-test/ui-mode-test-progress.spec.ts +++ b/tests/playwright-test/ui-mode-test-progress.spec.ts @@ -55,13 +55,13 @@ test('should update trace live', async ({ runUITest, server }) => { 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "\/one.html"/, + /Navigate.*\/one.html/, ]); await expect( listItem.locator(':scope[aria-selected="true"]'), 'last action to be selected' - ).toHaveText(/Navigate to/); + ).toHaveText(/Navigate/); await expect( listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'), 'spinner' @@ -81,13 +81,13 @@ test('should update trace live', async ({ runUITest, server }) => { ).toHaveText('One'); await expect(listItem).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "\/one.html"/, - /Navigate to "\/two.html"/ + /Navigate.*\/one.html/, + /Navigate.*\/two.html/ ]); await expect( listItem.locator(':scope[aria-selected="true"]'), 'last action to be selected' - ).toHaveText(/Navigate to/); + ).toHaveText(/Navigate/); await expect( listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'), 'spinner' @@ -108,8 +108,8 @@ test('should update trace live', async ({ runUITest, server }) => { await expect(listItem).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "\/one.html"/, - /Navigate to "\/two.html"/, + /Navigate.*\/one.html/, + /Navigate.*\/two.html/, /After Hooks[\d.]+m?s/, ]); }); @@ -140,12 +140,12 @@ test('should preserve action list selection upon live trace update', async ({ ru 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "about:blank"/, + /Navigate.*about:blank/, /Set content/, ]); // Manually select page.goto. - await page.getByTestId('actions-tree').getByText('Navigate to').click(); + await page.getByTestId('actions-tree').getByText('Navigate').click(); // Generate more actions and check that we are still on the page.goto action. latch.open(); @@ -154,14 +154,14 @@ test('should preserve action list selection upon live trace update', async ({ ru 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "about:blank"/, + /Navigate.*about:blank/, /Set content/, /Set content/, ]); await expect( listItem.locator(':scope[aria-selected="true"]'), 'selected action stays the same' - ).toHaveText(/Navigate to/); + ).toHaveText(/Navigate/); }); test('should update tracing network live', async ({ runUITest, server }) => { @@ -201,7 +201,7 @@ test('should update tracing network live', async ({ runUITest, server }) => { 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "\/one.html"/, + /Navigate.*\/one.html/, /Set content/, ]); @@ -241,7 +241,7 @@ test('should show trace w/ multiple contexts', async ({ runUITest, server, creat 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "about:blank"/, + /Navigate.*about:blank/, ]); latch.open(); diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index b337fba3b44ff..14aa61d3e93a2 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -314,7 +314,7 @@ test('should not fail on internal page logs', async ({ runUITest, server }) => { /Before Hooks/, /Create context/, /Create page/, - /Navigate to "\/empty.html"/, + /Navigate.*\/empty.html/, /After Hooks/, ]); }); @@ -492,12 +492,12 @@ test('should filter actions tab on double-click', async ({ runUITest, server }) const actionsTree = page.getByTestId('actions-tree'); await expect(actionsTree.getByRole('treeitem')).toHaveText([ /Before Hooks/, - /Navigate to "\/empty.html"/, + /Navigate.*\/empty.html/, /After Hooks/, ]); - await actionsTree.getByRole('treeitem', { name: 'Navigate to "\/empty.html"' }).dblclick(); + await actionsTree.getByRole('treeitem', { name: 'Navigate' }).dblclick(); await expect(actionsTree.getByRole('treeitem')).toHaveText([ - /Navigate to "\/empty.html"/, + /Navigate.*\/empty.html/, ]); }); diff --git a/utils/generate_channels.js b/utils/generate_channels.js index b0b44bb84647c..ded01189edca6 100755 --- a/utils/generate_channels.js +++ b/utils/generate_channels.js @@ -327,15 +327,33 @@ function generateChannels(target) { throw new Error(`Method "${className}.${methodName}" must should not specify "group" because it is "internal" in protocol.yml`); if (method.group && !['getter', 'configuration', 'route', 'default'].includes(method.group)) throw new Error(`Unknown group "${method.group}" for method "${className}.${methodName}" in protocol.yml`); + if (method.subtitle && method.internal) + throw new Error(`Method "${className}.${methodName}" should not specify "subtitle" because it is "internal" in protocol.yml`); + if (!method.subtitle && !method.internal && (method.parameters || {}).selector) + throw new Error(`Method "${className}.${methodName}" has a "selector" parameter, so it must have an explicit "subtitle" in protocol.yml`); + if (method.renderParams) { + if (method.internal) + throw new Error(`Method "${className}.${methodName}" should not specify "renderParams" because it is "internal" in protocol.yml`); + if (!Array.isArray(method.renderParams) || method.renderParams.some(entry => typeof entry !== 'string')) + throw new Error(`Method "${className}.${methodName}" must specify "renderParams" as a list of strings in protocol.yml`); + for (const entry of method.renderParams) { + // Each entry is "[key=]path[:selector]", path root must be a parameter. + const paramName = entry.split(':')[0].split('=').pop().split('.')[0]; + if (!(method.parameters || {})[paramName]) + throw new Error(`Method "${className}.${methodName}" renderParams entry "${entry}" does not match any parameter in protocol.yml`); + } + } const internalProp = method.internal ? ` internal: ${method.internal},` : ''; const titleProp = method.title ? ` title: '${method.title}',` : ''; + const subtitleProp = method.subtitle ? ` subtitle: '${method.subtitle}',` : ''; + const renderParamsProp = method.renderParams ? ` renderParams: [${method.renderParams.map(entry => `'${entry}'`).join(', ')}],` : ''; const groupProp = method.group ? ` group: '${method.group}',` : ''; const slowMoProp = method.flags?.slowMo ? ` slowMo: ${method.flags.slowMo},` : ''; const snapshotProp = method.flags?.snapshot ? ` snapshot: ${method.flags.snapshot},` : ''; const pauseProp = method.flags?.pause ? ` pause: ${method.flags.pause},` : ''; const inputProp = method.flags?.input ? ` input: ${method.flags.input},` : ''; const isAutoWaitingProp = method.flags?.isAutoWaiting ? ` isAutoWaiting: ${method.flags.isAutoWaiting},` : ''; - methodMetainfo.push(`['${className + '.' + methodName}', {${internalProp}${titleProp}${slowMoProp}${snapshotProp}${pauseProp}${inputProp}${isAutoWaitingProp}${groupProp} }]`); + methodMetainfo.push(`['${className + '.' + methodName}', {${internalProp}${titleProp}${subtitleProp}${renderParamsProp}${slowMoProp}${snapshotProp}${pauseProp}${inputProp}${isAutoWaitingProp}${groupProp} }]`); } const parameters = objectType(method.parameters || {}, ''); @@ -432,7 +450,7 @@ const structs_ts = generateStructs(); const client_channels_ts = generateChannels('Channel'); -metainfo_ts.push(`export type MethodMetainfo = { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string }; +metainfo_ts.push(`export type MethodMetainfo = { internal?: boolean, title?: string, subtitle?: string, renderParams?: string[], slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string }; export const methodMetainfo = new Map([ ${methodMetainfo.join(`,\n `)} diff --git a/utils/generate_types/overrides-testReporter.d.ts b/utils/generate_types/overrides-testReporter.d.ts index 6ca13fdcf2157..78dd048a9e554 100644 --- a/utils/generate_types/overrides-testReporter.d.ts +++ b/utils/generate_types/overrides-testReporter.d.ts @@ -132,6 +132,7 @@ export interface JSONReportTestResult { export interface JSONReportTestStep { title: string; + subtitle?: string; duration: number; error: TestError | undefined; steps?: JSONReportTestStep[];