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');
+});