Skip to content

Commit ab44d68

Browse files
authored
feat(selectors): remove index for now, add documentation (#4640)
1 parent 1d90d7a commit ab44d68

File tree

6 files changed

+83
-40
lines changed

6 files changed

+83
-40
lines changed

docs-src/api-footer.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ Selector describes an element in the page. It can be used to obtain `ElementHand
8484

8585
Selector has the following format: `engine=body [>> engine=body]*`. Here `engine` is one of the supported [selector engines](selectors.md) (e.g. `css` or `xpath`), and `body` is a selector body in the format of the particular engine. When multiple `engine=body` clauses are present (separated by `>>`), next one is queried relative to the previous one's result.
8686

87+
Playwright also supports the following CSS extensions:
88+
* `:text("string")` - Matches elements that contain specific text node. Learn more about [text selector](./selectors.md#css-extension-text).
89+
* `:visible` - Matches only visible elements. Learn more about [visible selector](./selectors.md#css-extension-visible).
90+
* `:light(selector)` - Matches in the light DOM only as opposite to piercing open shadow roots. Learn more about [shadow piercing](./selectors.md#shadow-piercing).
91+
* `:right-of(selector)`, `:left-of(selector)`, `:above(selector)`, `:below(selector)`, `:near(selector)`, `:within(selector)` - Match elements based on their relative position to another element. Learn more about [proximity selectors](./selectors.md#css-extension-proximity).
92+
8793
For convenience, selectors in the wrong format are heuristically converted to the right format:
8894
- selector starting with `//` or `..` is assumed to be `xpath=selector`;
8995
- selector starting and ending with a quote (either `"` or `'`) is assumed to be `text=selector`;

docs/api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5456,6 +5456,12 @@ Selector describes an element in the page. It can be used to obtain `ElementHand
54565456

54575457
Selector has the following format: `engine=body [>> engine=body]*`. Here `engine` is one of the supported [selector engines](selectors.md) (e.g. `css` or `xpath`), and `body` is a selector body in the format of the particular engine. When multiple `engine=body` clauses are present (separated by `>>`), next one is queried relative to the previous one's result.
54585458

5459+
Playwright also supports the following CSS extensions:
5460+
* `:text("string")` - Matches elements that contain specific text node. Learn more about [text selector](./selectors.md#css-extension-text).
5461+
* `:visible` - Matches only visible elements. Learn more about [visible selector](./selectors.md#css-extension-visible).
5462+
* `:light(selector)` - Matches in the light DOM only as opposite to piercing open shadow roots. Learn more about [shadow piercing](./selectors.md#shadow-piercing).
5463+
* `:right-of(selector)`, `:left-of(selector)`, `:above(selector)`, `:below(selector)`, `:near(selector)`, `:within(selector)` - Match elements based on their relative position to another element. Learn more about [proximity selectors](./selectors.md#css-extension-proximity).
5464+
54595465
For convenience, selectors in the wrong format are heuristically converted to the right format:
54605466
- selector starting with `//` or `..` is assumed to be `xpath=selector`;
54615467
- selector starting and ending with a quote (either `"` or `'`) is assumed to be `text=selector`;

docs/selectors.md

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Selectors query elements on the web page for interactions, like [page.click](api
1010
<!-- GEN:stop -->
1111

1212
## Syntax
13-
Selectors are defined by selector engine name and selector body, `engine=body`.
13+
Selectors are defined by selector engine name and selector body, `engine=body`.
1414

1515
* `engine` refers to one of the [supported engines](#selector-engines)
1616
* Built-in selector engines: [css], [text], [xpath] and [id selectors][id]
@@ -136,11 +136,15 @@ const handle = await divHandle.$('css=span');
136136

137137
`css` is a default engine - any malformed selector not starting with `//` nor starting and ending with a quote is assumed to be a css selector. For example, Playwright converts `page.$('span > button')` to `page.$('css=span > button')`.
138138

139-
`css:light` engine is equivalent to [`Document.querySelector`](https://developer.mozilla.org/en/docs/Web/API/Document/querySelector) and behaves according to the CSS spec. However, it does not pierce shadow roots, which may be inconvenient when working with [Shadow DOM and Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). For that reason, `css` engine pierces shadow roots. More specifically, every [Descendant combinator](https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_combinator) pierces an arbitrary number of open shadow roots, including the implicit descendant combinator at the start of the selector.
139+
Playwright augments standard CSS selectors in two ways, see below for more details:
140+
* `css` engine pierces open shadow DOM by default.
141+
* Playwright adds a few custom pseudo-classes like `:visible`.
140142

141-
`css` engine first searches for elements in the light dom in the iteration order, and then recursively inside open shadow roots in the iteration order. It does not search inside closed shadow roots or iframes.
143+
#### Shadow piercing
144+
145+
`css:light` engine is equivalent to [`Document.querySelector`](https://developer.mozilla.org/en/docs/Web/API/Document/querySelector) and behaves according to the CSS spec. However, it does not pierce shadow roots, which may be inconvenient when working with [Shadow DOM and Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). For that reason, `css` engine pierces shadow roots. More specifically, any [Descendant combinator](https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_combinator) or [Child combinator](https://developer.mozilla.org/en-US/docs/Web/CSS/Child_combinator) pierces an arbitrary number of open shadow roots, including the implicit descendant combinator at the start of the selector.
142146

143-
#### Examples
147+
`css` engine first searches for elements in the light dom in the iteration order, and then recursively inside open shadow roots in the iteration order. It does not search inside closed shadow roots or iframes.
144148

145149
```html
146150
<article>
@@ -171,6 +175,68 @@ Note that `<open mode shadow root>` is not an html element, but rather a shadow
171175
- `"css:light=article > .in-the-shadow"` does not match anything.
172176
- `"css=article li#target"` matches the `<li id='target'>Deep in the shadow</li>`, piercing two shadow roots.
173177

178+
#### CSS extension: visible
179+
180+
The `:visible` pseudo-class matches elements that are visible as defined in the [actionability](./actionability.md#visible) guide. 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.
181+
182+
```js
183+
// Clicks the first button.
184+
await page.click('button');
185+
// Clicks the first visible button. If there are some invisible buttons, this click will just ignore them.
186+
await page.click('button:visible');
187+
```
188+
189+
Use `:visible` with caution, because it has two major drawbacks:
190+
* When elements change their visibility dynamically, `:visible` will give upredictable results based on the timing.
191+
* `:visible` forces a layout and may lead to querying being slow, especially when used with `page.waitForSelector(selector[, options])` method.
192+
193+
#### CSS extension: text
194+
195+
The `:text` pseudo-class matches elements that have a text node child with specific text. It is similar to the [text engine](#text-and-textlight). There are a few variations that support different arguments:
196+
197+
* `:text("exact match")` - Only matches when element's text exactly equals to passed string.
198+
* `:text("substring", "g")` - Matches when element's text contains "substring" somewhere.
199+
* `:text("String", "i")` - Performs case-insensitive match.
200+
* `:text("string with spaces", "s")` - Normalizes whitespace when matching, for example turns multiple spaces into one and line breaks into spaces.
201+
* `:text("substring", "sgi")` - Different flags may be combined. For example, pass `"sgi"` to match by case-insensitive substring with normalized whitespace.
202+
* `button:text("Sign in")` - Text selector may be combined with regular CSS.
203+
* `:matches-text("[+-]?\\d+")` - Matches text against a regular expression. Note that back-slash `\` and quotes `"` must be escaped.
204+
* `:matches-text("regex", "g")` - Matches text against a regular expression with specified flags. Learn more about [regular expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp).
205+
206+
```js
207+
// Click a button with text "Sign in".
208+
await page.click('button:text("Sign in")');
209+
```
210+
211+
#### CSS extension: light
212+
213+
`css` engine [pierces shadow](#shadow-piercing) by default. It is possible to disable this behavior by wrapping a selector in `:light` pseudo-class: `:light(section > button.class)` matches in light DOM only.
214+
215+
```js
216+
await page.click(':light(.article > .header)');
217+
```
218+
219+
#### CSS extension: proximity
220+
221+
Playwright provides a few proximity selectors based on the page layout. These can be combined with regular CSS for better results, for example `input:right-of(:text("Password"))` matches an input field that is to the right of text "Password".
222+
223+
Note that Playwright uses some heuristics to determine whether one element should be considered to the left/right/above/below/near/within another. Therefore, using proximity selectors may produce unpredictable results. For example, selector could stop matching when element moves by one pixel.
224+
225+
* `:right-of(css > selector)` - Matches elements that are to the right of any element matching the inner selector.
226+
* `:left-of(css > selector)` - Matches elements that are to the left of any element matching the inner selector.
227+
* `:above(css > selector)` - Matches elements that are above any of the elements matching the inner selector.
228+
* `:below(css > selector)` - Matches elements that are below any of the elements matching the inner selector.
229+
* `:near(css > selector)` - Matches elements that are near any of the elements matching the inner selector.
230+
* `:within(css > selector)` - Matches elements that are within any of the elements matching the inner selector.
231+
232+
```js
233+
// Fill an input to the right of "Username".
234+
await page.fill('input:right-of(:text("Username"))');
235+
236+
// Click a button near the promo card.
237+
await page.click('button:near(.promo-card)');
238+
```
239+
174240
### xpath
175241

176242
XPath engine is equivalent to [`Document.evaluate`](https://developer.mozilla.org/en/docs/Web/API/Document/evaluate). Example: `xpath=//html/body`.

src/server/common/selectorParser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function selectorsV2Enabled() {
3535
}
3636

3737
export function selectorsV2EngineNames() {
38-
return ['not', 'is', 'where', 'has', 'scope', 'light', 'index', 'visible', 'matches-text', 'above', 'below', 'right-of', 'left-of', 'near', 'within'];
38+
return ['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'matches-text', 'above', 'below', 'right-of', 'left-of', 'near', 'within'];
3939
}
4040

4141
export function parseSelector(selector: string, customNames: Set<string>): ParsedSelector {

src/server/injected/selectorEvaluator.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
5353
this._engines.set('has', hasEngine);
5454
this._engines.set('scope', scopeEngine);
5555
this._engines.set('light', lightEngine);
56-
this._engines.set('index', indexEngine);
5756
this._engines.set('visible', visibleEngine);
5857
this._engines.set('text', textEngine);
5958
this._engines.set('matches-text', matchesTextEngine);
@@ -353,16 +352,6 @@ const lightEngine: SelectorEngine = {
353352
}
354353
};
355354

356-
const indexEngine: SelectorEngine = {
357-
query(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[] {
358-
if (args.length < 2 || typeof args[0] !== 'number')
359-
throw new Error(`"index" engine expects a number and non-empty selector list`);
360-
const list = evaluator.query(context, args.slice(1));
361-
const index = (args[0] as number) - 1;
362-
return [list[index]];
363-
},
364-
};
365-
366355
const visibleEngine: SelectorEngine = {
367356
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
368357
if (args.length)

test/selectors-misc.spec.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,30 +30,6 @@ it('should work for open shadow roots', async ({page, server}) => {
3030
expect(await page.$$(`data-testid:light=foo`)).toEqual([]);
3131
});
3232

33-
it('should work with :index', async ({page}) => {
34-
if (!selectorsV2Enabled())
35-
return; // Selectors v1 do not support this.
36-
await page.setContent(`
37-
<section>
38-
<div id=target1></div>
39-
<div id=target2></div>
40-
<span id=target3></span>
41-
<div id=target4></div>
42-
</section>
43-
`);
44-
expect(await page.$$eval(`:index(1, div, span)`, els => els.map(e => e.id).join(';'))).toBe('target1');
45-
expect(await page.$$eval(`:index(2, div, span)`, els => els.map(e => e.id).join(';'))).toBe('target2');
46-
expect(await page.$$eval(`:index(3, div, span)`, els => els.map(e => e.id).join(';'))).toBe('target3');
47-
48-
const error = await page.waitForSelector(`:index(5, div, span)`, { timeout: 100 }).catch(e => e);
49-
expect(error.message).toContain('100ms');
50-
51-
const promise = page.waitForSelector(`:index(5, div, span)`, { state: 'attached' });
52-
await page.$eval('section', section => section.appendChild(document.createElement('span')));
53-
const element = await promise;
54-
expect(await element.evaluate(e => e.tagName)).toBe('SPAN');
55-
});
56-
5733
it('should work with :visible', async ({page}) => {
5834
if (!selectorsV2Enabled())
5935
return; // Selectors v1 do not support this.

0 commit comments

Comments
 (0)