mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(locators): allow ambiguous locators when parsing (#23034)
This supports locators like `nth(0)` and `locator('div', { hasText: 'foo' })` that are not canonical, but still work. Fixes #22990, #22965.
This commit is contained in:
parent
f469e4b1eb
commit
73f9f81db4
@ -22,25 +22,35 @@ 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-not-text' | 'has' | 'hasNot' | 'frame' | 'and' | 'or';
|
||||
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||
|
||||
type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp };
|
||||
type LocatorOptions = {
|
||||
attrs?: { name: string, value: string | boolean | number }[],
|
||||
exact?: boolean,
|
||||
name?: string | RegExp,
|
||||
hasText?: string | RegExp,
|
||||
hasNotText?: string | RegExp,
|
||||
};
|
||||
export interface LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options?: LocatorOptions): string;
|
||||
}
|
||||
|
||||
export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false, playSafe: boolean = false): string {
|
||||
return asLocators(lang, selector, isFrameLocator, playSafe)[0];
|
||||
}
|
||||
|
||||
export function asLocators(lang: Language, selector: string, isFrameLocator: boolean = false, playSafe: boolean = false, maxOutputSize = 20): string[] {
|
||||
if (playSafe) {
|
||||
try {
|
||||
return innerAsLocator(generators[lang], parseSelector(selector), isFrameLocator);
|
||||
return innerAsLocators(generators[lang], parseSelector(selector), isFrameLocator, maxOutputSize);
|
||||
} catch (e) {
|
||||
// Tolerate invalid input.
|
||||
return selector;
|
||||
return [selector];
|
||||
}
|
||||
} else {
|
||||
return innerAsLocator(generators[lang], parseSelector(selector), isFrameLocator);
|
||||
return innerAsLocators(generators[lang], parseSelector(selector), isFrameLocator, maxOutputSize);
|
||||
}
|
||||
}
|
||||
|
||||
function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrameLocator: boolean = false): string {
|
||||
function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFrameLocator: boolean = false, maxOutputSize = 20): string[] {
|
||||
const parts = [...parsed.parts];
|
||||
// frameLocator('iframe').first is actually "iframe >> nth=0 >> internal:control=enter-frame"
|
||||
// To make it easier to parse, we turn it into "iframe >> internal:control=enter-frame >> nth=0"
|
||||
@ -52,7 +62,7 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||
}
|
||||
}
|
||||
|
||||
const tokens: string[] = [];
|
||||
const tokens: string[][] = [];
|
||||
let nextBase: LocatorBase = isFrameLocator ? 'frame-locator' : 'page';
|
||||
for (let index = 0; index < parts.length; index++) {
|
||||
const part = parts[index];
|
||||
@ -61,23 +71,23 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||
|
||||
if (part.name === 'nth') {
|
||||
if (part.body === '0')
|
||||
tokens.push(factory.generateLocator(base, 'first', ''));
|
||||
tokens.push([factory.generateLocator(base, 'first', ''), factory.generateLocator(base, 'nth', '0')]);
|
||||
else if (part.body === '-1')
|
||||
tokens.push(factory.generateLocator(base, 'last', ''));
|
||||
tokens.push([factory.generateLocator(base, 'last', ''), factory.generateLocator(base, 'nth', '-1')]);
|
||||
else
|
||||
tokens.push(factory.generateLocator(base, 'nth', part.body as string));
|
||||
tokens.push([factory.generateLocator(base, 'nth', part.body as string)]);
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:text') {
|
||||
const { exact, text } = detectExact(part.body as string);
|
||||
tokens.push(factory.generateLocator(base, 'text', text, { exact }));
|
||||
tokens.push([factory.generateLocator(base, 'text', text, { exact })]);
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:has-text') {
|
||||
const { exact, text } = detectExact(part.body as string);
|
||||
// There is no locator equivalent for strict has-text, leave it as is.
|
||||
if (!exact) {
|
||||
tokens.push(factory.generateLocator(base, 'has-text', text, { exact }));
|
||||
tokens.push([factory.generateLocator(base, 'has-text', text, { exact })]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -85,33 +95,33 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||
const { exact, text } = detectExact(part.body as string);
|
||||
// There is no locator equivalent for strict has-not-text, leave it as is.
|
||||
if (!exact) {
|
||||
tokens.push(factory.generateLocator(base, 'has-not-text', text, { exact }));
|
||||
tokens.push([factory.generateLocator(base, 'has-not-text', text, { exact })]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (part.name === 'internal:has') {
|
||||
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||
tokens.push(factory.generateLocator(base, 'has', inner));
|
||||
const inners = innerAsLocators(factory, (part.body as NestedSelectorBody).parsed, false, maxOutputSize);
|
||||
tokens.push(inners.map(inner => factory.generateLocator(base, 'has', inner)));
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:has-not') {
|
||||
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||
tokens.push(factory.generateLocator(base, 'hasNot', inner));
|
||||
const inners = innerAsLocators(factory, (part.body as NestedSelectorBody).parsed, false, maxOutputSize);
|
||||
tokens.push(inners.map(inner => factory.generateLocator(base, 'hasNot', inner)));
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:and') {
|
||||
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||
tokens.push(factory.generateLocator(base, 'and', inner));
|
||||
const inners = innerAsLocators(factory, (part.body as NestedSelectorBody).parsed, false, maxOutputSize);
|
||||
tokens.push(inners.map(inner => factory.generateLocator(base, 'and', inner)));
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:or') {
|
||||
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||
tokens.push(factory.generateLocator(base, 'or', inner));
|
||||
const inners = innerAsLocators(factory, (part.body as NestedSelectorBody).parsed, false, maxOutputSize);
|
||||
tokens.push(inners.map(inner => factory.generateLocator(base, 'or', inner)));
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:label') {
|
||||
const { exact, text } = detectExact(part.body as string);
|
||||
tokens.push(factory.generateLocator(base, 'label', text, { exact }));
|
||||
tokens.push([factory.generateLocator(base, 'label', text, { exact })]);
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:role') {
|
||||
@ -127,13 +137,13 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||
options.attrs!.push({ name: attr.name === 'include-hidden' ? 'includeHidden' : attr.name, value: attr.value });
|
||||
}
|
||||
}
|
||||
tokens.push(factory.generateLocator(base, 'role', attrSelector.name, options));
|
||||
tokens.push([factory.generateLocator(base, 'role', attrSelector.name, options)]);
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:testid') {
|
||||
const attrSelector = parseAttributeSelector(part.body as string, true);
|
||||
const { value } = attrSelector.attributes[0];
|
||||
tokens.push(factory.generateLocator(base, 'test-id', value));
|
||||
tokens.push([factory.generateLocator(base, 'test-id', value)]);
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:attr') {
|
||||
@ -142,15 +152,15 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||
const text = value as string | RegExp;
|
||||
const exact = !!caseSensitive;
|
||||
if (name === 'placeholder') {
|
||||
tokens.push(factory.generateLocator(base, 'placeholder', text, { exact }));
|
||||
tokens.push([factory.generateLocator(base, 'placeholder', text, { exact })]);
|
||||
continue;
|
||||
}
|
||||
if (name === 'alt') {
|
||||
tokens.push(factory.generateLocator(base, 'alt', text, { exact }));
|
||||
tokens.push([factory.generateLocator(base, 'alt', text, { exact })]);
|
||||
continue;
|
||||
}
|
||||
if (name === 'title') {
|
||||
tokens.push(factory.generateLocator(base, 'title', text, { exact }));
|
||||
tokens.push([factory.generateLocator(base, 'title', text, { exact })]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -164,10 +174,54 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||
index++;
|
||||
}
|
||||
|
||||
const p: ParsedSelector = { parts: [part] };
|
||||
tokens.push(factory.generateLocator(base, locatorType, stringifySelector(p)));
|
||||
const selectorPart = stringifySelector({ parts: [part] });
|
||||
const locatorPart = factory.generateLocator(base, locatorType, selectorPart);
|
||||
|
||||
if (locatorType === 'default' && nextPart && ['internal:has-text', 'internal:has-not-text'].includes(nextPart.name)) {
|
||||
const { exact, text } = detectExact(nextPart.body as string);
|
||||
// There is no locator equivalent for strict has-text and has-not-text, leave it as is.
|
||||
if (!exact) {
|
||||
const nextLocatorPart = factory.generateLocator('locator', nextPart.name === 'internal:has-text' ? 'has-text' : 'has-not-text', text, { exact });
|
||||
const options: LocatorOptions = {};
|
||||
if (nextPart.name === 'internal:has-text')
|
||||
options.hasText = text;
|
||||
else
|
||||
options.hasNotText = text;
|
||||
const combinedPart = factory.generateLocator(base, 'default', selectorPart, options);
|
||||
// Two options:
|
||||
// - locator('div').filter({ hasText: 'foo' })
|
||||
// - locator('div', { hasText: 'foo' })
|
||||
tokens.push([locatorPart + '.' + nextLocatorPart, combinedPart]);
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push([locatorPart]);
|
||||
}
|
||||
return tokens.join('.');
|
||||
|
||||
return combineTokens(tokens, maxOutputSize);
|
||||
}
|
||||
|
||||
function combineTokens(tokens: string[][], maxOutputSize: number): string[] {
|
||||
const currentTokens = tokens.map(() => '');
|
||||
const result: string[] = [];
|
||||
|
||||
const visit = (index: number) => {
|
||||
if (index === tokens.length) {
|
||||
result.push(currentTokens.join('.'));
|
||||
return currentTokens.length < maxOutputSize;
|
||||
}
|
||||
for (const taken of tokens[index]) {
|
||||
currentTokens[index] = taken;
|
||||
if (!visit(index + 1))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
visit(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
function detectExact(text: string): { exact?: boolean, text: string | RegExp } {
|
||||
@ -192,6 +246,10 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
if (options.hasText !== undefined)
|
||||
return `locator(${this.quote(body as string)}, { hasText: ${this.toHasText(options.hasText)} })`;
|
||||
if (options.hasNotText !== undefined)
|
||||
return `locator(${this.quote(body as string)}, { hasNotText: ${this.toHasText(options.hasNotText)} })`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `frameLocator(${this.quote(body as string)})`;
|
||||
@ -215,9 +273,9 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||
const attrString = attrs.length ? `, { ${attrs.join(', ')} }` : '';
|
||||
return `getByRole(${this.quote(body as string)}${attrString})`;
|
||||
case 'has-text':
|
||||
return `filter({ hasText: ${this.toHasText(body as string)} })`;
|
||||
return `filter({ hasText: ${this.toHasText(body)} })`;
|
||||
case 'has-not-text':
|
||||
return `filter({ hasNotText: ${this.toHasText(body as string)} })`;
|
||||
return `filter({ hasNotText: ${this.toHasText(body)} })`;
|
||||
case 'has':
|
||||
return `filter({ has: ${body} })`;
|
||||
case 'hasNot':
|
||||
@ -264,6 +322,10 @@ export class PythonLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
if (options.hasText !== undefined)
|
||||
return `locator(${this.quote(body as string)}, has_text=${this.toHasText(options.hasText)})`;
|
||||
if (options.hasNotText !== undefined)
|
||||
return `locator(${this.quote(body as string)}, has_not_text=${this.toHasText(options.hasNotText)})`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `frame_locator(${this.quote(body as string)})`;
|
||||
@ -291,9 +353,9 @@ export class PythonLocatorFactory implements LocatorFactory {
|
||||
const attrString = attrs.length ? `, ${attrs.join(', ')}` : '';
|
||||
return `get_by_role(${this.quote(body as string)}${attrString})`;
|
||||
case 'has-text':
|
||||
return `filter(has_text=${this.toHasText(body as string)})`;
|
||||
return `filter(has_text=${this.toHasText(body)})`;
|
||||
case 'has-not-text':
|
||||
return `filter(has_not_text=${this.toHasText(body as string)})`;
|
||||
return `filter(has_not_text=${this.toHasText(body)})`;
|
||||
case 'has':
|
||||
return `filter(has=${body})`;
|
||||
case 'hasNot':
|
||||
@ -353,6 +415,10 @@ export class JavaLocatorFactory implements LocatorFactory {
|
||||
}
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
if (options.hasText !== undefined)
|
||||
return `locator(${this.quote(body as string)}, new ${clazz}.LocatorOptions().setHasText(${this.toHasText(options.hasText)}))`;
|
||||
if (options.hasNotText !== undefined)
|
||||
return `locator(${this.quote(body as string)}, new ${clazz}.LocatorOptions().setHasNotText(${this.toHasText(options.hasNotText)}))`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `frameLocator(${this.quote(body as string)})`;
|
||||
@ -432,6 +498,10 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
if (options.hasText !== undefined)
|
||||
return `Locator(${this.quote(body as string)}, new() { ${this.toHasText(options.hasText)} })`;
|
||||
if (options.hasNotText !== undefined)
|
||||
return `Locator(${this.quote(body as string)}, new() { ${this.toHasNotText(options.hasNotText)} })`;
|
||||
return `Locator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `FrameLocator(${this.quote(body as string)})`;
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { escapeForAttributeSelector, escapeForTextSelector } from '../../utils/isomorphic/stringUtils';
|
||||
import { asLocator } from './locatorGenerators';
|
||||
import { asLocators } from './locatorGenerators';
|
||||
import type { Language } from './locatorGenerators';
|
||||
import { parseSelector } from './selectorParser';
|
||||
|
||||
@ -78,7 +78,7 @@ function parseLocator(locator: string, testIdAttributeName: string): string {
|
||||
.replace(/[{}\s]/g, '')
|
||||
.replace(/new\(\)/g, '')
|
||||
.replace(/new[\w]+\.[\w]+options\(\)/g, '')
|
||||
.replace(/\.set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase())
|
||||
.replace(/\.set/g, ',set')
|
||||
.replace(/\.or_\(/g, 'or(') // Python has "or_" instead of "or".
|
||||
.replace(/\.and_\(/g, 'and(') // Python has "and_" instead of "and".
|
||||
.replace(/:/g, '=')
|
||||
@ -104,10 +104,10 @@ function shiftParams(template: string, sub: number) {
|
||||
}
|
||||
|
||||
function transform(template: string, params: TemplateParams, testIdAttributeName: string): string {
|
||||
// Recursively handle filter(has=, hasnot=).
|
||||
// TODO: handle and(locator), or(locator).
|
||||
// Recursively handle filter(has=, hasnot=, sethas(), sethasnot()).
|
||||
// TODO: handle and(locator), or(locator), locator(has=, hasnot=, sethas(), sethasnot()).
|
||||
while (true) {
|
||||
const hasMatch = template.match(/filter\(,?(has|hasnot)=/);
|
||||
const hasMatch = template.match(/filter\(,?(has=|hasnot=|sethas\(|sethasnot\()/);
|
||||
if (!hasMatch)
|
||||
break;
|
||||
|
||||
@ -124,6 +124,15 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
||||
break;
|
||||
}
|
||||
|
||||
// Replace Java sethas(...) and sethasnot(...) with has=... and hasnot=...
|
||||
let prefix = template.substring(0, start);
|
||||
let extraSymbol = 0;
|
||||
if (['sethas(', 'sethasnot('].includes(hasMatch[1])) {
|
||||
// Eat extra ) symbol at the end of sethas(...)
|
||||
extraSymbol = 1;
|
||||
prefix = prefix.replace(/sethas\($/, 'has=').replace(/sethasnot\($/, 'hasnot=');
|
||||
}
|
||||
|
||||
const paramsCountBeforeHas = countParams(template.substring(0, start));
|
||||
const hasTemplate = shiftParams(template.substring(start, end), paramsCountBeforeHas);
|
||||
const paramsCountInHas = countParams(hasTemplate);
|
||||
@ -132,7 +141,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
||||
|
||||
// Replace filter(has=...) with filter(has2=$5). Use has2 to avoid matching the same filter again.
|
||||
// Replace filter(hasnot=...) with filter(hasnot2=$5). Use hasnot2 to avoid matching the same filter again.
|
||||
template = template.substring(0, start - 1) + `2=$${paramsCountBeforeHas + 1}` + shiftParams(template.substring(end), paramsCountInHas - 1);
|
||||
template = prefix.replace(/=$/, '2=') + `$${paramsCountBeforeHas + 1}` + shiftParams(template.substring(end + extraSymbol), paramsCountInHas - 1);
|
||||
|
||||
// Replace inner params with $5 value.
|
||||
const paramsBeforeHas = params.slice(0, paramsCountBeforeHas);
|
||||
@ -142,7 +151,11 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
||||
|
||||
// Transform to selector engines.
|
||||
template = template
|
||||
.replace(/\,set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase())
|
||||
.replace(/framelocator\(([^)]+)\)/g, '$1.internal:control=enter-frame')
|
||||
.replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2')
|
||||
.replace(/locator\(([^)]+),hasnottext=([^),]+)\)/g, 'locator($1).internal:has-not-text=$2')
|
||||
.replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2')
|
||||
.replace(/locator\(([^)]+)\)/g, '$1')
|
||||
.replace(/getbyrole\(([^)]+)\)/g, 'internal:role=$1')
|
||||
.replace(/getbytext\(([^)]+)\)/g, 'internal:text=$1')
|
||||
@ -203,7 +216,9 @@ export function locatorOrSelectorAsSelector(language: Language, locator: string,
|
||||
}
|
||||
try {
|
||||
const selector = parseLocator(locator, testIdAttributeName);
|
||||
if (digestForComparison(asLocator(language, selector)) === digestForComparison(locator))
|
||||
const locators = asLocators(language, selector);
|
||||
const digest = digestForComparison(locator);
|
||||
if (locators.some(candidate => digestForComparison(candidate) === digest))
|
||||
return selector;
|
||||
} catch (e) {
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { contextTest as it, expect } from '../config/browserTest';
|
||||
import { asLocator } from '../../packages/playwright-core/lib/utils/isomorphic/locatorGenerators';
|
||||
import { asLocator, asLocators } from '../../packages/playwright-core/lib/utils/isomorphic/locatorGenerators';
|
||||
import { locatorOrSelectorAsSelector as parseLocator } from '../../packages/playwright-core/lib/utils/isomorphic/locatorParser';
|
||||
import type { Page, Frame, Locator, FrameLocator } from 'playwright-core';
|
||||
|
||||
@ -361,6 +361,89 @@ it('reverse engineer frameLocator', async ({ page }) => {
|
||||
expect.soft(asLocator('javascript', selector, false)).toBe(`locator('div').frameLocator('iframe').locator('span')`);
|
||||
});
|
||||
|
||||
it('generate multiple locators', async ({ page }) => {
|
||||
const selector = (page.locator('div', { hasText: 'foo' }).nth(0).filter({ has: page.locator('span', { hasNotText: 'bar' }).nth(-1) }) as any)._selector;
|
||||
const locators = {
|
||||
javascript: [
|
||||
`locator('div').filter({ hasText: 'foo' }).first().filter({ has: locator('span').filter({ hasNotText: 'bar' }).last() })`,
|
||||
`locator('div').filter({ hasText: 'foo' }).first().filter({ has: locator('span').filter({ hasNotText: 'bar' }).nth(-1) })`,
|
||||
`locator('div').filter({ hasText: 'foo' }).first().filter({ has: locator('span', { hasNotText: 'bar' }).last() })`,
|
||||
`locator('div').filter({ hasText: 'foo' }).first().filter({ has: locator('span', { hasNotText: 'bar' }).nth(-1) })`,
|
||||
`locator('div').filter({ hasText: 'foo' }).nth(0).filter({ has: locator('span').filter({ hasNotText: 'bar' }).last() })`,
|
||||
`locator('div').filter({ hasText: 'foo' }).nth(0).filter({ has: locator('span').filter({ hasNotText: 'bar' }).nth(-1) })`,
|
||||
`locator('div').filter({ hasText: 'foo' }).nth(0).filter({ has: locator('span', { hasNotText: 'bar' }).last() })`,
|
||||
`locator('div').filter({ hasText: 'foo' }).nth(0).filter({ has: locator('span', { hasNotText: 'bar' }).nth(-1) })`,
|
||||
`locator('div', { hasText: 'foo' }).first().filter({ has: locator('span').filter({ hasNotText: 'bar' }).last() })`,
|
||||
`locator('div', { hasText: 'foo' }).first().filter({ has: locator('span').filter({ hasNotText: 'bar' }).nth(-1) })`,
|
||||
`locator('div', { hasText: 'foo' }).first().filter({ has: locator('span', { hasNotText: 'bar' }).last() })`,
|
||||
`locator('div', { hasText: 'foo' }).first().filter({ has: locator('span', { hasNotText: 'bar' }).nth(-1) })`,
|
||||
`locator('div', { hasText: 'foo' }).nth(0).filter({ has: locator('span').filter({ hasNotText: 'bar' }).last() })`,
|
||||
`locator('div', { hasText: 'foo' }).nth(0).filter({ has: locator('span').filter({ hasNotText: 'bar' }).nth(-1) })`,
|
||||
`locator('div', { hasText: 'foo' }).nth(0).filter({ has: locator('span', { hasNotText: 'bar' }).last() })`,
|
||||
`locator('div', { hasText: 'foo' }).nth(0).filter({ has: locator('span', { hasNotText: 'bar' }).nth(-1) })`,
|
||||
],
|
||||
java: [
|
||||
`locator("div").filter(new Locator.FilterOptions().setHasText("foo")).first().filter(new Locator.FilterOptions().setHas(locator("span").filter(new Locator.FilterOptions().setHasNotText("bar")).last()))`,
|
||||
`locator("div").filter(new Locator.FilterOptions().setHasText("foo")).first().filter(new Locator.FilterOptions().setHas(locator("span").filter(new Locator.FilterOptions().setHasNotText("bar")).nth(-1)))`,
|
||||
`locator("div").filter(new Locator.FilterOptions().setHasText("foo")).first().filter(new Locator.FilterOptions().setHas(locator("span", new Page.LocatorOptions().setHasNotText("bar")).last()))`,
|
||||
`locator("div").filter(new Locator.FilterOptions().setHasText("foo")).first().filter(new Locator.FilterOptions().setHas(locator("span", new Page.LocatorOptions().setHasNotText("bar")).nth(-1)))`,
|
||||
`locator("div").filter(new Locator.FilterOptions().setHasText("foo")).nth(0).filter(new Locator.FilterOptions().setHas(locator("span").filter(new Locator.FilterOptions().setHasNotText("bar")).last()))`,
|
||||
`locator("div").filter(new Locator.FilterOptions().setHasText("foo")).nth(0).filter(new Locator.FilterOptions().setHas(locator("span").filter(new Locator.FilterOptions().setHasNotText("bar")).nth(-1)))`,
|
||||
`locator("div").filter(new Locator.FilterOptions().setHasText("foo")).nth(0).filter(new Locator.FilterOptions().setHas(locator("span", new Page.LocatorOptions().setHasNotText("bar")).last()))`,
|
||||
`locator("div").filter(new Locator.FilterOptions().setHasText("foo")).nth(0).filter(new Locator.FilterOptions().setHas(locator("span", new Page.LocatorOptions().setHasNotText("bar")).nth(-1)))`,
|
||||
`locator("div", new Page.LocatorOptions().setHasText("foo")).first().filter(new Locator.FilterOptions().setHas(locator("span").filter(new Locator.FilterOptions().setHasNotText("bar")).last()))`,
|
||||
`locator("div", new Page.LocatorOptions().setHasText("foo")).first().filter(new Locator.FilterOptions().setHas(locator("span").filter(new Locator.FilterOptions().setHasNotText("bar")).nth(-1)))`,
|
||||
`locator("div", new Page.LocatorOptions().setHasText("foo")).first().filter(new Locator.FilterOptions().setHas(locator("span", new Page.LocatorOptions().setHasNotText("bar")).last()))`,
|
||||
`locator("div", new Page.LocatorOptions().setHasText("foo")).first().filter(new Locator.FilterOptions().setHas(locator("span", new Page.LocatorOptions().setHasNotText("bar")).nth(-1)))`,
|
||||
`locator("div", new Page.LocatorOptions().setHasText("foo")).nth(0).filter(new Locator.FilterOptions().setHas(locator("span").filter(new Locator.FilterOptions().setHasNotText("bar")).last()))`,
|
||||
`locator("div", new Page.LocatorOptions().setHasText("foo")).nth(0).filter(new Locator.FilterOptions().setHas(locator("span").filter(new Locator.FilterOptions().setHasNotText("bar")).nth(-1)))`,
|
||||
`locator("div", new Page.LocatorOptions().setHasText("foo")).nth(0).filter(new Locator.FilterOptions().setHas(locator("span", new Page.LocatorOptions().setHasNotText("bar")).last()))`,
|
||||
`locator("div", new Page.LocatorOptions().setHasText("foo")).nth(0).filter(new Locator.FilterOptions().setHas(locator("span", new Page.LocatorOptions().setHasNotText("bar")).nth(-1)))`,
|
||||
],
|
||||
python: [
|
||||
`locator("div").filter(has_text="foo").first.filter(has=locator("span").filter(has_not_text="bar").last)`,
|
||||
`locator("div").filter(has_text="foo").first.filter(has=locator("span").filter(has_not_text="bar").nth(-1))`,
|
||||
`locator("div").filter(has_text="foo").first.filter(has=locator("span", has_not_text="bar").last)`,
|
||||
`locator("div").filter(has_text="foo").first.filter(has=locator("span", has_not_text="bar").nth(-1))`,
|
||||
`locator("div").filter(has_text="foo").nth(0).filter(has=locator("span").filter(has_not_text="bar").last)`,
|
||||
`locator("div").filter(has_text="foo").nth(0).filter(has=locator("span").filter(has_not_text="bar").nth(-1))`,
|
||||
`locator("div").filter(has_text="foo").nth(0).filter(has=locator("span", has_not_text="bar").last)`,
|
||||
`locator("div").filter(has_text="foo").nth(0).filter(has=locator("span", has_not_text="bar").nth(-1))`,
|
||||
`locator("div", has_text="foo").first.filter(has=locator("span").filter(has_not_text="bar").last)`,
|
||||
`locator("div", has_text="foo").first.filter(has=locator("span").filter(has_not_text="bar").nth(-1))`,
|
||||
`locator("div", has_text="foo").first.filter(has=locator("span", has_not_text="bar").last)`,
|
||||
`locator("div", has_text="foo").first.filter(has=locator("span", has_not_text="bar").nth(-1))`,
|
||||
`locator("div", has_text="foo").nth(0).filter(has=locator("span").filter(has_not_text="bar").last)`,
|
||||
`locator("div", has_text="foo").nth(0).filter(has=locator("span").filter(has_not_text="bar").nth(-1))`,
|
||||
`locator("div", has_text="foo").nth(0).filter(has=locator("span", has_not_text="bar").last)`,
|
||||
`locator("div", has_text="foo").nth(0).filter(has=locator("span", has_not_text="bar").nth(-1))`,
|
||||
],
|
||||
csharp: [
|
||||
`Locator("div").Filter(new() { HasText = "foo" }).First.Filter(new() { Has = Locator("span").Filter(new() { HasNotText = "bar" }).Last })`,
|
||||
`Locator("div").Filter(new() { HasText = "foo" }).First.Filter(new() { Has = Locator("span").Filter(new() { HasNotText = "bar" }).Nth(-1) })`,
|
||||
`Locator("div").Filter(new() { HasText = "foo" }).First.Filter(new() { Has = Locator("span", new() { HasNotText = "bar" }).Last })`,
|
||||
`Locator("div").Filter(new() { HasText = "foo" }).First.Filter(new() { Has = Locator("span", new() { HasNotText = "bar" }).Nth(-1) })`,
|
||||
`Locator("div").Filter(new() { HasText = "foo" }).Nth(0).Filter(new() { Has = Locator("span").Filter(new() { HasNotText = "bar" }).Last })`,
|
||||
`Locator("div").Filter(new() { HasText = "foo" }).Nth(0).Filter(new() { Has = Locator("span").Filter(new() { HasNotText = "bar" }).Nth(-1) })`,
|
||||
`Locator("div").Filter(new() { HasText = "foo" }).Nth(0).Filter(new() { Has = Locator("span", new() { HasNotText = "bar" }).Last })`,
|
||||
`Locator("div").Filter(new() { HasText = "foo" }).Nth(0).Filter(new() { Has = Locator("span", new() { HasNotText = "bar" }).Nth(-1) })`,
|
||||
`Locator("div", new() { HasText = "foo" }).First.Filter(new() { Has = Locator("span").Filter(new() { HasNotText = "bar" }).Last })`,
|
||||
`Locator("div", new() { HasText = "foo" }).First.Filter(new() { Has = Locator("span").Filter(new() { HasNotText = "bar" }).Nth(-1) })`,
|
||||
`Locator("div", new() { HasText = "foo" }).First.Filter(new() { Has = Locator("span", new() { HasNotText = "bar" }).Last })`,
|
||||
`Locator("div", new() { HasText = "foo" }).First.Filter(new() { Has = Locator("span", new() { HasNotText = "bar" }).Nth(-1) })`,
|
||||
`Locator("div", new() { HasText = "foo" }).Nth(0).Filter(new() { Has = Locator("span").Filter(new() { HasNotText = "bar" }).Last })`,
|
||||
`Locator("div", new() { HasText = "foo" }).Nth(0).Filter(new() { Has = Locator("span").Filter(new() { HasNotText = "bar" }).Nth(-1) })`,
|
||||
`Locator("div", new() { HasText = "foo" }).Nth(0).Filter(new() { Has = Locator("span", new() { HasNotText = "bar" }).Last })`,
|
||||
`Locator("div", new() { HasText = "foo" }).Nth(0).Filter(new() { Has = Locator("span", new() { HasNotText = "bar" }).Nth(-1) })`,
|
||||
],
|
||||
};
|
||||
for (const lang of ['javascript', 'java', 'python', 'csharp'] as const) {
|
||||
expect.soft(asLocators(lang, selector, false)).toEqual(locators[lang]);
|
||||
for (const locator of locators[lang])
|
||||
expect.soft(parseLocator(lang, locator, 'data-testid'), `parse(${lang}): ${locator}`).toBe(selector);
|
||||
}
|
||||
});
|
||||
|
||||
it.describe(() => {
|
||||
it.beforeEach(async ({ context }) => {
|
||||
await (context as any)._enableRecorder({ language: 'javascript' });
|
||||
|
Loading…
x
Reference in New Issue
Block a user