mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(nth): make nth and visible selectors public (#8142)
This commit is contained in:
parent
aaf565c9ce
commit
1d4e2fe98c
@ -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
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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`);
|
||||
});
|
||||
|
||||
|
||||
@ -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}) => {
|
||||
/*
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user