From ef1b68a998e1588bc8128dc9d1f83d159656c21e Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 8 Nov 2022 17:08:08 -0800 Subject: [PATCH] feat(locators): support frame locators in `asLocator` (#18653) Drive-by: change `true` to `True` in python. References #18524. --- .../server/isomorphic/locatorGenerators.ts | 32 ++++++++++++++--- .../src/server/isomorphic/locatorParser.ts | 4 ++- tests/library/locator-generator.spec.ts | 35 ++++++++++++++----- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/packages/playwright-core/src/server/isomorphic/locatorGenerators.ts b/packages/playwright-core/src/server/isomorphic/locatorGenerators.ts index 3d1f24bdc7..d9e88f5593 100644 --- a/packages/playwright-core/src/server/isomorphic/locatorGenerators.ts +++ b/packages/playwright-core/src/server/isomorphic/locatorGenerators.ts @@ -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': diff --git a/packages/playwright-core/src/server/isomorphic/locatorParser.ts b/packages/playwright-core/src/server/isomorphic/locatorParser.ts index 099cdb8c76..eb754c8734 100644 --- a/packages/playwright-core/src/server/isomorphic/locatorParser.ts +++ b/packages/playwright-core/src/server/isomorphic/locatorParser.ts @@ -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 diff --git a/tests/library/locator-generator.spec.ts b/tests/library/locator-generator.spec.ts index 8c1646883f..8699877f12 100644 --- a/tests/library/locator-generator.spec.ts +++ b/tests/library/locator-generator.spec.ts @@ -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' });