diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md index e27cc4129b..c4d73d7335 100644 --- a/docs/src/api/class-framelocator.md +++ b/docs/src/api/class-framelocator.md @@ -27,6 +27,57 @@ var locator = page.FrameLocator("#my-frame").Locator("text=Submit"); await locator.ClickAsync(); ``` +**Strictness** + +Frame locators are strict. This means that all operations on frame locators will throw if more than one element matches given selector. + +```js +// Throws if there are several frames in DOM: +await page.frameLocator('.result-frame').locator('button').click(); + +// Works because we explicitly tell locator to pick the first frame: +await page.frameLocator('.result-frame').first().locator('button').click(); +``` + +```python async +# Throws if there are several frames in DOM: +await page.frame_locator('.result-frame').locator('button')..click() + +# Works because we explicitly tell locator to pick the first frame: +await page.frame_locator('.result-frame').first.locator('button')..click() +``` + +```python sync +# Throws if there are several frames in DOM: +page.frame_locator('.result-frame').locator('button').click() + +# Works because we explicitly tell locator to pick the first frame: +page.frame_locator('.result-frame').first.locator('button').click() +``` + +```java +// Throws if there are several frames in DOM: +page.frame_locator(".result-frame").locator("button").click(); + +// Works because we explicitly tell locator to pick the first frame: +page.frame_locator(".result-frame").first().locator("button").click(); +``` + +```csharp +// Throws if there are several frames in DOM: +await page.FrameLocator(".result-frame").Locator("button").ClickAsync(); + +// Works because we explicitly tell locator to pick the first frame: +await page.FrameLocator(".result-frame").First.Locator("button").ClickAsync(); +``` + + +## method: FrameLocator.first +- returns: <[FrameLocator]> + +Returns locator to the first matching frame. + + ## method: FrameLocator.frameLocator - returns: <[FrameLocator]> @@ -36,9 +87,24 @@ in that iframe. ### param: FrameLocator.frameLocator.selector = %%-find-selector-%% +## method: FrameLocator.last +- returns: <[FrameLocator]> + +Returns locator to the last matching frame. + + ## method: FrameLocator.locator - returns: <[Locator]> The method finds an element matching the specified selector in the FrameLocator's subtree. ### param: FrameLocator.locator.selector = %%-find-selector-%% + + +## method: FrameLocator.nth +- returns: <[FrameLocator]> + +Returns locator to the n-th matching frame. + +### param: FrameLocator.nth.index +- `index` <[int]> diff --git a/docs/src/library-js.md b/docs/src/library-js.md index 0da6adabb1..fbb5e59639 100644 --- a/docs/src/library-js.md +++ b/docs/src/library-js.md @@ -1,6 +1,6 @@ --- id: library -title: "Overview" +title: "Library" --- Playwright can either be used as a part of the [Playwright Test](./intro.md), or as a Playwright Library (this guide). If you are working on an application that utilizes Playwright capabilities or you are using Playwright with another test runner, read on. diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index a34df56e15..706d4381a8 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -252,18 +252,30 @@ export class Locator implements api.Locator { export class FrameLocator implements api.FrameLocator { private _frame: Frame; - private _selector: string; + private _frameSelector: string; constructor(frame: Frame, selector: string) { this._frame = frame; - this._selector = selector + ' >> control=enter-frame'; + this._frameSelector = selector; } locator(selector: string): Locator { - return new Locator(this._frame, this._selector + ' >> ' + selector); + return new Locator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector); } frameLocator(selector: string): FrameLocator { - return new FrameLocator(this._frame, this._selector + ' >> ' + selector); + return new FrameLocator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector); + } + + first(): FrameLocator { + return new FrameLocator(this._frame, this._frameSelector + ' >> nth=0'); + } + + last(): FrameLocator { + return new FrameLocator(this._frame, this._frameSelector + ` >> nth=-1`); + } + + nth(index: number): FrameLocator { + return new FrameLocator(this._frame, this._frameSelector + ` >> nth=${index}`); } } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 83e1ac80b0..6a4db4a05a 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -13481,8 +13481,26 @@ export interface FileChooser { * await locator.click(); * ``` * + * **Strictness** + * + * Frame locators are strict. This means that all operations on frame locators will throw if more than one element matches + * given selector. + * + * ```js + * // Throws if there are several frames in DOM: + * await page.frameLocator('.result-frame').locator('button').click(); + * + * // Works because we explicitly tell locator to pick the first frame: + * await page.frameLocator('.result-frame').first().locator('button').click(); + * ``` + * */ export interface FrameLocator { + /** + * Returns locator to the first matching frame. + */ + first(): FrameLocator; + /** * When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in * that iframe. @@ -13490,11 +13508,22 @@ export interface FrameLocator { */ frameLocator(selector: string): FrameLocator; + /** + * Returns locator to the last matching frame. + */ + last(): FrameLocator; + /** * The method finds an element matching the specified selector in the FrameLocator's subtree. * @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details. */ locator(selector: string): Locator; + + /** + * Returns locator to the n-th matching frame. + * @param index + */ + nth(index: number): FrameLocator; } /** diff --git a/tests/page/locator-frame.spec.ts b/tests/page/locator-frame.spec.ts index b4a207c9f0..36e95b0912 100644 --- a/tests/page/locator-frame.spec.ts +++ b/tests/page/locator-frame.spec.ts @@ -47,6 +47,24 @@ async function routeIframe(page: Page) { }); } +async function routeAmbiguous(page: Page) { + await page.route('**/empty.html', route => { + route.fulfill({ + body: ` + + `, + contentType: 'text/html' + }).catch(() => {}); + }); + await page.route('**/iframe-*', route => { + const path = new URL(route.request().url()).pathname.slice(1); + route.fulfill({ + body: ``, + contentType: 'text/html' + }).catch(() => {}); + }); +} + it('should work for iframe', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); @@ -198,3 +216,22 @@ it('locator.frameLocator should work for iframe', async ({ page, server }) => { await expect(button).toHaveText('Hello iframe'); await button.click(); }); + +it('locator.frameLocator should throw on ambiguity', async ({ page, server }) => { + await routeAmbiguous(page); + await page.goto(server.EMPTY_PAGE); + const button = page.locator('body').frameLocator('iframe').locator('button'); + const error = await button.waitFor().catch(e => e); + expect(error.message).toContain('Error: strict mode violation: "body >> iframe" resolved to 3 elements'); +}); + +it('locator.frameLocator should not throw on first/last/nth', async ({ page, server }) => { + await routeAmbiguous(page); + await page.goto(server.EMPTY_PAGE); + const button1 = page.locator('body').frameLocator('iframe').first().locator('button'); + await expect(button1).toHaveText('Hello from iframe-1.html'); + const button2 = page.locator('body').frameLocator('iframe').nth(1).locator('button'); + await expect(button2).toHaveText('Hello from iframe-2.html'); + const button3 = page.locator('body').frameLocator('iframe').last().locator('button'); + await expect(button3).toHaveText('Hello from iframe-3.html'); +});