feat(locators): implement last,nth (#7870)

This commit is contained in:
Pavel Feldman 2021-07-27 15:58:18 -07:00 committed by GitHub
parent b9aad5eb86
commit cc43f9339f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 7 deletions

View File

@ -534,6 +534,11 @@ Returns whether the element is [visible](./actionability.md#visible).
### option: Locator.isVisible.timeout = %%-input-timeout-%% ### option: Locator.isVisible.timeout = %%-input-timeout-%%
## method: Locator.last
- returns: <[Locator]>
Returns locator to the last matching element.
## method: Locator.locator ## method: Locator.locator
- returns: <[Locator]> - returns: <[Locator]>
@ -542,6 +547,14 @@ The method finds an element matching the specified selector in the `Locator`'s s
### param: Locator.locator.selector = %%-find-selector-%% ### param: Locator.locator.selector = %%-find-selector-%%
## method: Locator.nth
- returns: <[Locator]>
Returns locator to the n-th matching element.
### param: Locator.nth.index
- `index` <[int]>
## async method: Locator.press ## async method: Locator.press
Focuses the element, and then uses [`method: Keyboard.down`] and [`method: Keyboard.up`]. Focuses the element, and then uses [`method: Keyboard.down`] and [`method: Keyboard.up`].

View File

@ -99,7 +99,15 @@ export class Locator implements api.Locator {
} }
first(): Locator { first(): Locator {
return new Locator(this._frame, this._selector + ' >> _first=true'); return new Locator(this._frame, this._selector + ' >> _nth=first');
}
last(): Locator {
return new Locator(this._frame, this._selector + ` >> _nth=last`);
}
nth(index: number): Locator {
return new Locator(this._frame, this._selector + ` >> _nth=${index}`);
} }
async focus(options?: TimeoutOptions): Promise<void> { async focus(options?: TimeoutOptions): Promise<void> {

View File

@ -75,6 +75,7 @@ export class InjectedScript {
this._engines.set('css', this._createCSSEngine()); this._engines.set('css', this._createCSSEngine());
this._engines.set('_first', { queryAll: () => [] }); this._engines.set('_first', { queryAll: () => [] });
this._engines.set('_visible', { queryAll: () => [] }); this._engines.set('_visible', { queryAll: () => [] });
this._engines.set('_nth', { queryAll: () => [] });
for (const { name, engine } of customEngines) for (const { name, engine } of customEngines)
this._engines.set(name, engine); this._engines.set(name, engine);
@ -110,11 +111,30 @@ export class InjectedScript {
if (index === selector.parts.length) if (index === selector.parts.length)
return roots; return roots;
if (selector.parts[index].name === '_first') const part = selector.parts[index];
return roots.slice(0, 1); if (part.name === '_nth') {
let filtered: ElementMatch[] = [];
if (part.body === 'first') {
filtered = roots.slice(0, 1);
} else if (part.body === 'last') {
if (roots.length)
filtered = roots.slice(roots.length - 1);
} else {
if (typeof selector.capture === 'number')
throw new Error(`Can't query n-th element in a request with the capture.`);
const nth = +part.body;
const set = new Set<Element>();
for (const root of roots) {
set.add(root.element);
if (nth + 1 === set.size)
filtered = [root];
}
}
return this._querySelectorRecursively(filtered, selector, index + 1, queryCache);
}
if (selector.parts[index].name === '_visible') { if (part.name === '_visible') {
const visible = Boolean(selector.parts[index].body); const visible = Boolean(part.body);
return roots.filter(match => visible === isVisible(match.element)); return roots.filter(match => visible === isVisible(match.element));
} }

View File

@ -43,7 +43,7 @@ export class Selectors {
'data-testid', 'data-testid:light', 'data-testid', 'data-testid:light',
'data-test-id', 'data-test-id:light', 'data-test-id', 'data-test-id:light',
'data-test', 'data-test:light', 'data-test', 'data-test:light',
'_visible', '_first' '_visible', '_nth'
]); ]);
this._engines = new Map(); this._engines = new Map();
} }

View File

@ -17,7 +17,7 @@
import { test as it, expect } from './pageTest'; import { test as it, expect } from './pageTest';
it('should respect first()', async ({page}) => { it('should respect first() and last()', async ({page}) => {
await page.setContent(` await page.setContent(`
<section> <section>
<div><p>A</p></div> <div><p>A</p></div>
@ -27,4 +27,23 @@ it('should respect first()', async ({page}) => {
expect(await page.locator('div >> p').count()).toBe(6); expect(await page.locator('div >> p').count()).toBe(6);
expect(await page.locator('div').locator('p').count()).toBe(6); expect(await page.locator('div').locator('p').count()).toBe(6);
expect(await page.locator('div').first().locator('p').count()).toBe(1); expect(await page.locator('div').first().locator('p').count()).toBe(1);
expect(await page.locator('div').last().locator('p').count()).toBe(3);
});
it('should respect nth()', async ({page}) => {
await page.setContent(`
<section>
<div><p>A</p></div>
<div><p>A</p><p>A</p></div>
<div><p>A</p><p>A</p><p>A</p></div>
</section>`);
expect(await page.locator('div >> p').nth(0).count()).toBe(1);
expect(await page.locator('div').nth(1).locator('p').count()).toBe(2);
expect(await page.locator('div').nth(2).locator('p').count()).toBe(3);
});
it('should throw on capture w/ nth()', async ({page}) => {
await page.setContent(`<section><div><p>A</p></div></section>`);
const e = await page.locator('*css=div >> p').nth(0).click().catch(e => e);
expect(e.message).toContain(`Can't query n-th element`);
}); });

11
types/types.d.ts vendored
View File

@ -7592,6 +7592,11 @@ export interface Locator {
timeout?: number; timeout?: number;
}): Promise<boolean>; }): Promise<boolean>;
/**
* Returns locator to the last matching element.
*/
last(): Locator;
/** /**
* The method finds an element matching the specified selector in the `Locator`'s subtree. See * The method finds an element matching the specified selector in the `Locator`'s subtree. See
* [Working with selectors](https://playwright.dev/docs/selectors) for more details. * [Working with selectors](https://playwright.dev/docs/selectors) for more details.
@ -7599,6 +7604,12 @@ export interface Locator {
*/ */
locator(selector: string): Locator; locator(selector: string): Locator;
/**
* Returns locator to the n-th matching element.
* @param index
*/
nth(index: number): Locator;
/** /**
* Focuses the element, and then uses [keyboard.down(key)](https://playwright.dev/docs/api/class-keyboard#keyboard-down) * Focuses the element, and then uses [keyboard.down(key)](https://playwright.dev/docs/api/class-keyboard#keyboard-down)
* and [keyboard.up(key)](https://playwright.dev/docs/api/class-keyboard#keyboard-up). * and [keyboard.up(key)](https://playwright.dev/docs/api/class-keyboard#keyboard-up).