feat(nth): make nth and visible selectors public (#8142)

This commit is contained in:
Pavel Feldman 2021-08-11 11:06:09 -07:00 committed by GitHub
parent aaf565c9ce
commit 1d4e2fe98c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 115 additions and 22 deletions

View File

@ -407,10 +407,15 @@ await page.ClickAsync("button");
## Selecting visible elements
The `:visible` pseudo-class in CSS selectors matches the elements that are
[visible](./actionability.md#visible). For example, `input` matches all the inputs on the page, while
`input:visible` matches only visible inputs. This is useful to distinguish elements that are very
similar but differ in visibility.
There are two ways of selecting only [visible](./actionability.md#visible) elements with Playwright:
- `:visible` pseudo-class in CSS selectors
- `visible=` selector engine
If you prefer your selectors to be CSS and don't want to rely on [chaining selectors](#chaining-selectors), use `:visible` pseudo class like so: `input:visible`. If you prefer combining selector engines, use `input >> visible=true`. The latter allows you combining `text=`, `xpath=` and other selector engines with the visibility filter.
For example, `input` matches all the inputs on the page, while
`input:visible` and `input >> visible=true` only match visible inputs. This is useful to distinguish elements
that are very similar but differ in visibility.
:::note
It's usually better to follow the [best practices](#best-practices) and find a more reliable way to
@ -446,28 +451,29 @@ Consider a page with two buttons, first invisible and second visible.
await page.ClickAsync("button");
```
* This will find a second button, because it is visible, and then click it.
* These will find a second button, because it is visible, and then click it.
```js
await page.click('button:visible');
await page.click('button >> visible=true');
```
```java
page.click("button:visible");
page.click("button >> visible=true");
```
```python async
await page.click("button:visible")
await page.click("button >> visible=true")
```
```python sync
page.click("button:visible")
page.click("button >> visible=true")
```
```csharp
await page.ClickAsync("button:visible");
await page.ClickAsync("button >> visible=true");
```
Use `:visible` with caution, because it has two major drawbacks:
* When elements change their visibility dynamically, `:visible` will give unpredictable results based on the timing.
* `:visible` forces a layout and may lead to querying being slow, especially when used with `page.waitForSelector(selector[, options])` method.
## Selecting elements that contain other elements
The `:has()` pseudo-class is an [experimental CSS pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/:has). It returns an element if any of the selectors passed as parameters
@ -661,6 +667,50 @@ converts `'//html/body'` to `'xpath=//html/body'`.
`xpath` does not pierce shadow roots
:::
## N-th element selector
You can narrow down query to the n-th match using the `nth=` selector. Unlike CSS's nth-match, provided index is 0-based.
```js
// Click first button
await page.click('button >> nth=0');
// Click last button
await page.click('button >> nth=-1');
```
```java
// Click first button
page.click("button >> nth=0");
// Click last button
page.click("button >> nth=-1");
```
```python async
# Click first button
await page.click("button >> nth=0")
# Click last button
await page.click("button >> nth=-1")
```
```python sync
# Click first button
page.click("button >> nth=0")
# Click last button
page.click("button >> nth=-1")
```
```csharp
// Click first button
await page.ClickAsync("button >> nth=0");
// Click last button
await page.ClickAsync("button >> nth=-1");
```
## React selectors
:::note

View File

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

View File

@ -77,9 +77,8 @@ export class InjectedScript {
this._engines.set('data-test', this._createAttributeEngine('data-test', true));
this._engines.set('data-test:light', this._createAttributeEngine('data-test', false));
this._engines.set('css', this._createCSSEngine());
this._engines.set('_first', { queryAll: () => [] });
this._engines.set('_visible', { queryAll: () => [] });
this._engines.set('_nth', { queryAll: () => [] });
this._engines.set('nth', { queryAll: () => [] });
this._engines.set('visible', { queryAll: () => [] });
for (const { name, engine } of customEngines)
this._engines.set(name, engine);
@ -116,11 +115,11 @@ export class InjectedScript {
return roots;
const part = selector.parts[index];
if (part.name === '_nth') {
if (part.name === 'nth') {
let filtered: ElementMatch[] = [];
if (part.body === 'first') {
if (part.body === '0') {
filtered = roots.slice(0, 1);
} else if (part.body === 'last') {
} else if (part.body === '-1') {
if (roots.length)
filtered = roots.slice(roots.length - 1);
} else {
@ -137,7 +136,7 @@ export class InjectedScript {
return this._querySelectorRecursively(filtered, selector, index + 1, queryCache);
}
if (part.name === '_visible') {
if (part.name === 'visible') {
const visible = Boolean(part.body);
return roots.filter(match => visible === isVisible(match.element));
}

View File

@ -45,7 +45,7 @@ export class Selectors {
'data-testid', 'data-testid:light',
'data-test-id', 'data-test-id:light',
'data-test', 'data-test:light',
'_visible', '_nth'
'nth', 'visible'
]);
this._builtinEnginesInMainWorld = new Set([
'_react', '_vue',

View File

@ -282,7 +282,7 @@ function escapeForRegex(text: string): string {
}
function quoteString(text: string): string {
return `"${text.replaceAll(/"/g, '\\"').replaceAll(/\n/g, '\\n')}"`;
return `"${text.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
}
function joinTokens(tokens: SelectorToken[]): string {

View File

@ -44,7 +44,7 @@ it('should respect nth()', async ({page}) => {
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);
const e = await page.locator('*css=div >> p').nth(1).click().catch(e => e);
expect(e.message).toContain(`Can't query n-th element`);
});

View File

@ -58,6 +58,26 @@ it('should work with :visible', async ({page}) => {
expect(await page.$eval('div:visible', div => div.id)).toBe('target2');
});
it('should work with >> visible=', async ({page}) => {
await page.setContent(`
<section>
<div id=target1></div>
<div id=target2></div>
</section>
`);
expect(await page.$('div >> visible=true')).toBe(null);
const error = await page.waitForSelector(`div >> visible=true`, { timeout: 100 }).catch(e => e);
expect(error.message).toContain('100ms');
const promise = page.waitForSelector(`div >> visible=true`, { state: 'attached' });
await page.$eval('#target2', div => div.textContent = 'Now visible');
const element = await promise;
expect(await element.evaluate(e => e.id)).toBe('target2');
expect(await page.$eval('div >> visible=true', div => div.id)).toBe('target2');
});
it('should work with :nth-match', async ({page}) => {
await page.setContent(`
<section>
@ -93,6 +113,30 @@ it('should work with :nth-match', async ({page}) => {
expect(await element.evaluate(e => e.id)).toBe('target3');
});
it('should work with nth=', async ({page}) => {
await page.setContent(`
<section>
<div id=target1></div>
<div id=target2></div>
</section>
`);
expect(await page.$('div >> nth=2')).toBe(null);
expect(await page.$eval('div >> nth=0', e => e.id)).toBe('target1');
expect(await page.$eval('div >> nth=1', e => e.id)).toBe('target2');
expect(await page.$eval('section > div >> nth=1', e => e.id)).toBe('target2');
expect(await page.$eval('section, div >> nth=1', e => e.id)).toBe('target1');
expect(await page.$eval('div, section >> nth=2', e => e.id)).toBe('target2');
const promise = page.waitForSelector(`div >> nth=2`, { state: 'attached' });
await page.$eval('section', section => {
const div = document.createElement('div');
div.setAttribute('id', 'target3');
section.appendChild(div);
});
const element = await promise;
expect(await element.evaluate(e => e.id)).toBe('target3');
});
it('should work with position selectors', async ({page}) => {
/*