feat: allow chaining locators with Locator.locator(anotherLocator) (#21391)

This commit is contained in:
Dmitry Gozman 2023-03-03 14:50:53 -08:00 committed by GitHub
parent d904a6129f
commit 0c5d46bb94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 80 additions and 12 deletions

View file

@ -196,7 +196,7 @@ Returns locator to the last matching frame.
%%-template-locator-locator-%% %%-template-locator-locator-%%
### param: FrameLocator.locator.selector = %%-find-selector-%% ### param: FrameLocator.locator.selectorOrLocator = %%-find-selector-or-locator-%%
* since: v1.17 * since: v1.17
### option: FrameLocator.locator.-inline- = %%-locator-options-list-v1.14-%% ### option: FrameLocator.locator.-inline- = %%-locator-options-list-v1.14-%%

View file

@ -959,7 +959,7 @@ var locator = page.FrameLocator("iframe").GetByText("Submit");
await locator.ClickAsync(); await locator.ClickAsync();
``` ```
### param: Locator.frameLocator.selector = %%-find-selector-%% ### param: Locator.frameLocator.selectorOrLocator = %%-find-selector-%%
* since: v1.17 * since: v1.17
## async method: Locator.getAttribute ## async method: Locator.getAttribute
@ -1389,7 +1389,7 @@ var banana = await page.GetByRole(AriaRole.Listitem).Last(1);
%%-template-locator-locator-%% %%-template-locator-locator-%%
### param: Locator.locator.selector = %%-find-selector-%% ### param: Locator.locator.selectorOrLocator = %%-find-selector-or-locator-%%
* since: v1.14 * since: v1.14
### option: Locator.locator.-inline- = %%-locator-options-list-v1.14-%% ### option: Locator.locator.-inline- = %%-locator-options-list-v1.14-%%

View file

@ -134,6 +134,11 @@ A selector to query for.
A selector to use when resolving DOM element. 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 ## wait-for-selector-state
- `state` <[WaitForSelectorState]<"attached"|"detached"|"visible"|"hidden">> - `state` <[WaitForSelectorState]<"attached"|"detached"|"visible"|"hidden">>

View file

@ -1044,6 +1044,44 @@ await product
.ClickAsync(); .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 ## Lists
### Count items in a list ### Count items in a list

View file

@ -18,7 +18,7 @@ import type * as structs from '../../types/structs';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import * as util from 'util'; import * as util from 'util';
import { monotonicTime } from '../utils'; import { isString, monotonicTime } from '../utils';
import { ElementHandle } from './elementHandle'; import { ElementHandle } from './elementHandle';
import type { Frame } from './frame'; import type { Frame } from './frame';
import type { FilePayload, FrameExpectOptions, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types'; 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); return this._frame._highlight(this._selector);
} }
locator(selector: string, options?: LocatorOptions): Locator { locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator {
return new Locator(this._frame, this._selector + ' >> ' + selector, options); 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 { getByTestId(testId: string | RegExp): Locator {
@ -336,8 +340,12 @@ export class FrameLocator implements api.FrameLocator {
this._frameSelector = selector; this._frameSelector = selector;
} }
locator(selector: string, options?: { hasText?: string | RegExp }): Locator { locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator {
return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector, options); 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 { getByTestId(testId: string | RegExp): Locator {

View file

@ -11392,10 +11392,10 @@ export interface Locator {
* method. * method.
* *
* [Learn more about locators](https://playwright.dev/docs/locators). * [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 * @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 * 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 `<article><div>Playwright</div></article>`. * one. For example, `article` that has `text=Playwright` matches `<article><div>Playwright</div></article>`.
@ -16969,10 +16969,10 @@ export interface FrameLocator {
* method. * method.
* *
* [Learn more about locators](https://playwright.dev/docs/locators). * [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 * @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 * 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 `<article><div>Playwright</div></article>`. * one. For example, `article` that has `text=Playwright` matches `<article><div>Playwright</div></article>`.

View file

@ -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); await expect(page.locator('#searchResultTableDiv .x-grid3-row')).toHaveCount(0);
}); });
it('Locator.locator() and FrameLocator.locator() should accept locator', async ({ page }) => {
await page.setContent(`
<div><input value=outer></div>
<iframe srcdoc="<div><input value=inner></div>"></iframe>
`);
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');
});