feat(locators): support frame locators in asLocator (#18653)

Drive-by: change `true` to `True` in python.

References #18524.
This commit is contained in:
Dmitry Gozman 2022-11-08 17:08:08 -08:00 committed by GitHub
parent 54a235284a
commit ef1b68a998
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 57 additions and 14 deletions

View File

@ -19,7 +19,7 @@ import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringi
import type { ParsedSelector } from '../isomorphic/selectorParser';
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has';
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has' | 'frame';
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
export interface LocatorFactory {
@ -32,8 +32,12 @@ export function asLocator(lang: Language, selector: string, isFrameLocator: bool
function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrameLocator: boolean = false): string {
const tokens: string[] = [];
for (const part of parsed.parts) {
const base = part === parsed.parts[0] ? (isFrameLocator ? 'frame-locator' : 'page') : 'locator';
let nextBase: LocatorBase = isFrameLocator ? 'frame-locator' : 'page';
for (let index = 0; index < parsed.parts.length; index++) {
const part = parsed.parts[index];
const base = nextBase;
nextBase = 'locator';
if (part.name === 'nth') {
if (part.body === '0')
tokens.push(factory.generateLocator(base, 'first', ''));
@ -95,8 +99,18 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
continue;
}
}
let locatorType: LocatorType = 'default';
const nextPart = parsed.parts[index + 1];
if (nextPart && nextPart.name === 'internal:control' && (nextPart.body as string) === 'enter-frame') {
locatorType = 'frame';
nextBase = 'frame-locator';
index++;
}
const p: ParsedSelector = { parts: [part] };
tokens.push(factory.generateLocator(base, 'default', stringifySelector(p)));
tokens.push(factory.generateLocator(base, locatorType, stringifySelector(p)));
}
return tokens.join('.');
}
@ -124,6 +138,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
switch (kind) {
case 'default':
return `locator(${this.quote(body as string)})`;
case 'frame':
return `frameLocator(${this.quote(body as string)})`;
case 'nth':
return `nth(${body})`;
case 'first':
@ -179,6 +195,8 @@ export class PythonLocatorFactory implements LocatorFactory {
switch (kind) {
case 'default':
return `locator(${this.quote(body as string)})`;
case 'frame':
return `frame_locator(${this.quote(body as string)})`;
case 'nth':
return `nth(${body})`;
case 'first':
@ -218,7 +236,7 @@ export class PythonLocatorFactory implements LocatorFactory {
return `${method}(re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix}))`;
}
if (exact)
return `${method}(${this.quote(body)}, exact=true)`;
return `${method}(${this.quote(body)}, exact=True)`;
return `${method}(${this.quote(body)})`;
}
@ -246,6 +264,8 @@ export class JavaLocatorFactory implements LocatorFactory {
switch (kind) {
case 'default':
return `locator(${this.quote(body as string)})`;
case 'frame':
return `frameLocator(${this.quote(body as string)})`;
case 'nth':
return `nth(${body})`;
case 'first':
@ -307,6 +327,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
switch (kind) {
case 'default':
return `Locator(${this.quote(body as string)})`;
case 'frame':
return `FrameLocator(${this.quote(body as string)})`;
case 'nth':
return `Nth(${body})`;
case 'first':

View File

@ -72,6 +72,7 @@ function parseLocator(locator: string, testIdAttributeName: string): string {
.replace(/get_by_test_id/g, 'getbytestid')
.replace(/get_by_([\w]+)/g, 'getby$1')
.replace(/has_text/g, 'hastext')
.replace(/frame_locator/g, 'framelocator')
.replace(/[{}\s]/g, '')
.replace(/new\(\)/g, '')
.replace(/new[\w]+\.[\w]+options\(\)/g, '')
@ -135,6 +136,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
// Transform to selector engines.
template = template
.replace(/framelocator\(([^)]+)\)/g, '$1.internal:control=enter-frame')
.replace(/locator\(([^)]+)\)/g, '$1')
.replace(/getbyrole\(([^)]+)\)/g, 'internal:role=$1')
.replace(/getbytext\(([^)]+)\)/g, 'internal:text=$1')
@ -152,7 +154,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
// Substitute params.
return template.split('.').map(t => {
if (!t.startsWith('internal:'))
if (!t.startsWith('internal:') || t === 'internal:control')
return t.replace(/\$(\d+)/g, (_, ordinal) => { const param = params[+ordinal - 1]; return param.text; });
t = t.includes('[') ? t.replace(/\]/, '') + ']' : t;
t = t

View File

@ -17,12 +17,12 @@
import { contextTest as it, expect } from '../config/browserTest';
import { asLocator } from '../../packages/playwright-core/lib/server/isomorphic/locatorGenerators';
import { locatorOrSelectorAsSelector as parseLocator } from '../../packages/playwright-core/lib/server/isomorphic/locatorParser';
import type { Page, Frame, Locator } from 'playwright-core';
import type { Page, Frame, Locator, FrameLocator } from 'playwright-core';
it.skip(({ mode }) => mode !== 'default');
function generate(locator: Locator) {
return generateForSelector((locator as any)._selector);
function generate(locator: Locator | FrameLocator) {
return generateForSelector((locator as any)._selector || (locator as any)._frameSelector);
}
function generateForSelector(selector: string) {
@ -65,7 +65,7 @@ it('reverse engineer locators', async ({ page }) => {
csharp: 'GetByText("Hello", new() { Exact: true })',
java: 'getByText("Hello", new Page.GetByTextOptions().setExact(true))',
javascript: 'getByText(\'Hello\', { exact: true })',
python: 'get_by_text("Hello", exact=true)',
python: 'get_by_text("Hello", exact=True)',
});
expect.soft(generate(page.getByText('Hello'))).toEqual({
@ -90,7 +90,7 @@ it('reverse engineer locators', async ({ page }) => {
csharp: 'GetByLabel("Last Name", new() { Exact: true })',
java: 'getByLabel("Last Name", new Page.GetByLabelOptions().setExact(true))',
javascript: 'getByLabel(\'Last Name\', { exact: true })',
python: 'get_by_label("Last Name", exact=true)',
python: 'get_by_label("Last Name", exact=True)',
});
expect.soft(generate(page.getByLabel(/Last\s+name/i))).toEqual({
csharp: 'GetByLabel(new Regex("Last\\\\s+name", RegexOptions.IgnoreCase))',
@ -109,7 +109,7 @@ it('reverse engineer locators', async ({ page }) => {
csharp: 'GetByPlaceholder("Hello", new() { Exact: true })',
java: 'getByPlaceholder("Hello", new Page.GetByPlaceholderOptions().setExact(true))',
javascript: 'getByPlaceholder(\'Hello\', { exact: true })',
python: 'get_by_placeholder("Hello", exact=true)',
python: 'get_by_placeholder("Hello", exact=True)',
});
expect.soft(generate(page.getByPlaceholder(/wor/i))).toEqual({
csharp: 'GetByPlaceholder(new Regex("wor", RegexOptions.IgnoreCase))',
@ -128,7 +128,7 @@ it('reverse engineer locators', async ({ page }) => {
csharp: 'GetByAltText("Hello", new() { Exact: true })',
java: 'getByAltText("Hello", new Page.GetByAltTextOptions().setExact(true))',
javascript: 'getByAltText(\'Hello\', { exact: true })',
python: 'get_by_alt_text("Hello", exact=true)',
python: 'get_by_alt_text("Hello", exact=True)',
});
expect.soft(generate(page.getByAltText(/wor/i))).toEqual({
csharp: 'GetByAltText(new Regex("wor", RegexOptions.IgnoreCase))',
@ -147,7 +147,7 @@ it('reverse engineer locators', async ({ page }) => {
csharp: 'GetByTitle("Hello", new() { Exact: true })',
java: 'getByTitle("Hello", new Page.GetByTitleOptions().setExact(true))',
javascript: 'getByTitle(\'Hello\', { exact: true })',
python: 'get_by_title("Hello", exact=true)',
python: 'get_by_title("Hello", exact=True)',
});
expect.soft(generate(page.getByTitle(/wor/i))).toEqual({
csharp: 'GetByTitle(new Regex("wor", RegexOptions.IgnoreCase))',
@ -279,6 +279,25 @@ it('reverse engineer has', async ({ page }) => {
});
});
it('reverse engineer frameLocator', async ({ page }) => {
const locator = page
.frameLocator('iframe')
.getByText('foo', { exact: true })
.frameLocator('frame')
.frameLocator('iframe')
.locator('span');
expect.soft(generate(locator)).toEqual({
csharp: `FrameLocator("iframe").GetByText("foo", new() { Exact: true }).FrameLocator("frame").FrameLocator("iframe").Locator("span")`,
java: `frameLocator("iframe").getByText("foo", new FrameLocator.GetByTextOptions().setExact(true)).frameLocator("frame").frameLocator("iframe").locator("span")`,
javascript: `frameLocator('iframe').getByText('foo', { exact: true }).frameLocator('frame').frameLocator('iframe').locator('span')`,
python: `frame_locator("iframe").get_by_text("foo", exact=True).frame_locator("frame").frame_locator("iframe").locator("span")`,
});
// Note that frame locators with ">>" are not restored back due to ambiguity.
const selector = (page.frameLocator('div >> iframe').locator('span') as any)._selector;
expect.soft(asLocator('javascript', selector, false)).toBe(`locator('div').frameLocator('iframe').locator('span')`);
});
it.describe(() => {
it.beforeEach(async ({ context }) => {
await (context as any)._enableRecorder({ language: 'javascript' });