mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(locators): support frame locators in asLocator (#18653)
Drive-by: change `true` to `True` in python. References #18524.
This commit is contained in:
parent
54a235284a
commit
ef1b68a998
@ -19,7 +19,7 @@ import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringi
|
|||||||
import type { ParsedSelector } from '../isomorphic/selectorParser';
|
import type { ParsedSelector } from '../isomorphic/selectorParser';
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
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 type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||||
|
|
||||||
export interface LocatorFactory {
|
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 {
|
function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrameLocator: boolean = false): string {
|
||||||
const tokens: string[] = [];
|
const tokens: string[] = [];
|
||||||
for (const part of parsed.parts) {
|
let nextBase: LocatorBase = isFrameLocator ? 'frame-locator' : 'page';
|
||||||
const base = part === parsed.parts[0] ? (isFrameLocator ? 'frame-locator' : 'page') : 'locator';
|
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.name === 'nth') {
|
||||||
if (part.body === '0')
|
if (part.body === '0')
|
||||||
tokens.push(factory.generateLocator(base, 'first', ''));
|
tokens.push(factory.generateLocator(base, 'first', ''));
|
||||||
@ -95,8 +99,18 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
|||||||
continue;
|
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] };
|
const p: ParsedSelector = { parts: [part] };
|
||||||
tokens.push(factory.generateLocator(base, 'default', stringifySelector(p)));
|
tokens.push(factory.generateLocator(base, locatorType, stringifySelector(p)));
|
||||||
}
|
}
|
||||||
return tokens.join('.');
|
return tokens.join('.');
|
||||||
}
|
}
|
||||||
@ -124,6 +138,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'default':
|
case 'default':
|
||||||
return `locator(${this.quote(body as string)})`;
|
return `locator(${this.quote(body as string)})`;
|
||||||
|
case 'frame':
|
||||||
|
return `frameLocator(${this.quote(body as string)})`;
|
||||||
case 'nth':
|
case 'nth':
|
||||||
return `nth(${body})`;
|
return `nth(${body})`;
|
||||||
case 'first':
|
case 'first':
|
||||||
@ -179,6 +195,8 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'default':
|
case 'default':
|
||||||
return `locator(${this.quote(body as string)})`;
|
return `locator(${this.quote(body as string)})`;
|
||||||
|
case 'frame':
|
||||||
|
return `frame_locator(${this.quote(body as string)})`;
|
||||||
case 'nth':
|
case 'nth':
|
||||||
return `nth(${body})`;
|
return `nth(${body})`;
|
||||||
case 'first':
|
case 'first':
|
||||||
@ -218,7 +236,7 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||||||
return `${method}(re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix}))`;
|
return `${method}(re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix}))`;
|
||||||
}
|
}
|
||||||
if (exact)
|
if (exact)
|
||||||
return `${method}(${this.quote(body)}, exact=true)`;
|
return `${method}(${this.quote(body)}, exact=True)`;
|
||||||
return `${method}(${this.quote(body)})`;
|
return `${method}(${this.quote(body)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,6 +264,8 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'default':
|
case 'default':
|
||||||
return `locator(${this.quote(body as string)})`;
|
return `locator(${this.quote(body as string)})`;
|
||||||
|
case 'frame':
|
||||||
|
return `frameLocator(${this.quote(body as string)})`;
|
||||||
case 'nth':
|
case 'nth':
|
||||||
return `nth(${body})`;
|
return `nth(${body})`;
|
||||||
case 'first':
|
case 'first':
|
||||||
@ -307,6 +327,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'default':
|
case 'default':
|
||||||
return `Locator(${this.quote(body as string)})`;
|
return `Locator(${this.quote(body as string)})`;
|
||||||
|
case 'frame':
|
||||||
|
return `FrameLocator(${this.quote(body as string)})`;
|
||||||
case 'nth':
|
case 'nth':
|
||||||
return `Nth(${body})`;
|
return `Nth(${body})`;
|
||||||
case 'first':
|
case 'first':
|
||||||
|
|||||||
@ -72,6 +72,7 @@ function parseLocator(locator: string, testIdAttributeName: string): string {
|
|||||||
.replace(/get_by_test_id/g, 'getbytestid')
|
.replace(/get_by_test_id/g, 'getbytestid')
|
||||||
.replace(/get_by_([\w]+)/g, 'getby$1')
|
.replace(/get_by_([\w]+)/g, 'getby$1')
|
||||||
.replace(/has_text/g, 'hastext')
|
.replace(/has_text/g, 'hastext')
|
||||||
|
.replace(/frame_locator/g, 'framelocator')
|
||||||
.replace(/[{}\s]/g, '')
|
.replace(/[{}\s]/g, '')
|
||||||
.replace(/new\(\)/g, '')
|
.replace(/new\(\)/g, '')
|
||||||
.replace(/new[\w]+\.[\w]+options\(\)/g, '')
|
.replace(/new[\w]+\.[\w]+options\(\)/g, '')
|
||||||
@ -135,6 +136,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
|||||||
|
|
||||||
// Transform to selector engines.
|
// Transform to selector engines.
|
||||||
template = template
|
template = template
|
||||||
|
.replace(/framelocator\(([^)]+)\)/g, '$1.internal:control=enter-frame')
|
||||||
.replace(/locator\(([^)]+)\)/g, '$1')
|
.replace(/locator\(([^)]+)\)/g, '$1')
|
||||||
.replace(/getbyrole\(([^)]+)\)/g, 'internal:role=$1')
|
.replace(/getbyrole\(([^)]+)\)/g, 'internal:role=$1')
|
||||||
.replace(/getbytext\(([^)]+)\)/g, 'internal:text=$1')
|
.replace(/getbytext\(([^)]+)\)/g, 'internal:text=$1')
|
||||||
@ -152,7 +154,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
|||||||
|
|
||||||
// Substitute params.
|
// Substitute params.
|
||||||
return template.split('.').map(t => {
|
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; });
|
return t.replace(/\$(\d+)/g, (_, ordinal) => { const param = params[+ordinal - 1]; return param.text; });
|
||||||
t = t.includes('[') ? t.replace(/\]/, '') + ']' : t;
|
t = t.includes('[') ? t.replace(/\]/, '') + ']' : t;
|
||||||
t = t
|
t = t
|
||||||
|
|||||||
@ -17,12 +17,12 @@
|
|||||||
import { contextTest as it, expect } from '../config/browserTest';
|
import { contextTest as it, expect } from '../config/browserTest';
|
||||||
import { asLocator } from '../../packages/playwright-core/lib/server/isomorphic/locatorGenerators';
|
import { asLocator } from '../../packages/playwright-core/lib/server/isomorphic/locatorGenerators';
|
||||||
import { locatorOrSelectorAsSelector as parseLocator } from '../../packages/playwright-core/lib/server/isomorphic/locatorParser';
|
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');
|
it.skip(({ mode }) => mode !== 'default');
|
||||||
|
|
||||||
function generate(locator: Locator) {
|
function generate(locator: Locator | FrameLocator) {
|
||||||
return generateForSelector((locator as any)._selector);
|
return generateForSelector((locator as any)._selector || (locator as any)._frameSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateForSelector(selector: string) {
|
function generateForSelector(selector: string) {
|
||||||
@ -65,7 +65,7 @@ it('reverse engineer locators', async ({ page }) => {
|
|||||||
csharp: 'GetByText("Hello", new() { Exact: true })',
|
csharp: 'GetByText("Hello", new() { Exact: true })',
|
||||||
java: 'getByText("Hello", new Page.GetByTextOptions().setExact(true))',
|
java: 'getByText("Hello", new Page.GetByTextOptions().setExact(true))',
|
||||||
javascript: 'getByText(\'Hello\', { exact: 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({
|
expect.soft(generate(page.getByText('Hello'))).toEqual({
|
||||||
@ -90,7 +90,7 @@ it('reverse engineer locators', async ({ page }) => {
|
|||||||
csharp: 'GetByLabel("Last Name", new() { Exact: true })',
|
csharp: 'GetByLabel("Last Name", new() { Exact: true })',
|
||||||
java: 'getByLabel("Last Name", new Page.GetByLabelOptions().setExact(true))',
|
java: 'getByLabel("Last Name", new Page.GetByLabelOptions().setExact(true))',
|
||||||
javascript: 'getByLabel(\'Last Name\', { exact: 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({
|
expect.soft(generate(page.getByLabel(/Last\s+name/i))).toEqual({
|
||||||
csharp: 'GetByLabel(new Regex("Last\\\\s+name", RegexOptions.IgnoreCase))',
|
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 })',
|
csharp: 'GetByPlaceholder("Hello", new() { Exact: true })',
|
||||||
java: 'getByPlaceholder("Hello", new Page.GetByPlaceholderOptions().setExact(true))',
|
java: 'getByPlaceholder("Hello", new Page.GetByPlaceholderOptions().setExact(true))',
|
||||||
javascript: 'getByPlaceholder(\'Hello\', { exact: 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({
|
expect.soft(generate(page.getByPlaceholder(/wor/i))).toEqual({
|
||||||
csharp: 'GetByPlaceholder(new Regex("wor", RegexOptions.IgnoreCase))',
|
csharp: 'GetByPlaceholder(new Regex("wor", RegexOptions.IgnoreCase))',
|
||||||
@ -128,7 +128,7 @@ it('reverse engineer locators', async ({ page }) => {
|
|||||||
csharp: 'GetByAltText("Hello", new() { Exact: true })',
|
csharp: 'GetByAltText("Hello", new() { Exact: true })',
|
||||||
java: 'getByAltText("Hello", new Page.GetByAltTextOptions().setExact(true))',
|
java: 'getByAltText("Hello", new Page.GetByAltTextOptions().setExact(true))',
|
||||||
javascript: 'getByAltText(\'Hello\', { exact: 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({
|
expect.soft(generate(page.getByAltText(/wor/i))).toEqual({
|
||||||
csharp: 'GetByAltText(new Regex("wor", RegexOptions.IgnoreCase))',
|
csharp: 'GetByAltText(new Regex("wor", RegexOptions.IgnoreCase))',
|
||||||
@ -147,7 +147,7 @@ it('reverse engineer locators', async ({ page }) => {
|
|||||||
csharp: 'GetByTitle("Hello", new() { Exact: true })',
|
csharp: 'GetByTitle("Hello", new() { Exact: true })',
|
||||||
java: 'getByTitle("Hello", new Page.GetByTitleOptions().setExact(true))',
|
java: 'getByTitle("Hello", new Page.GetByTitleOptions().setExact(true))',
|
||||||
javascript: 'getByTitle(\'Hello\', { exact: 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({
|
expect.soft(generate(page.getByTitle(/wor/i))).toEqual({
|
||||||
csharp: 'GetByTitle(new Regex("wor", RegexOptions.IgnoreCase))',
|
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.describe(() => {
|
||||||
it.beforeEach(async ({ context }) => {
|
it.beforeEach(async ({ context }) => {
|
||||||
await (context as any)._enableRecorder({ language: 'javascript' });
|
await (context as any)._enableRecorder({ language: 'javascript' });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user