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-%%
### 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-%%

View File

@ -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-%%

View File

@ -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">>

View File

@ -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

View File

@ -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 {

View File

@ -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 `<article><div>Playwright</div></article>`.
@ -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 `<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);
});
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');
});