From 70e6cdac5753b0d5e89864fcdc65d130d39c06f9 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 18 Mar 2024 13:42:08 -0700 Subject: [PATCH] feat: enterFrame/exitFrame (#29992) This introduces `Locator.enterFrame()` and `FrameLocator.exitFrame()` to convert between locator and frame locator. Fixes #29336. --- docs/src/api/class-framelocator.md | 43 +++++++++++++++--- docs/src/api/class-locator.md | 45 +++++++++++++++++++ .../playwright-core/src/client/locator.ts | 8 ++++ packages/playwright-core/types/types.d.ts | 44 ++++++++++++++++-- tests/page/locator-frame.spec.ts | 22 ++++++++- 5 files changed, 151 insertions(+), 11 deletions(-) diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md index c90ff1fa19..cf75fc8422 100644 --- a/docs/src/api/class-framelocator.md +++ b/docs/src/api/class-framelocator.md @@ -74,28 +74,59 @@ await page.FrameLocator(".result-frame").First.getByRole(AriaRole.Button).ClickA **Converting Locator to FrameLocator** -If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`:scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/:scope) CSS selector: +If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`method: Locator.enterFrame`]. + +**Converting FrameLocator to Locator** + +If you have a [FrameLocator] object it can be converted to [Locator] pointing to the same `iframe` using [`method: FrameLocator.exitFrame`]. + + +## method: FrameLocator.exitFrame +* since: v1.43 +- returns: <[Locator]> + +Returns a [Locator] object pointing to the same `iframe` as this frame locator. + +Useful when you have a [FrameLocator] object obtained somewhere, and later on would like to interact with the `iframe` element. + +**Usage** ```js -const frameLocator = locator.frameLocator(':scope'); +const frameLocator = page.frameLocator('iframe[name="embedded"]'); +// ... +const locator = frameLocator.exitFrame(); +await expect(locator).toBeVisible(); ``` ```java -Locator frameLocator = locator.frameLocator(':scope'); +FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]"); +// ... +Locator locator = frameLocator.exitFrame(); +assertThat(locator).isVisible(); ``` ```python async -frameLocator = locator.frame_locator(":scope") +frame_locator = page.frame_locator("iframe[name=\"embedded\"]") +# ... +locator = frame_locator.exit_frame +await expect(locator).to_be_visible() ``` ```python sync -frameLocator = locator.frame_locator(":scope") +frame_locator = page.frame_locator("iframe[name=\"embedded\"]") +# ... +locator = frame_locator.exit_frame +expect(locator).to_be_visible() ``` ```csharp -var frameLocator = locator.FrameLocator(":scope"); +var frameLocator = Page.FrameLocator("iframe[name=\"embedded\"]"); +// ... +var locator = frameLocator.ExitFrame; +await Expect(locator).ToBeVisibleAsync(); ``` + ## method: FrameLocator.first * since: v1.17 - returns: <[FrameLocator]> diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index bea8507def..c254adc1c0 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -747,6 +747,51 @@ Resolves given locator to the first matching DOM element. If there are no matchi Resolves given locator to all matching DOM elements. If there are no matching elements, returns an empty list. +## method: Locator.enterFrame +* since: v1.43 +- returns: <[FrameLocator]> + +Returns a [FrameLocator] object pointing to the same `iframe` as this locator. + +Useful when you have a [Locator] object obtained somewhere, and later on would like to interact with the content inside the frame. + +**Usage** + +```js +const locator = page.locator('iframe[name="embedded"]'); +// ... +const frameLocator = locator.enterFrame(); +await frameLocator.getByRole('button').click(); +``` + +```java +Locator locator = page.locator("iframe[name=\"embedded\"]"); +// ... +FrameLocator frameLocator = locator.enterFrame(); +frameLocator.getByRole(AriaRole.BUTTON).click(); +``` + +```python async +locator = page.locator("iframe[name=\"embedded\"]") +# ... +frame_locator = locator.enter_frame +await frame_locator.get_by_role("button").click() +``` + +```python sync +locator = page.locator("iframe[name=\"embedded\"]") +# ... +frame_locator = locator.enter_frame +frame_locator.get_by_role("button").click() +``` + +```csharp +var locator = Page.Locator("iframe[name=\"embedded\"]"); +// ... +var frameLocator = locator.EnterFrame; +await frameLocator.GetByRole(AriaRole.Button).ClickAsync(); +``` + ## async method: Locator.evaluate * since: v1.14 - returns: <[Serializable]> diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 36ded11194..a24286e71e 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -192,6 +192,10 @@ export class Locator implements api.Locator { return await this._frame.$$(this._selector); } + enterFrame() { + return new FrameLocator(this._frame, this._selector); + } + first(): Locator { return new Locator(this._frame, this._selector + ' >> nth=0'); } @@ -404,6 +408,10 @@ export class FrameLocator implements api.FrameLocator { return this.locator(getByRoleSelector(role, options)); } + exitFrame() { + return new Locator(this._frame, this._frameSelector); + } + frameLocator(selector: string): FrameLocator { return new FrameLocator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d2dbbd0543..28867d932c 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -11460,6 +11460,24 @@ export interface Locator { */ elementHandles(): Promise>; + /** + * Returns a {@link FrameLocator} object pointing to the same `iframe` as this locator. + * + * Useful when you have a {@link Locator} object obtained somewhere, and later on would like to interact with the + * content inside the frame. + * + * **Usage** + * + * ```js + * const locator = page.locator('iframe[name="embedded"]'); + * // ... + * const frameLocator = locator.enterFrame(); + * await frameLocator.getByRole('button').click(); + * ``` + * + */ + enterFrame(): FrameLocator; + /** * Set a value to the input field. * @@ -17761,14 +17779,32 @@ export interface FileChooser { * **Converting Locator to FrameLocator** * * If you have a {@link Locator} object pointing to an `iframe` it can be converted to {@link FrameLocator} using - * [`:scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/:scope) CSS selector: + * [locator.enterFrame()](https://playwright.dev/docs/api/class-locator#locator-enter-frame). * - * ```js - * const frameLocator = locator.frameLocator(':scope'); - * ``` + * **Converting FrameLocator to Locator** * + * If you have a {@link FrameLocator} object it can be converted to {@link Locator} pointing to the same `iframe` + * using [frameLocator.exitFrame()](https://playwright.dev/docs/api/class-framelocator#frame-locator-exit-frame). */ export interface FrameLocator { + /** + * Returns a {@link Locator} object pointing to the same `iframe` as this frame locator. + * + * Useful when you have a {@link FrameLocator} object obtained somewhere, and later on would like to interact with the + * `iframe` element. + * + * **Usage** + * + * ```js + * const frameLocator = page.frameLocator('iframe[name="embedded"]'); + * // ... + * const locator = frameLocator.exitFrame(); + * await expect(locator).toBeVisible(); + * ``` + * + */ + exitFrame(): Locator; + /** * Returns locator to the first matching frame. */ diff --git a/tests/page/locator-frame.spec.ts b/tests/page/locator-frame.spec.ts index 08da3dff0a..4b79039ff9 100644 --- a/tests/page/locator-frame.spec.ts +++ b/tests/page/locator-frame.spec.ts @@ -21,7 +21,7 @@ import { test as it, expect } from './pageTest'; async function routeIframe(page: Page) { await page.route('**/empty.html', route => { route.fulfill({ - body: '', + body: '', contentType: 'text/html' }).catch(() => {}); }); @@ -298,3 +298,23 @@ it('should work with COEP/COOP/CORP isolated iframe', async ({ page, server, bro await page.frameLocator('iframe').getByRole('button').click(); expect(await page.frames()[1].evaluate(() => window['__clicked'])).toBe(true); }); + +it('locator.enterFrame should work', async ({ page, server }) => { + await routeIframe(page); + await page.goto(server.EMPTY_PAGE); + const locator = page.locator('iframe'); + const frameLocator = locator.enterFrame(); + const button = frameLocator.locator('button'); + expect(await button.innerText()).toBe('Hello iframe'); + await expect(button).toHaveText('Hello iframe'); + await button.click(); +}); + +it('frameLocator.exitFrame should work', async ({ page, server }) => { + await routeIframe(page); + await page.goto(server.EMPTY_PAGE); + const frameLocator = page.frameLocator('iframe'); + const locator = frameLocator.exitFrame(); + await expect(locator).toBeVisible(); + expect(await locator.getAttribute('name')).toBe('frame1'); +});