From 1311767f87c9601af2c2002c30471d5cdab7fb92 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 3 Oct 2022 16:14:02 -0800 Subject: [PATCH] chore: generate api calls (#17794) --- .../src/server/recorder/DEPS.list | 1 + .../src/server/recorder/csharp.ts | 77 ++++-- .../src/server/recorder/java.ts | 87 +++++-- .../src/server/recorder/javascript.ts | 67 ++++-- .../src/server/recorder/language.ts | 84 +++++++ .../src/server/recorder/python.ts | 73 ++++-- .../src/utils/isomorphic/stringUtils.ts | 4 + tests/library/inspector/cli-codegen-1.spec.ts | 68 +++--- tests/library/inspector/cli-codegen-2.spec.ts | 22 +- tests/library/inspector/cli-codegen-3.spec.ts | 227 +++++++++++++----- 10 files changed, 535 insertions(+), 175 deletions(-) diff --git a/packages/playwright-core/src/server/recorder/DEPS.list b/packages/playwright-core/src/server/recorder/DEPS.list index bb668d0e80..408667f8f6 100644 --- a/packages/playwright-core/src/server/recorder/DEPS.list +++ b/packages/playwright-core/src/server/recorder/DEPS.list @@ -1,5 +1,6 @@ [*] ../ +../isomorphic/** ../registry/** ../../common/ ../../protocol/ diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/recorder/csharp.ts index 1b6d52721a..8966c9fc97 100644 --- a/packages/playwright-core/src/server/recorder/csharp.ts +++ b/packages/playwright-core/src/server/recorder/csharp.ts @@ -15,13 +15,14 @@ */ import type { BrowserContextOptions } from '../../..'; -import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { asLocator } from './language'; +import type { LanguageGenerator, LanguageGeneratorOptions, LocatorBase, LocatorType } from './language'; import { sanitizeDeviceOptions, toSignalMap } from './language'; import type { ActionInContext } from './codeGenerator'; import type { Action } from './recorderActions'; import type { MouseClickOptions } from './utils'; import { toModifiers } from './utils'; -import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils'; +import { escapeWithQuotes, toTitleCase } from '../../utils/isomorphic/stringUtils'; import deviceDescriptors from '../deviceDescriptors'; type CSharpLanguageMode = 'library' | 'mstest' | 'nunit'; @@ -76,7 +77,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { if (actionInContext.frame.isMainFrame) { subject = pageAlias; } else if (actionInContext.frame.selectorsChain && action.name !== 'navigate') { - const locators = actionInContext.frame.selectorsChain.map(selector => '.' + asLocator(selector, 'FrameLocator')); + const locators = actionInContext.frame.selectorsChain.map(selector => `.FrameLocator(${quote(selector)})`); subject = `${pageAlias}${locators.join('')}`; } else if (actionInContext.frame.name) { subject = `${pageAlias}.Frame(${quote(actionInContext.frame.name)})`; @@ -139,30 +140,34 @@ export class CSharpLanguageGenerator implements LanguageGenerator { if (action.position) options.position = action.position; if (!Object.entries(options).length) - return asLocator(action.selector) + `.${method}Async()`; + return this._asLocator(action.selector) + `.${method}Async()`; const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options'); - return asLocator(action.selector) + `.${method}Async(${optionsString})`; + return this._asLocator(action.selector) + `.${method}Async(${optionsString})`; } case 'check': - return asLocator(action.selector) + `.CheckAsync()`; + return this._asLocator(action.selector) + `.CheckAsync()`; case 'uncheck': - return asLocator(action.selector) + `.UncheckAsync()`; + return this._asLocator(action.selector) + `.UncheckAsync()`; case 'fill': - return asLocator(action.selector) + `.FillAsync(${quote(action.text)})`; + return this._asLocator(action.selector) + `.FillAsync(${quote(action.text)})`; case 'setInputFiles': - return asLocator(action.selector) + `.SetInputFilesAsync(${formatObject(action.files)})`; + return this._asLocator(action.selector) + `.SetInputFilesAsync(${formatObject(action.files)})`; case 'press': { const modifiers = toModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); - return asLocator(action.selector) + `.PressAsync(${quote(shortcut)})`; + return this._asLocator(action.selector) + `.PressAsync(${quote(shortcut)})`; } case 'navigate': return `GotoAsync(${quote(action.url)})`; case 'select': - return asLocator(action.selector) + `.SelectOptionAsync(${formatObject(action.options)})`; + return this._asLocator(action.selector) + `.SelectOptionAsync(${formatObject(action.options)})`; } } + private _asLocator(selector: string) { + return asLocator(this, selector); + } + generateHeader(options: LanguageGeneratorOptions): string { if (this._mode === 'library') return this.generateStandaloneHeader(options); @@ -216,6 +221,47 @@ export class CSharpLanguageGenerator implements LanguageGenerator { return `${storageStateLine} } }\n`; } + + generateLocator(base: LocatorBase, kind: LocatorType, body: string, options: { attrs?: Record, hasText?: string, exact?: boolean } = {}): string { + switch (kind) { + case 'default': + return `Locator(${quote(body)})`; + case 'nth': + return `Nth(${body})`; + case 'first': + return `First`; + case 'last': + return `Last`; + case 'role': + const attrs: string[] = []; + for (const [name, value] of Object.entries(options.attrs!)) + attrs.push(`${toTitleCase(name)} = ${typeof value === 'string' ? quote(value) : value}`); + const attrString = attrs.length ? `, new () { ${attrs.join(', ')} }` : ''; + return `GetByRole(${quote(body)}${attrString})`; + case 'has-text': + return `Locator(${quote(body)}, new () { HasTextString: ${quote(options.hasText!)} })`; + case 'test-id': + return `GetByTestId(${quote(body)})`; + case 'text': + return toCallWithExact('GetByText', body, !!options.exact); + case 'alt': + return toCallWithExact('GetByAltText', body, !!options.exact); + case 'placeholder': + return toCallWithExact('GetByPlaceholderText', body, !!options.exact); + case 'label': + return toCallWithExact('GetByLabelText', body, !!options.exact); + case 'title': + return toCallWithExact('GetByTitle', body, !!options.exact); + default: + throw new Error('Unknown selector kind ' + kind); + } + } +} + +function toCallWithExact(method: string, body: string, exact: boolean) { + if (exact) + return `${method}(${quote(body)}, new () { Exact: true })`; + return `${method}(${quote(body)})`; } function formatObject(value: any, indent = ' ', name = ''): string { @@ -347,12 +393,3 @@ class CSharpFormatter { function quote(text: string) { return escapeWithQuotes(text, '\"'); } - -function asLocator(selector: string, locatorFn = 'Locator') { - const match = selector.match(/(.*)\s+>>\s+nth=(\d+)$/); - if (!match) - return `${locatorFn}(${quote(selector)})`; - if (+match[2] === 0) - return `${locatorFn}(${quote(match[1])}).First`; - return `${locatorFn}(${quote(match[1])}).Nth(${match[2]})`; -} diff --git a/packages/playwright-core/src/server/recorder/java.ts b/packages/playwright-core/src/server/recorder/java.ts index 5eb65534df..2395fee007 100644 --- a/packages/playwright-core/src/server/recorder/java.ts +++ b/packages/playwright-core/src/server/recorder/java.ts @@ -15,7 +15,8 @@ */ import type { BrowserContextOptions } from '../../..'; -import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { asLocator } from './language'; +import type { LanguageGenerator, LanguageGeneratorOptions, LocatorBase, LocatorType } from './language'; import { toSignalMap } from './language'; import type { ActionInContext } from './codeGenerator'; import type { Action } from './recorderActions'; @@ -23,7 +24,7 @@ import type { MouseClickOptions } from './utils'; import { toModifiers } from './utils'; import deviceDescriptors from '../deviceDescriptors'; import { JavaScriptFormatter } from './javascript'; -import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils'; +import { escapeWithQuotes, toTitleCase } from '../../utils/isomorphic/stringUtils'; export class JavaLanguageGenerator implements LanguageGenerator { id = 'java'; @@ -45,11 +46,13 @@ export class JavaLanguageGenerator implements LanguageGenerator { } let subject: string; + let inFrameLocator = false; if (actionInContext.frame.isMainFrame) { subject = pageAlias; } else if (actionInContext.frame.selectorsChain && action.name !== 'navigate') { - const locators = actionInContext.frame.selectorsChain.map(selector => '.' + asLocator(selector, 'frameLocator')); + const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`); subject = `${pageAlias}${locators.join('')}`; + inFrameLocator = true; } else if (actionInContext.frame.name) { subject = `${pageAlias}.frame(${quote(actionInContext.frame.name)})`; } else { @@ -65,7 +68,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { });`); } - const actionCall = this._generateActionCall(action); + const actionCall = this._generateActionCall(action, inFrameLocator); let code = `${subject}.${actionCall};`; if (signals.popup) { @@ -87,7 +90,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(action: Action): string { + private _generateActionCall(action: Action, inFrameLocator: boolean): string { switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -108,28 +111,32 @@ export class JavaLanguageGenerator implements LanguageGenerator { if (action.position) options.position = action.position; const optionsText = formatClickOptions(options); - return asLocator(action.selector) + `.${method}(${optionsText})`; + return this._asLocator(action.selector, inFrameLocator) + `.${method}(${optionsText})`; } case 'check': - return asLocator(action.selector) + `.check()`; + return this._asLocator(action.selector, inFrameLocator) + `.check()`; case 'uncheck': - return asLocator(action.selector) + `.uncheck()`; + return this._asLocator(action.selector, inFrameLocator) + `.uncheck()`; case 'fill': - return asLocator(action.selector) + `.fill(${quote(action.text)})`; + return this._asLocator(action.selector, inFrameLocator) + `.fill(${quote(action.text)})`; case 'setInputFiles': - return asLocator(action.selector) + `.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)})`; + return this._asLocator(action.selector, inFrameLocator) + `.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)})`; case 'press': { const modifiers = toModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); - return asLocator(action.selector) + `.press(${quote(shortcut)})`; + return this._asLocator(action.selector, inFrameLocator) + `.press(${quote(shortcut)})`; } case 'navigate': return `navigate(${quote(action.url)})`; case 'select': - return asLocator(action.selector) + `.selectOption(${formatSelectOption(action.options.length > 1 ? action.options : action.options[0])})`; + return this._asLocator(action.selector, inFrameLocator) + `.selectOption(${formatSelectOption(action.options.length > 1 ? action.options : action.options[0])})`; } } + private _asLocator(selector: string, inFrameLocator: boolean) { + return asLocator(this, selector, inFrameLocator); + } + generateHeader(options: LanguageGeneratorOptions): string { const formatter = new JavaScriptFormatter(); formatter.add(` @@ -152,6 +159,53 @@ export class JavaLanguageGenerator implements LanguageGenerator { } }`; } + + generateLocator(base: LocatorBase, kind: LocatorType, body: string, options: { attrs?: Record, hasText?: string, exact?: boolean } = {}): string { + let clazz: string; + switch (base) { + case 'page': clazz = 'Page'; break; + case 'frame-locator': clazz = 'FrameLocator'; break; + case 'locator': clazz = 'Locator'; break; + } + switch (kind) { + case 'default': + return `locator(${quote(body)})`; + case 'nth': + return `nth(${body})`; + case 'first': + return `first()`; + case 'last': + return `last()`; + case 'role': + const attrs: string[] = []; + for (const [name, value] of Object.entries(options.attrs!)) + attrs.push(`.set${toTitleCase(name)}(${typeof value === 'string' ? quote(value) : value})`); + const attrString = attrs.length ? `, new ${clazz}.GetByRoleOptions()${attrs.join('')}` : ''; + return `getByRole(${quote(body)}${attrString})`; + case 'has-text': + return `locator(${quote(body)}, new ${clazz}.LocatorOptions().setHasText(${quote(options.hasText!)}))`; + case 'test-id': + return `getByTestId(${quote(body)})`; + case 'text': + return toCallWithExact(clazz, 'getByText', body, !!options.exact); + case 'alt': + return toCallWithExact(clazz, 'getByAltText', body, !!options.exact); + case 'placeholder': + return toCallWithExact(clazz, 'getByPlaceholderText', body, !!options.exact); + case 'label': + return toCallWithExact(clazz, 'getByLabelText', body, !!options.exact); + case 'title': + return toCallWithExact(clazz, 'getByTitle', body, !!options.exact); + default: + throw new Error('Unknown selector kind ' + kind); + } + } +} + +function toCallWithExact(clazz: string, method: string, body: string, exact: boolean) { + if (exact) + return `${method}(${quote(body)}, new ${clazz}.${toTitleCase(method)}Options().setExact(exact))`; + return `${method}(${quote(body)})`; } function formatPath(files: string | string[]): string { @@ -251,12 +305,3 @@ function formatClickOptions(options: MouseClickOptions) { function quote(text: string) { return escapeWithQuotes(text, '\"'); } - -function asLocator(selector: string, locatorFn = 'locator') { - const match = selector.match(/(.*)\s+>>\s+nth=(\d+)$/); - if (!match) - return `${locatorFn}(${quote(selector)})`; - if (+match[2] === 0) - return `${locatorFn}(${quote(match[1])}).first()`; - return `${locatorFn}(${quote(match[1])}).nth(${match[2]})`; -} diff --git a/packages/playwright-core/src/server/recorder/javascript.ts b/packages/playwright-core/src/server/recorder/javascript.ts index 895c851a0c..6e46936927 100644 --- a/packages/playwright-core/src/server/recorder/javascript.ts +++ b/packages/playwright-core/src/server/recorder/javascript.ts @@ -15,7 +15,8 @@ */ import type { BrowserContextOptions } from '../../..'; -import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { asLocator } from './language'; +import type { LanguageGenerator, LanguageGeneratorOptions, LocatorBase, LocatorType } from './language'; import { sanitizeDeviceOptions, toSignalMap } from './language'; import type { ActionInContext } from './codeGenerator'; import type { Action } from './recorderActions'; @@ -57,7 +58,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { if (actionInContext.frame.isMainFrame) { subject = pageAlias; } else if (actionInContext.frame.selectorsChain && action.name !== 'navigate') { - const locators = actionInContext.frame.selectorsChain.map(selector => '.' + asLocator(selector, 'frameLocator')); + const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`); subject = `${pageAlias}${locators.join('')}`; } else if (actionInContext.frame.name) { subject = `${pageAlias}.frame(${formatObject({ name: actionInContext.frame.name })})`; @@ -131,28 +132,32 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { if (action.position) options.position = action.position; const optionsString = formatOptions(options, false); - return asLocator(action.selector) + `.${method}(${optionsString})`; + return this._asLocator(action.selector) + `.${method}(${optionsString})`; } case 'check': - return asLocator(action.selector) + `.check()`; + return this._asLocator(action.selector) + `.check()`; case 'uncheck': - return asLocator(action.selector) + `.uncheck()`; + return this._asLocator(action.selector) + `.uncheck()`; case 'fill': - return asLocator(action.selector) + `.fill(${quote(action.text)})`; + return this._asLocator(action.selector) + `.fill(${quote(action.text)})`; case 'setInputFiles': - return asLocator(action.selector) + `.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)})`; + return this._asLocator(action.selector) + `.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)})`; case 'press': { const modifiers = toModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); - return asLocator(action.selector) + `.press(${quote(shortcut)})`; + return this._asLocator(action.selector) + `.press(${quote(shortcut)})`; } case 'navigate': return `goto(${quote(action.url)})`; case 'select': - return asLocator(action.selector) + `.selectOption(${formatObject(action.options.length > 1 ? action.options : action.options[0])})`; + return this._asLocator(action.selector) + `.selectOption(${formatObject(action.options.length > 1 ? action.options : action.options[0])})`; } } + private _asLocator(selector: string) { + return asLocator(this, selector); + } + generateHeader(options: LanguageGeneratorOptions): string { if (this._isTest) return this.generateTestHeader(options); @@ -197,15 +202,45 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''} await browser.close(); })();`; } + + generateLocator(base: LocatorBase, kind: LocatorType, body: string, options: { attrs?: Record, hasText?: string, exact?: boolean } = {}): string { + switch (kind) { + case 'default': + return `locator(${quote(body)})`; + case 'nth': + return `nth(${body})`; + case 'first': + return `first()`; + case 'last': + return `last()`; + case 'role': + const attrs: string[] = []; + for (const [name, value] of Object.entries(options.attrs!)) + attrs.push(`${name}: ${typeof value === 'string' ? quote(value) : value}`); + const attrString = attrs.length ? `, { ${attrs.join(', ')} }` : ''; + return `getByRole(${quote(body)}${attrString})`; + case 'has-text': + return `locator(${quote(body)}, { hasText: ${quote(options.hasText!)} })`; + case 'test-id': + return `getByTestId(${quote(body)})`; + case 'text': + return toCallWithExact('getByText', body, !!options.exact); + case 'alt': + return toCallWithExact('getByAltText', body, !!options.exact); + case 'placeholder': + return toCallWithExact('getByPlaceholderText', body, !!options.exact); + case 'label': + return toCallWithExact('getByLabelText', body, !!options.exact); + case 'title': + return toCallWithExact('getByTitle', body, !!options.exact); + default: + throw new Error('Unknown selector kind ' + kind); + } + } } -function asLocator(selector: string, locatorFn = 'locator') { - const match = selector.match(/(.*)\s+>>\s+nth=(\d+)$/); - if (!match) - return `${locatorFn}(${quote(selector)})`; - if (+match[2] === 0) - return `${locatorFn}(${quote(match[1])}).first()`; - return `${locatorFn}(${quote(match[1])}).nth(${match[2]})`; +function toCallWithExact(method: string, body: string, exact: boolean) { + return exact ? `${method}(${quote(body)}, { exact: true })` : `${method}(${quote(body)})`; } function formatOptions(value: any, hasArguments: boolean): string { diff --git a/packages/playwright-core/src/server/recorder/language.ts b/packages/playwright-core/src/server/recorder/language.ts index b646c76807..fc1e8751b5 100644 --- a/packages/playwright-core/src/server/recorder/language.ts +++ b/packages/playwright-core/src/server/recorder/language.ts @@ -15,6 +15,9 @@ */ import type { BrowserContextOptions, LaunchOptions } from '../../..'; +import type { CSSComplexSelectorList } from '../isomorphic/cssParser'; +import { parseAttributeSelector, parseSelector, stringifySelector } from '../isomorphic/selectorParser'; +import type { ParsedSelector } from '../isomorphic/selectorParser'; import type { ActionInContext } from './codeGenerator'; import type { Action, DialogSignal, DownloadSignal, NavigationSignal, PopupSignal } from './recorderActions'; @@ -26,6 +29,9 @@ export type LanguageGeneratorOptions = { saveStorage?: string; }; +export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text'; +export type LocatorBase = 'page' | 'locator' | 'frame-locator'; + export interface LanguageGenerator { id: string; groupName: string; @@ -34,6 +40,7 @@ export interface LanguageGenerator { generateHeader(options: LanguageGeneratorOptions): string; generateAction(actionInContext: ActionInContext): string; generateFooter(saveStorage: string | undefined): string; + generateLocator(base: LocatorBase, kind: LocatorType, body: string, options?: { attrs?: Record, hasText?: string, exact?: boolean }): string; } export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions { @@ -68,3 +75,80 @@ export function toSignalMap(action: Action) { dialog, }; } + +function detectExact(text: string): { exact: boolean, text: string } { + let exact = false; + if (text.startsWith('"') && text.endsWith('"')) { + text = JSON.parse(text); + exact = true; + } + return { exact, text }; +} + +export function asLocator(generator: LanguageGenerator, selector: string, isFrameLocator: boolean = false): string { + const parsed = parseSelector(selector); + const tokens: string[] = []; + for (const part of parsed.parts) { + const base = part === parsed.parts[0] ? (isFrameLocator ? 'frame-locator' : 'page') : 'locator'; + if (part.name === 'nth') { + if (part.body === '0') + tokens.push(generator.generateLocator(base, 'first', '')); + else if (part.body === '-1') + tokens.push(generator.generateLocator(base, 'last', '')); + else + tokens.push(generator.generateLocator(base, 'nth', part.body as string)); + continue; + } + if (part.name === 'text') { + const { exact, text } = detectExact(part.body as string); + tokens.push(generator.generateLocator(base, 'text', text, { exact })); + continue; + } + if (part.name === 'role') { + const attrSelector = parseAttributeSelector(part.body as string, true); + const attrs: Record = {}; + for (const attr of attrSelector.attributes!) + attrs[attr.name === 'include-hidden' ? 'includeHidden' : attr.name] = attr.value; + tokens.push(generator.generateLocator(base, 'role', attrSelector.name, { attrs })); + continue; + } + if (part.name === 'css') { + const parsed = part.body as CSSComplexSelectorList; + if (parsed[0].simples.length === 1 && parsed[0].simples[0].selector.functions.length === 1 && parsed[0].simples[0].selector.functions[0].name === 'hasText') { + const hasText = parsed[0].simples[0].selector.functions[0].args[0] as string; + tokens.push(generator.generateLocator(base, 'has-text', parsed[0].simples[0].selector.css!, { hasText })); + continue; + } + } + + if (part.name === 'attr') { + const attrSelector = parseAttributeSelector(part.body as string, true); + const { name, value } = attrSelector.attributes[0]; + if (name === 'data-testid') { + tokens.push(generator.generateLocator(base, 'test-id', value)); + continue; + } + + const { exact, text } = detectExact(value); + if (name === 'placeholder') { + tokens.push(generator.generateLocator(base, 'placeholder', text, { exact })); + continue; + } + if (name === 'alt') { + tokens.push(generator.generateLocator(base, 'alt', text, { exact })); + continue; + } + if (name === 'title') { + tokens.push(generator.generateLocator(base, 'title', text, { exact })); + continue; + } + if (name === 'label') { + tokens.push(generator.generateLocator(base, 'label', text, { exact })); + continue; + } + } + const p: ParsedSelector = { parts: [part] }; + tokens.push(generator.generateLocator(base, 'default', stringifySelector(p))); + } + return tokens.join('.'); +} diff --git a/packages/playwright-core/src/server/recorder/python.ts b/packages/playwright-core/src/server/recorder/python.ts index 0055e6d680..3f49bc61ec 100644 --- a/packages/playwright-core/src/server/recorder/python.ts +++ b/packages/playwright-core/src/server/recorder/python.ts @@ -15,7 +15,8 @@ */ import type { BrowserContextOptions } from '../../..'; -import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { asLocator } from './language'; +import type { LanguageGenerator, LanguageGeneratorOptions, LocatorBase, LocatorType } from './language'; import { sanitizeDeviceOptions, toSignalMap } from './language'; import type { ActionInContext } from './codeGenerator'; import type { Action } from './recorderActions'; @@ -64,7 +65,7 @@ export class PythonLanguageGenerator implements LanguageGenerator { if (actionInContext.frame.isMainFrame) { subject = pageAlias; } else if (actionInContext.frame.selectorsChain && action.name !== 'navigate') { - const locators = actionInContext.frame.selectorsChain.map(selector => '.' + asLocator(selector, 'frame_locator')); + const locators = actionInContext.frame.selectorsChain.map(selector => `.frame_locator(${quote(selector)})`); subject = `${pageAlias}${locators.join('')}`; } else if (actionInContext.frame.name) { subject = `${pageAlias}.frame(${formatOptions({ name: actionInContext.frame.name }, false)})`; @@ -126,28 +127,32 @@ export class PythonLanguageGenerator implements LanguageGenerator { if (action.position) options.position = action.position; const optionsString = formatOptions(options, false); - return asLocator(action.selector) + `.${method}(${optionsString})`; + return this._asLocator(action.selector) + `.${method}(${optionsString})`; } case 'check': - return asLocator(action.selector) + `.check()`; + return this._asLocator(action.selector) + `.check()`; case 'uncheck': - return asLocator(action.selector) + `.uncheck()`; + return this._asLocator(action.selector) + `.uncheck()`; case 'fill': - return asLocator(action.selector) + `.fill(${quote(action.text)})`; + return this._asLocator(action.selector) + `.fill(${quote(action.text)})`; case 'setInputFiles': - return asLocator(action.selector) + `.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`; + return this._asLocator(action.selector) + `.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`; case 'press': { const modifiers = toModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); - return asLocator(action.selector) + `.press(${quote(shortcut)})`; + return this._asLocator(action.selector) + `.press(${quote(shortcut)})`; } case 'navigate': return `goto(${quote(action.url)})`; case 'select': - return asLocator(action.selector) + `.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`; + return this._asLocator(action.selector) + `.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`; } } + private _asLocator(selector: string) { + return asLocator(this, selector); + } + generateHeader(options: LanguageGeneratorOptions): string { const formatter = new PythonFormatter(); if (this._isPyTest) { @@ -215,6 +220,47 @@ with sync_playwright() as playwright: `; } } + + generateLocator(base: LocatorBase, kind: LocatorType, body: string, options: { attrs?: Record, hasText?: string, exact?: boolean } = {}): string { + switch (kind) { + case 'default': + return `locator(${quote(body)})`; + case 'nth': + return `nth(${body})`; + case 'first': + return `first`; + case 'last': + return `last`; + case 'role': + const attrs: string[] = []; + for (const [name, value] of Object.entries(options.attrs!)) + attrs.push(`${toSnakeCase(name)}=${typeof value === 'string' ? quote(value) : value}`); + const attrString = attrs.length ? `, ${attrs.join(', ')}` : ''; + return `get_by_role(${quote(body)}${attrString})`; + case 'has-text': + return `locator(${quote(body)}, has_text=${quote(options.hasText!)})`; + case 'test-id': + return `get_by_test_id(${quote(body)})`; + case 'text': + return toCallWithExact('get_by_text', body, !!options.exact); + case 'alt': + return toCallWithExact('get_by_alt_text', body, !!options.exact); + case 'placeholder': + return toCallWithExact('get_by_placeholder_text', body, !!options.exact); + case 'label': + return toCallWithExact('get_by_label_text', body, !!options.exact); + case 'title': + return toCallWithExact('get_by_title', body, !!options.exact); + default: + throw new Error('Unknown selector kind ' + kind); + } + } +} + +function toCallWithExact(method: string, body: string, exact: boolean) { + if (exact) + return `${method}(${quote(body)}, exact=true)`; + return `${method}(${quote(body)})`; } function formatValue(value: any): string { @@ -316,12 +362,3 @@ class PythonFormatter { function quote(text: string) { return escapeWithQuotes(text, '\"'); } - -function asLocator(selector: string, locatorFn = 'locator') { - const match = selector.match(/(.*)\s+>>\s+nth=(\d+)$/); - if (!match) - return `${locatorFn}(${quote(selector)})`; - if (+match[2] === 0) - return `${locatorFn}(${quote(match[1])}).first`; - return `${locatorFn}(${quote(match[1])}).nth(${match[2]})`; -} diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts index 5c1272de47..e82e1890ed 100644 --- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts @@ -25,3 +25,7 @@ export function escapeWithQuotes(text: string, char: string = '\'') { return char + escapedText.replace(/[`]/g, '`') + char; throw new Error('Invalid escape char'); } + +export function toTitleCase(name: string) { + return name.charAt(0).toUpperCase() + name.substring(1); +} diff --git a/tests/library/inspector/cli-codegen-1.spec.ts b/tests/library/inspector/cli-codegen-1.spec.ts index 9f9680fc4c..d121e80624 100644 --- a/tests/library/inspector/cli-codegen-1.spec.ts +++ b/tests/library/inspector/cli-codegen-1.spec.ts @@ -35,19 +35,19 @@ test.describe('cli codegen', () => { ]); expect(sources.get('JavaScript').text).toContain(` - await page.locator('role=button[name=\"Submit\"]').click();`); + await page.getByRole('button', { name: 'Submit' }).click();`); expect(sources.get('Python').text).toContain(` - page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`); + page.get_by_role("button", name="Submit").click()`); expect(sources.get('Python Async').text).toContain(` - await page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`); + await page.get_by_role("button", name="Submit").click()`); expect(sources.get('Java').text).toContain(` - page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`); + page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).click()`); expect(sources.get('C#').text).toContain(` - await page.Locator(\"role=button[name=\\\"Submit\\\"]\").ClickAsync();`); + await page.GetByRole("button", new () { Name = "Submit" }).ClickAsync();`); expect(message.text()).toBe('click'); }); @@ -78,7 +78,7 @@ test.describe('cli codegen', () => { ]); expect(sources.get('JavaScript').text).toContain(` - await page.locator('role=button[name=\"Submit\"]').click();`); + await page.getByRole('button', { name: 'Submit' }).click();`); expect(message.text()).toBe('click'); }); @@ -158,19 +158,19 @@ test.describe('cli codegen', () => { ]); expect(sources.get('JavaScript').text).toContain(` - await page.locator('role=button[name=\"Submit\"]').click();`); + await page.getByRole('button', { name: 'Submit' }).click();`); expect(sources.get('Python').text).toContain(` - page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`); + page.get_by_role("button", name="Submit").click()`); expect(sources.get('Python Async').text).toContain(` - await page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`); + await page.get_by_role("button", name="Submit").click()`); expect(sources.get('Java').text).toContain(` - page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`); + page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).click()`); expect(sources.get('C#').text).toContain(` - await page.Locator(\"role=button[name=\\\"Submit\\\"]\").ClickAsync();`); + await page.GetByRole("button", new () { Name = "Submit" }).ClickAsync();`); expect(message.text()).toBe('click'); }); @@ -204,7 +204,7 @@ test.describe('cli codegen', () => { page.dispatchEvent('div', 'click', { detail: 1 }) ]); expect(sources.get('JavaScript').text).toContain(` - await page.locator('text=Some long text here').click();`); + await page.getByText('Some long text here').click();`); expect(message.text()).toBe('click'); }); @@ -551,28 +551,28 @@ test.describe('cli codegen', () => { expect(sources.get('JavaScript').text).toContain(` const [page1] = await Promise.all([ page.waitForEvent('popup'), - page.locator('role=link[name=\"link\"]').click() + page.getByRole('link', { name: 'link' }).click() ]);`); expect(sources.get('Java').text).toContain(` Page page1 = page.waitForPopup(() -> { - page.locator("role=link[name=\\\"link\\\"]").click(); + page.getByRole("link", new Page.GetByRoleOptions().setName("link")).click(); });`); expect(sources.get('Python').text).toContain(` with page.expect_popup() as popup_info: - page.locator(\"role=link[name=\\\"link\\\"]\").click() + page.get_by_role("link", name="link").click() page1 = popup_info.value`); expect(sources.get('Python Async').text).toContain(` async with page.expect_popup() as popup_info: - await page.locator(\"role=link[name=\\\"link\\\"]\").click() + await page.get_by_role("link", name="link").click() page1 = await popup_info.value`); expect(sources.get('C#').text).toContain(` var page1 = await page.RunAndWaitForPopupAsync(async () => { - await page.Locator(\"role=link[name=\\\"link\\\"]\").ClickAsync(); + await page.GetByRole("link", new () { Name = "link" }).ClickAsync(); });`); expect(popup.url()).toBe('about:blank'); @@ -592,31 +592,31 @@ test.describe('cli codegen', () => { ]); expect.soft(sources.get('JavaScript').text).toContain(` - await page.locator('text=link').click(); + await page.getByText('link').click(); await page.waitForURL('about:blank#foo');`); expect.soft(sources.get('Playwright Test').text).toContain(` - await page.locator('text=link').click(); + await page.getByText('link').click(); await expect(page).toHaveURL('about:blank#foo');`); expect.soft(sources.get('Java').text).toContain(` - page.locator("text=link").click(); + page.getByText("link").click(); assertThat(page).hasURL("about:blank#foo");`); expect.soft(sources.get('Python').text).toContain(` - page.locator("text=link").click() + page.get_by_text("link").click() page.wait_for_url("about:blank#foo")`); expect.soft(sources.get('Python Async').text).toContain(` - await page.locator("text=link").click() + await page.get_by_text("link").click() await page.wait_for_url("about:blank#foo")`); expect.soft(sources.get('Pytest').text).toContain(` - page.locator("text=link").click() + page.get_by_text("link").click() expect(page).to_have_url("about:blank#foo")`); expect.soft(sources.get('C#').text).toContain(` - await page.Locator("text=link").ClickAsync(); + await page.GetByText("link").ClickAsync(); await page.WaitForURLAsync("about:blank#foo");`); expect(page.url()).toContain('about:blank#foo'); @@ -638,23 +638,23 @@ test.describe('cli codegen', () => { ]); expect.soft(sources.get('JavaScript').text).toContain(` - await page.locator('text=link').click(); + await page.getByText('link').click(); await page.waitForURL('about:blank#foo');`); expect.soft(sources.get('Java').text).toContain(` - page.locator("text=link").click(); + page.getByText("link").click(); assertThat(page).hasURL("about:blank#foo");`); expect.soft(sources.get('Python').text).toContain(` - page.locator(\"text=link\").click() + page.get_by_text("link").click() page.wait_for_url("about:blank#foo")`); expect.soft(sources.get('Python Async').text).toContain(` - await page.locator(\"text=link\").click() + await page.get_by_text("link").click() await page.wait_for_url("about:blank#foo")`); expect.soft(sources.get('C#').text).toContain(` - await page.Locator(\"text=link\").ClickAsync(); + await page.GetByText("link").ClickAsync(); await page.WaitForURLAsync(\"about:blank#foo\");`); expect(page.url()).toContain('about:blank#foo'); @@ -685,22 +685,22 @@ test.describe('cli codegen', () => { ]); expect(sources.get('JavaScript').text).toContain(` - await page.locator('text=Click me').click({ + await page.getByText('Click me').click({ button: 'middle' });`); expect(sources.get('Python').text).toContain(` - page.locator("text=Click me").click(button="middle")`); + page.get_by_text("Click me").click(button="middle")`); expect(sources.get('Python Async').text).toContain(` - await page.locator("text=Click me").click(button="middle")`); + await page.get_by_text("Click me").click(button="middle")`); expect(sources.get('Java').text).toContain(` - page.locator("text=Click me").click(new Locator.ClickOptions() + page.getByText("Click me").click(new Locator.ClickOptions() .setButton(MouseButton.MIDDLE));`); expect(sources.get('C#').text).toContain(` - await page.Locator("text=Click me").ClickAsync(new LocatorClickOptions + await page.GetByText("Click me").ClickAsync(new LocatorClickOptions { Button = MouseButton.Middle, });`); diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index 4ecc89c81f..fafeda94eb 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -231,28 +231,28 @@ test.describe('cli codegen', () => { expect(sources.get('JavaScript').text).toContain(` const [download] = await Promise.all([ page.waitForEvent('download'), - page.locator('role=link[name=\"Download\"]').click() + page.getByRole('link', { name: 'Download' }).click() ]);`); expect(sources.get('Java').text).toContain(` BrowserContext context = browser.newContext();`); expect(sources.get('Java').text).toContain(` Download download = page.waitForDownload(() -> { - page.locator("role=link[name=\\\"Download\\\"]").click(); + page.getByRole("link", new Page.GetByRoleOptions().setName("Download")).click(); });`); expect(sources.get('Python').text).toContain(` context = browser.new_context()`); expect(sources.get('Python').text).toContain(` with page.expect_download() as download_info: - page.locator(\"role=link[name=\\\"Download\\\"]\").click() + page.get_by_role("link", name="Download").click() download = download_info.value`); expect(sources.get('Python Async').text).toContain(` context = await browser.new_context()`); expect(sources.get('Python Async').text).toContain(` async with page.expect_download() as download_info: - await page.locator(\"role=link[name=\\\"Download\\\"]\").click() + await page.get_by_role("link", name="Download").click() download = await download_info.value`); expect(sources.get('C#').text).toContain(` @@ -260,7 +260,7 @@ test.describe('cli codegen', () => { expect(sources.get('C#').text).toContain(` var download1 = await page.RunAndWaitForDownloadAsync(async () => { - await page.Locator(\"role=link[name=\\\"Download\\\"]\").ClickAsync(); + await page.GetByRole("link", new () { Name = "Download" }).ClickAsync(); });`); }); @@ -283,22 +283,22 @@ test.describe('cli codegen', () => { console.log(\`Dialog message: \${dialog.message()}\`); dialog.dismiss().catch(() => {}); }); - await page.locator('role=button[name=\"click me\"]').click();`); + await page.getByRole('button', { name: 'click me' }).click();`); expect(sources.get('Java').text).toContain(` page.onceDialog(dialog -> { System.out.println(String.format("Dialog message: %s", dialog.message())); dialog.dismiss(); }); - page.locator("role=button[name=\\\"click me\\\"]").click();`); + page.getByRole("button", new Page.GetByRoleOptions().setName("click me")).click();`); expect(sources.get('Python').text).toContain(` page.once(\"dialog\", lambda dialog: dialog.dismiss()) - page.locator(\"role=button[name=\\\"click me\\\"]\").click()`); + page.get_by_role("button", name="click me").click()`); expect(sources.get('Python Async').text).toContain(` page.once(\"dialog\", lambda dialog: dialog.dismiss()) - await page.locator(\"role=button[name=\\\"click me\\\"]\").click()`); + await page.get_by_role("button", name="click me").click()`); expect(sources.get('C#').text).toContain(` void page_Dialog1_EventHandler(object sender, IDialog dialog) @@ -308,7 +308,7 @@ test.describe('cli codegen', () => { page.Dialog -= page_Dialog1_EventHandler; } page.Dialog += page_Dialog1_EventHandler; - await page.Locator(\"role=button[name=\\\"click me\\\"]\").ClickAsync();`); + await page.GetByRole("button", new () { Name = "click me" }).ClickAsync();`); }); @@ -352,7 +352,7 @@ test.describe('cli codegen', () => { expect(sources.get('JavaScript').text).toContain(` const [page1] = await Promise.all([ page.waitForEvent('popup'), - page.locator('role=link[name=\"link\"]').click({ + page.getByRole('link', { name: 'link' }).click({ modifiers: ['${platform === 'darwin' ? 'Meta' : 'Control'}'] }) ]);`); diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 544c85e82d..2ec12994a4 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -36,20 +36,20 @@ test.describe('cli codegen', () => { page.dispatchEvent('button', 'click', { detail: 1 }) ]); - expect(sources.get('JavaScript').text).toContain(` - await page.locator('role=button[name=\"Submit\"]').first().click();`); + expect.soft(sources.get('JavaScript').text).toContain(` + await page.getByRole('button', { name: 'Submit' }).first().click();`); - expect(sources.get('Python').text).toContain(` - page.locator("role=button[name=\\\"Submit\\\"]").first.click()`); + expect.soft(sources.get('Python').text).toContain(` + page.get_by_role("button", name="Submit").first.click()`); - expect(sources.get('Python Async').text).toContain(` - await page.locator("role=button[name=\\\"Submit\\\"]").first.click()`); + expect.soft(sources.get('Python Async').text).toContain(` + await page.get_by_role("button", name="Submit").first.click()`); - expect(sources.get('Java').text).toContain(` - page.locator("role=button[name=\\\"Submit\\\"]").first().click();`); + expect.soft(sources.get('Java').text).toContain(` + page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).first().click();`); - expect(sources.get('C#').text).toContain(` - await page.Locator("role=button[name=\\\"Submit\\\"]").First.ClickAsync();`); + expect.soft(sources.get('C#').text).toContain(` + await page.GetByRole("button", new () { Name = "Submit" }).First.ClickAsync();`); expect(message.text()).toBe('click1'); }); @@ -71,20 +71,20 @@ test.describe('cli codegen', () => { page.dispatchEvent('button', 'click', { detail: 1 }) ]); - expect(sources.get('JavaScript').text).toContain(` - await page.locator('role=button[name=\"Submit\"]').nth(1).click();`); + expect.soft(sources.get('JavaScript').text).toContain(` + await page.getByRole('button', { name: 'Submit' }).nth(1).click();`); - expect(sources.get('Python').text).toContain(` - page.locator("role=button[name=\\\"Submit\\\"]").nth(1).click()`); + expect.soft(sources.get('Python').text).toContain(` + page.get_by_role("button", name="Submit").nth(1).click()`); - expect(sources.get('Python Async').text).toContain(` - await page.locator("role=button[name=\\\"Submit\\\"]").nth(1).click()`); + expect.soft(sources.get('Python Async').text).toContain(` + await page.get_by_role("button", name="Submit").nth(1).click()`); - expect(sources.get('Java').text).toContain(` - page.locator("role=button[name=\\\"Submit\\\"]").nth(1).click();`); + expect.soft(sources.get('Java').text).toContain(` + page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).nth(1).click();`); - expect(sources.get('C#').text).toContain(` - await page.Locator("role=button[name=\\\"Submit\\\"]").Nth(1).ClickAsync();`); + expect.soft(sources.get('C#').text).toContain(` + await page.GetByRole("button", new () { Name = "Submit" }).Nth(1).ClickAsync();`); expect(message.text()).toBe('click2'); }); @@ -121,19 +121,19 @@ test.describe('cli codegen', () => { ]); expect(sources.get('JavaScript').text).toContain(` - await page.frameLocator('#frame1').locator('text=Hello1').click();`); + await page.frameLocator('#frame1').getByText('Hello1').click();`); expect(sources.get('Java').text).toContain(` - page.frameLocator("#frame1").locator("text=Hello1").click();`); + page.frameLocator("#frame1").getByText("Hello1").click();`); expect(sources.get('Python').text).toContain(` - page.frame_locator("#frame1").locator("text=Hello1").click()`); + page.frame_locator("#frame1").get_by_text("Hello1").click()`); expect(sources.get('Python Async').text).toContain(` - await page.frame_locator("#frame1").locator("text=Hello1").click()`); + await page.frame_locator("#frame1").get_by_text("Hello1").click()`); expect(sources.get('C#').text).toContain(` - await page.FrameLocator("#frame1").Locator("text=Hello1").ClickAsync();`); + await page.FrameLocator("#frame1").GetByText("Hello1").ClickAsync();`); [sources] = await Promise.all([ @@ -141,20 +141,20 @@ test.describe('cli codegen', () => { frameHello2.click('text=Hello2'), ]); - expect(sources.get('JavaScript').text).toContain(` - await page.frameLocator('#frame1').frameLocator('iframe').locator('text=Hello2').click();`); + expect.soft(sources.get('JavaScript').text).toContain(` + await page.frameLocator('#frame1').frameLocator('iframe').getByText('Hello2').click();`); - expect(sources.get('Java').text).toContain(` - page.frameLocator("#frame1").frameLocator("iframe").locator("text=Hello2").click();`); + expect.soft(sources.get('Java').text).toContain(` + page.frameLocator("#frame1").frameLocator("iframe").getByText("Hello2").click();`); - expect(sources.get('Python').text).toContain(` - page.frame_locator("#frame1").frame_locator("iframe").locator("text=Hello2").click()`); + expect.soft(sources.get('Python').text).toContain(` + page.frame_locator("#frame1").frame_locator("iframe").get_by_text("Hello2").click()`); - expect(sources.get('Python Async').text).toContain(` - await page.frame_locator("#frame1").frame_locator("iframe").locator("text=Hello2").click()`); + expect.soft(sources.get('Python Async').text).toContain(` + await page.frame_locator("#frame1").frame_locator("iframe").get_by_text("Hello2").click()`); - expect(sources.get('C#').text).toContain(` - await page.FrameLocator("#frame1").FrameLocator("iframe").Locator("text=Hello2").ClickAsync();`); + expect.soft(sources.get('C#').text).toContain(` + await page.FrameLocator("#frame1").FrameLocator("iframe").GetByText("Hello2").ClickAsync();`); [sources] = await Promise.all([ @@ -162,22 +162,22 @@ test.describe('cli codegen', () => { frameOne.click('text=HelloNameOne'), ]); - expect(sources.get('JavaScript').text).toContain(` + expect.soft(sources.get('JavaScript').text).toContain(` await page.frame({ name: 'one' - }).locator('text=HelloNameOne').click();`); + }).getByText('HelloNameOne').click();`); - expect(sources.get('Java').text).toContain(` - page.frame("one").locator("text=HelloNameOne").click();`); + expect.soft(sources.get('Java').text).toContain(` + page.frame("one").getByText("HelloNameOne").click();`); - expect(sources.get('Python').text).toContain(` - page.frame(name=\"one\").locator(\"text=HelloNameOne\").click()`); + expect.soft(sources.get('Python').text).toContain(` + page.frame(name=\"one\").get_by_text(\"HelloNameOne\").click()`); - expect(sources.get('Python Async').text).toContain(` - await page.frame(name=\"one\").locator(\"text=HelloNameOne\").click()`); + expect.soft(sources.get('Python Async').text).toContain(` + await page.frame(name=\"one\").get_by_text(\"HelloNameOne\").click()`); - expect(sources.get('C#').text).toContain(` - await page.Frame(\"one\").Locator(\"text=HelloNameOne\").ClickAsync();`); + expect.soft(sources.get('C#').text).toContain(` + await page.Frame(\"one\").GetByText(\"HelloNameOne\").ClickAsync();`); [sources] = await Promise.all([ @@ -185,21 +185,138 @@ test.describe('cli codegen', () => { frameAnonymous.click('text=HelloNameAnonymous'), ]); - expect(sources.get('JavaScript').text).toContain(` + expect.soft(sources.get('JavaScript').text).toContain(` await page.frame({ url: 'about:blank' - }).locator('text=HelloNameAnonymous').click();`); + }).getByText('HelloNameAnonymous').click();`); - expect(sources.get('Java').text).toContain(` - page.frameByUrl("about:blank").locator("text=HelloNameAnonymous").click();`); + expect.soft(sources.get('Java').text).toContain(` + page.frameByUrl("about:blank").getByText("HelloNameAnonymous").click();`); - expect(sources.get('Python').text).toContain(` - page.frame(url=\"about:blank\").locator(\"text=HelloNameAnonymous\").click()`); + expect.soft(sources.get('Python').text).toContain(` + page.frame(url=\"about:blank\").get_by_text(\"HelloNameAnonymous\").click()`); - expect(sources.get('Python Async').text).toContain(` - await page.frame(url=\"about:blank\").locator(\"text=HelloNameAnonymous\").click()`); + expect.soft(sources.get('Python Async').text).toContain(` + await page.frame(url=\"about:blank\").get_by_text(\"HelloNameAnonymous\").click()`); - expect(sources.get('C#').text).toContain(` - await page.FrameByUrl(\"about:blank\").Locator(\"text=HelloNameAnonymous\").ClickAsync();`); + expect.soft(sources.get('C#').text).toContain(` + await page.FrameByUrl(\"about:blank\").GetByText(\"HelloNameAnonymous\").ClickAsync();`); }); + + test('should generate role locators undef frame locators', async ({ page, openRecorder, server }) => { + const recorder = await openRecorder(); + await recorder.setContentAndWait(`