Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(locator generator): handle frameLocator() and locator().contentFrame() #33208

Merged
merged 14 commits into from
Oct 24, 2024
49 changes: 38 additions & 11 deletions packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringi
import type { ParsedSelector } from './selectorParser';

export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
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' | 'chain';
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'frame-locator' | 'and' | 'or' | 'chain';
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
export type Quote = '\'' | '"' | '`';

Expand Down Expand Up @@ -157,20 +157,13 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
continue;
}
}
if (part.name === 'internal:control' && (part.body as string) === 'enter-frame') {
tokens.push([factory.generateLocator(base, 'frame', '')]);
nextBase = 'frame-locator';
continue;
}

let locatorType: LocatorType = 'default';

const nextPart = parts[index + 1];

const selectorPart = stringifySelector({ parts: [part] });
const locatorPart = factory.generateLocator(base, locatorType, selectorPart);
const locatorPart = factory.generateLocator(base, 'default', selectorPart);

if (locatorType === 'default' && nextPart && ['internal:has-text', 'internal:has-not-text'].includes(nextPart.name)) {
if (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) {
Expand All @@ -194,7 +187,33 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
let locatorPartWithEngine: string | undefined;
if (['xpath', 'css'].includes(part.name)) {
const selectorPart = stringifySelector({ parts: [part] }, /* forceEngineName */ true);
locatorPartWithEngine = factory.generateLocator(base, locatorType, selectorPart);
locatorPartWithEngine = factory.generateLocator(base, 'default', selectorPart);
}

if (nextPart && nextPart.name === 'internal:control' && (nextPart.body as string) === 'enter-frame') {
Skn0tt marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking again, I see that we might be missing some cases. For example, page.getByTitle('iframe title').contentFrame() - does it correctly handle this case?

Copy link
Member Author

@Skn0tt Skn0tt Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, it does not. I've revised the approach in e1e9b66 to go back one produced token and update that in retrospect. This makes the change much smaller as well, really liking it. Makes the code a little more hairy though because it mutates across iterations.

// two options plus engine name:
// - locator('iframe').contentFrame()
// - locator('css|xpath=iframe').contentFrame()
// - frameLocator('iframe')
// - frameLocator('css|xpath=iframe')

const contentFrame = factory.generateLocator(base, 'frame', '')
const options = [
factory.chainLocators([locatorPart, contentFrame]),
factory.generateLocator(base, 'frame-locator', selectorPart),
]

if (locatorPartWithEngine) {
options.push(
factory.chainLocators([locatorPartWithEngine, contentFrame]),
factory.generateLocator(base, 'frame-locator', stringifySelector({ parts: [part] }, /* forceEngineName */ true)),
)
}

tokens.push(options);
nextBase = 'frame-locator';
index++;
continue;
}

tokens.push([locatorPart, locatorPartWithEngine].filter(Boolean) as string[]);
Expand Down Expand Up @@ -253,6 +272,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
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-locator':
return `frameLocator(${this.quote(body as string)})`;
case 'frame':
return `contentFrame()`;
case 'nth':
Expand Down Expand Up @@ -345,6 +366,8 @@ export class PythonLocatorFactory implements LocatorFactory {
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-locator':
return `frame_locator(${this.quote(body as string)})`;
case 'frame':
return `content_frame`;
case 'nth':
Expand Down Expand Up @@ -450,6 +473,8 @@ export class JavaLocatorFactory implements LocatorFactory {
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-locator':
return `frameLocator(${this.quote(body as string)})`;
case 'frame':
return `contentFrame()`;
case 'nth':
Expand Down Expand Up @@ -545,6 +570,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
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-locator':
return `FrameLocator(${this.quote(body as string)})`;
case 'frame':
return `ContentFrame`;
case 'nth':
Expand Down
16 changes: 16 additions & 0 deletions tests/library/locator-generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,19 @@ it('parse locators strictly', () => {
expect.soft(parseLocator('javascript', `locator('div').filter({ hasText: 'Goodbye world' }}).locator('span')`)).not.toBe(selector);
expect.soft(parseLocator('python', `locator("div").filter(has_text=="Goodbye world").locator("span")`)).not.toBe(selector);
});

it('parseLocator frames', async () => {
expect.soft(parseLocator('javascript', `locator('iframe').contentFrame().getByText('foo')`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('javascript', `frameLocator('iframe').getByText('foo')`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('javascript', `frameLocator('css=iframe').getByText('foo')`, '')).toBe(`css=iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);

expect.soft(parseLocator('python', `locator("iframe").content_frame.get_by_text("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('python', `frame_locator("iframe").get_by_text("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('python', `frame_locator("css=iframe").get_by_text("foo")`, '')).toBe(`css=iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);

expect.soft(parseLocator('csharp', `Locator("iframe").ContentFrame.GetByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('csharp', `FrameLocator("iframe").GetByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);

expect.soft(parseLocator('java', `locator("iframe").contentFrame().getByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('java', `frameLocator("iframe").getByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
});
Loading