Skip to content

Commit

Permalink
fix(locator generator): handle frameLocator() and `locator().conten…
Browse files Browse the repository at this point in the history
…tFrame()` (#33208)
  • Loading branch information
Skn0tt authored Oct 24, 2024
1 parent 6ae6b48 commit 69f56b9
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 7 deletions.
32 changes: 25 additions & 7 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 @@ -158,19 +158,29 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
}
}
if (part.name === 'internal:control' && (part.body as string) === 'enter-frame') {
tokens.push([factory.generateLocator(base, 'frame', '')]);
// transform last tokens from `${selector}` into `${selector}.contentFrame()` and `frameLocator(${selector})`
const lastTokens = tokens[tokens.length - 1];
const lastPart = parts[index - 1];

const transformed = lastTokens.map(token => factory.chainLocators([token, factory.generateLocator(base, 'frame', '')]));
if (['xpath', 'css'].includes(lastPart.name)) {
transformed.push(
factory.generateLocator(base, 'frame-locator', stringifySelector({ parts: [lastPart] })),
factory.generateLocator(base, 'frame-locator', stringifySelector({ parts: [lastPart] }, true))
);
}

lastTokens.splice(0, lastTokens.length, ...transformed);
nextBase = 'frame-locator';
continue;
}

const 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 +204,7 @@ 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);
}

tokens.push([locatorPart, locatorPartWithEngine].filter(Boolean) as string[]);
Expand Down Expand Up @@ -253,6 +263,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 +357,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 +464,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 +561,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
19 changes: 19 additions & 0 deletions tests/library/locator-generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,22 @@ 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('javascript', `getByTitle('iframe title').contentFrame()`)).toBe(`internal:attr=[title=\"iframe title\"i] >> internal:control=enter-frame`);

expect.soft(asLocators('javascript', 'internal:attr=[title=\"iframe title\"i] >> internal:control=enter-frame')).toEqual([`getByTitle('iframe title').contentFrame()`]);

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`);
});

0 comments on commit 69f56b9

Please sign in to comment.