Skip to content

Commit

Permalink
chore: use contentFrame() as a canonical locator representation (#32697)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Sep 19, 2024
1 parent 790dbfd commit 2f4acbb
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 81 deletions.
46 changes: 24 additions & 22 deletions docs/src/api/class-framelocator.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
# class: FrameLocator
* since: v1.17

FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe` and locate elements in that iframe. FrameLocator can be created with either [`method: Page.frameLocator`] or [`method: Locator.frameLocator`] method.
FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe` and locate elements in that iframe. FrameLocator can be created with either [`method: Locator.contentFrame`], [`method: Page.frameLocator`] or [`method: Locator.frameLocator`] method.

```js
const locator = page.frameLocator('#my-frame').getByText('Submit');
const locator = page.locator('#my-frame').contentFrame().getByText('Submit');
await locator.click();
```

```java
Locator locator = page.frameLocator("#my-frame").getByText("Submit");
Locator locator = page.locator("#my-frame").contentFrame().getByText("Submit");
locator.click();
```

```python async
locator = page.frame_locator("#my-frame").get_by_text("Submit")
locator = page.locator("#my-frame").content_frame.get_by_text("Submit")
await locator.click()
```

```python sync
locator = page.frame_locator("my-frame").get_by_text("Submit")
locator = page.locator("my-frame").content_frame.get_by_text("Submit")
locator.click()
```

```csharp
var locator = page.FrameLocator("#my-frame").GetByText("Submit");
var locator = page.Locator("#my-frame").ContentFrame.GetByText("Submit");
await locator.ClickAsync();
```

Expand All @@ -34,42 +34,42 @@ Frame locators are strict. This means that all operations on frame locators will

```js
// Throws if there are several frames in DOM:
await page.frameLocator('.result-frame').getByRole('button').click();
await page.locator('.result-frame').contentFrame().getByRole('button').click();

// Works because we explicitly tell locator to pick the first frame:
await page.frameLocator('.result-frame').first().getByRole('button').click();
await page.locator('.result-frame').contentFrame().first().getByRole('button').click();
```

```python async
# Throws if there are several frames in DOM:
await page.frame_locator('.result-frame').get_by_role('button').click()
await page.locator('.result-frame').content_frame.get_by_role('button').click()

# Works because we explicitly tell locator to pick the first frame:
await page.frame_locator('.result-frame').first.get_by_role('button').click()
await page.locator('.result-frame').first.content_frame.get_by_role('button').click()
```

```python sync
# Throws if there are several frames in DOM:
page.frame_locator('.result-frame').get_by_role('button').click()
page.locator('.result-frame').content_frame.get_by_role('button').click()

# Works because we explicitly tell locator to pick the first frame:
page.frame_locator('.result-frame').first.get_by_role('button').click()
page.locator('.result-frame').first.content_frame.get_by_role('button').click()
```

```java
// Throws if there are several frames in DOM:
page.frame_locator(".result-frame").getByRole(AriaRole.BUTTON).click();
page.locator(".result-frame").contentFrame().getByRole(AriaRole.BUTTON).click();

// Works because we explicitly tell locator to pick the first frame:
page.frame_locator(".result-frame").first().getByRole(AriaRole.BUTTON).click();
page.locator(".result-frame").first().contentFrame().getByRole(AriaRole.BUTTON).click();
```

```csharp
// Throws if there are several frames in DOM:
await page.FrameLocator(".result-frame").GetByRole(AriaRole.Button).ClickAsync();
await page.Locator(".result-frame").ContentFrame.GetByRole(AriaRole.Button).ClickAsync();

// Works because we explicitly tell locator to pick the first frame:
await page.FrameLocator(".result-frame").First.getByRole(AriaRole.Button).ClickAsync();
await page.Locator(".result-frame").First.ContentFrame.getByRole(AriaRole.Button).ClickAsync();
```

**Converting Locator to FrameLocator**
Expand All @@ -82,6 +82,7 @@ If you have a [FrameLocator] object it can be converted to [Locator] pointing to


## method: FrameLocator.first
* deprecated: Use [`method: Locator.first`] followed by [`method: Locator.contentFrame`] instead.
* since: v1.17
- returns: <[FrameLocator]>

Expand Down Expand Up @@ -171,6 +172,7 @@ in that iframe.
### option: FrameLocator.getByTitle.exact = %%-locator-get-by-text-exact-%%

## method: FrameLocator.last
* deprecated: Use [`method: Locator.last`] followed by [`method: Locator.contentFrame`] instead.
* since: v1.17
- returns: <[FrameLocator]>

Expand All @@ -195,6 +197,7 @@ Returns locator to the last matching frame.
* since: v1.33

## method: FrameLocator.nth
* deprecated: Use [`method: Locator.nth`] followed by [`method: Locator.contentFrame`] instead.
* since: v1.17
- returns: <[FrameLocator]>

Expand All @@ -217,37 +220,36 @@ For a reverse operation, use [`method: Locator.contentFrame`].
**Usage**

```js
const frameLocator = page.frameLocator('iframe[name="embedded"]');
const frameLocator = page.locator('iframe[name="embedded"]').contentFrame();
// ...
const locator = frameLocator.owner();
await expect(locator).toBeVisible();
```

```java
FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]");
FrameLocator frameLocator = page.locator("iframe[name=\"embedded\"]").contentFrame();
// ...
Locator locator = frameLocator.owner();
assertThat(locator).isVisible();
```

```python async
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
frame_locator = page.locator("iframe[name=\"embedded\"]").content_frame
# ...
locator = frame_locator.owner
await expect(locator).to_be_visible()
```

```python sync
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
frame_locator = page.locator("iframe[name=\"embedded\"]").content_frame
# ...
locator = frame_locator.owner
expect(locator).to_be_visible()
```

```csharp
var frameLocator = Page.FrameLocator("iframe[name=\"embedded\"]");
var frameLocator = Page.Locator("iframe[name=\"embedded\"]").ContentFrame;
// ...
var locator = frameLocator.Owner;
await Expect(locator).ToBeVisibleAsync();
```

28 changes: 9 additions & 19 deletions packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,6 @@ export function asLocators(lang: Language, selector: string, isFrameLocator: boo

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"
for (let index = 0; index < parts.length - 1; index++) {
if (parts[index].name === 'nth' && parts[index + 1].name === 'internal:control' && (parts[index + 1].body as string) === 'enter-frame') {
// Swap nth and enter-frame.
const [nth] = parts.splice(index, 1);
parts.splice(index + 1, 0, nth);
}
}

const tokens: string[][] = [];
let nextBase: LocatorBase = isFrameLocator ? 'frame-locator' : 'page';
for (let index = 0; index < parts.length; index++) {
Expand Down Expand Up @@ -167,15 +157,15 @@ 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];
if (nextPart && nextPart.name === 'internal:control' && (nextPart.body as string) === 'enter-frame') {
locatorType = 'frame';
nextBase = 'frame-locator';
index++;
}

const selectorPart = stringifySelector({ parts: [part] });
const locatorPart = factory.generateLocator(base, locatorType, selectorPart);
Expand Down Expand Up @@ -264,7 +254,7 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
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)})`;
return `contentFrame()`;
case 'nth':
return `nth(${body})`;
case 'first':
Expand Down Expand Up @@ -356,7 +346,7 @@ export class PythonLocatorFactory implements LocatorFactory {
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)})`;
return `content_frame`;
case 'nth':
return `nth(${body})`;
case 'first':
Expand Down Expand Up @@ -461,7 +451,7 @@ export class JavaLocatorFactory implements LocatorFactory {
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)})`;
return `contentFrame()`;
case 'nth':
return `nth(${body})`;
case 'first':
Expand Down Expand Up @@ -556,7 +546,7 @@ export class CSharpLocatorFactory implements LocatorFactory {
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)})`;
return `ContentFrame`;
case 'nth':
return `Nth(${body})`;
case 'first':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function parseLocator(locator: string, testIdAttributeName: string): { selector:
.replace(/has_text/g, 'hastext')
.replace(/has_not/g, 'hasnot')
.replace(/frame_locator/g, 'framelocator')
.replace(/content_frame/g, 'contentframe')
.replace(/[{}\s]/g, '')
.replace(/new\(\)/g, '')
.replace(/new[\w]+\.[\w]+options\(\)/g, '')
Expand Down Expand Up @@ -154,6 +155,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
template = template
.replace(/\,set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase())
.replace(/framelocator\(([^)]+)\)/g, '$1.internal:control=enter-frame')
.replace(/contentframe(\(\))?/g, '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')
Expand Down
15 changes: 11 additions & 4 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18240,11 +18240,12 @@ export interface FileChooser {
/**
* FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the
* `iframe` and locate elements in that iframe. FrameLocator can be created with either
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame),
* [page.frameLocator(selector)](https://playwright.dev/docs/api/class-page#page-frame-locator) or
* [locator.frameLocator(selector)](https://playwright.dev/docs/api/class-locator#locator-frame-locator) method.
*
* ```js
* const locator = page.frameLocator('#my-frame').getByText('Submit');
* const locator = page.locator('#my-frame').contentFrame().getByText('Submit');
* await locator.click();
* ```
*
Expand All @@ -18255,10 +18256,10 @@ export interface FileChooser {
*
* ```js
* // Throws if there are several frames in DOM:
* await page.frameLocator('.result-frame').getByRole('button').click();
* await page.locator('.result-frame').contentFrame().getByRole('button').click();
*
* // Works because we explicitly tell locator to pick the first frame:
* await page.frameLocator('.result-frame').first().getByRole('button').click();
* await page.locator('.result-frame').contentFrame().first().getByRole('button').click();
* ```
*
* **Converting Locator to FrameLocator**
Expand All @@ -18274,6 +18275,8 @@ export interface FileChooser {
export interface FrameLocator {
/**
* Returns locator to the first matching frame.
* @deprecated Use [locator.first()](https://playwright.dev/docs/api/class-locator#locator-first) followed by
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame) instead.
*/
first(): FrameLocator;

Expand Down Expand Up @@ -18598,6 +18601,8 @@ export interface FrameLocator {

/**
* Returns locator to the last matching frame.
* @deprecated Use [locator.last()](https://playwright.dev/docs/api/class-locator#locator-last) followed by
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame) instead.
*/
last(): FrameLocator;

Expand Down Expand Up @@ -18650,6 +18655,8 @@ export interface FrameLocator {

/**
* Returns locator to the n-th matching frame. It's zero based, `nth(0)` selects the first frame.
* @deprecated Use [locator.nth(index)](https://playwright.dev/docs/api/class-locator#locator-nth) followed by
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame) instead.
* @param index
*/
nth(index: number): FrameLocator;
Expand All @@ -18666,7 +18673,7 @@ export interface FrameLocator {
* **Usage**
*
* ```js
* const frameLocator = page.frameLocator('iframe[name="embedded"]');
* const frameLocator = page.locator('iframe[name="embedded"]').contentFrame();
* // ...
* const locator = frameLocator.owner();
* await expect(locator).toBeVisible();
Expand Down
Loading

0 comments on commit 2f4acbb

Please sign in to comment.