diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md index d3cef417f6..48829a775d 100644 --- a/docs/src/api/class-framelocator.md +++ b/docs/src/api/class-framelocator.md @@ -196,7 +196,7 @@ Returns locator to the last matching frame. %%-template-locator-locator-%% -### param: FrameLocator.locator.selector = %%-find-selector-%% +### param: FrameLocator.locator.selectorOrLocator = %%-find-selector-or-locator-%% * since: v1.17 ### option: FrameLocator.locator.-inline- = %%-locator-options-list-v1.14-%% diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 176011a1b5..f2f2acc619 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -959,7 +959,7 @@ var locator = page.FrameLocator("iframe").GetByText("Submit"); await locator.ClickAsync(); ``` -### param: Locator.frameLocator.selector = %%-find-selector-%% +### param: Locator.frameLocator.selectorOrLocator = %%-find-selector-%% * since: v1.17 ## async method: Locator.getAttribute @@ -1389,7 +1389,7 @@ var banana = await page.GetByRole(AriaRole.Listitem).Last(1); %%-template-locator-locator-%% -### param: Locator.locator.selector = %%-find-selector-%% +### param: Locator.locator.selectorOrLocator = %%-find-selector-or-locator-%% * since: v1.14 ### option: Locator.locator.-inline- = %%-locator-options-list-v1.14-%% diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 48571a4b62..702a6cbed9 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -134,6 +134,11 @@ A selector to query for. A selector to use when resolving DOM element. +## find-selector-or-locator +- `selectorOrLocator` <[string]|[Locator]> + +A selector or locator to use when resolving DOM element. + ## wait-for-selector-state - `state` <[WaitForSelectorState]<"attached"|"detached"|"visible"|"hidden">> diff --git a/docs/src/locators.md b/docs/src/locators.md index 40a36d2ae9..6fa7aa7ec7 100644 --- a/docs/src/locators.md +++ b/docs/src/locators.md @@ -1044,6 +1044,44 @@ await product .ClickAsync(); ``` +You can also chain two locators together, for example to find a "Save" button inside a particular dialog: + +```js +const saveButton = page.getByRole('button', { name: 'Save' }); +// ... +const dialog = page.getByTestId('settings-dialog'); +await dialog.locator(saveButton).click(); +``` + +```python async +save_button = page.get_by_role("button", name="Save") +# ... +dialog = page.get_by_test_id("settings-dialog") +await dialog.locator(save_button).click() +``` + +```python sync +save_button = page.get_by_role("button", name="Save") +# ... +dialog = page.get_by_test_id("settings-dialog") +dialog.locator(save_button).click() +``` + +```java +Locator saveButton = page.getByRole(AriaRole.BUTTON, + new Page.GetByRoleOptions().setName("Save")); +// ... +Locator dialog = page.getByTestId("settings-dialog"); +dialog.locator(saveButton).click(); +``` + +```csharp +var saveButton = page.GetByRole(AriaRole.Button, new() { Name = "Save" }); +// ... +var dialog = page.GetByTestId("settings-dialog"); +await dialog.Locator(saveButton).ClickAsync(); +``` + ## Lists ### Count items in a list diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 295fed4ed0..1c6530d28c 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -18,7 +18,7 @@ import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type * as channels from '@protocol/channels'; import * as util from 'util'; -import { monotonicTime } from '../utils'; +import { isString, monotonicTime } from '../utils'; import { ElementHandle } from './elementHandle'; import type { Frame } from './frame'; import type { FilePayload, FrameExpectOptions, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types'; @@ -128,8 +128,12 @@ export class Locator implements api.Locator { return this._frame._highlight(this._selector); } - locator(selector: string, options?: LocatorOptions): Locator { - return new Locator(this._frame, this._selector + ' >> ' + selector, options); + locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator { + if (isString(selectorOrLocator)) + return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options); + if (selectorOrLocator._frame !== this._frame) + throw new Error(`Locators must belong to the same frame.`); + return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator._selector, options); } getByTestId(testId: string | RegExp): Locator { @@ -336,8 +340,12 @@ export class FrameLocator implements api.FrameLocator { this._frameSelector = selector; } - locator(selector: string, options?: { hasText?: string | RegExp }): Locator { - return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector, options); + locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator { + if (isString(selectorOrLocator)) + return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator, options); + if (selectorOrLocator._frame !== this._frame) + throw new Error(`Locators must belong to the same frame.`); + return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator._selector, options); } getByTestId(testId: string | RegExp): Locator { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 7cd013cdf7..906225bd7b 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -11392,10 +11392,10 @@ export interface Locator { * method. * * [Learn more about locators](https://playwright.dev/docs/locators). - * @param selector A selector to use when resolving DOM element. + * @param selectorOrLocator A selector or locator to use when resolving DOM element. * @param options */ - locator(selector: string, options?: { + locator(selectorOrLocator: string|Locator, options?: { /** * Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer * one. For example, `article` that has `text=Playwright` matches `
Playwright
`. @@ -16969,10 +16969,10 @@ export interface FrameLocator { * method. * * [Learn more about locators](https://playwright.dev/docs/locators). - * @param selector A selector to use when resolving DOM element. + * @param selectorOrLocator A selector or locator to use when resolving DOM element. * @param options */ - locator(selector: string, options?: { + locator(selectorOrLocator: string|Locator, options?: { /** * Matches elements containing an element that matches an inner locator. Inner locator is queried against the outer * one. For example, `article` that has `text=Playwright` matches `
Playwright
`. diff --git a/tests/page/locator-misc-2.spec.ts b/tests/page/locator-misc-2.spec.ts index 1cc2b56004..1fd448cce3 100644 --- a/tests/page/locator-misc-2.spec.ts +++ b/tests/page/locator-misc-2.spec.ts @@ -154,3 +154,20 @@ it('locator.count should work with deleted Map in main world', async ({ page }) await expect(page.locator('#searchResultTableDiv .x-grid3-row')).toHaveCount(0); }); +it('Locator.locator() and FrameLocator.locator() should accept locator', async ({ page }) => { + await page.setContent(` +
+ + `); + + const inputLocator = page.locator('input'); + expect(await inputLocator.inputValue()).toBe('outer'); + expect(await page.locator('div').locator(inputLocator).inputValue()).toBe('outer'); + expect(await page.frameLocator('iframe').locator(inputLocator).inputValue()).toBe('inner'); + expect(await page.frameLocator('iframe').locator('div').locator(inputLocator).inputValue()).toBe('inner'); + + const divLocator = page.locator('div'); + expect(await divLocator.locator('input').inputValue()).toBe('outer'); + expect(await page.frameLocator('iframe').locator(divLocator).locator('input').inputValue()).toBe('inner'); +}); +