Skip to content

Commit

Permalink
feat(selectors): remove index for now, add documentation (#4640)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman committed Dec 9, 2020
1 parent 1d90d7a commit ab44d68
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 40 deletions.
6 changes: 6 additions & 0 deletions docs-src/api-footer.md
Expand Up @@ -84,6 +84,12 @@ Selector describes an element in the page. It can be used to obtain `ElementHand

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.

Playwright also supports the following CSS extensions:
* `:text("string")` - Matches elements that contain specific text node. Learn more about [text selector](./selectors.md#css-extension-text).
* `:visible` - Matches only visible elements. Learn more about [visible selector](./selectors.md#css-extension-visible).
* `:light(selector)` - Matches in the light DOM only as opposite to piercing open shadow roots. Learn more about [shadow piercing](./selectors.md#shadow-piercing).
* `: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).

For convenience, selectors in the wrong format are heuristically converted to the right format:
- selector starting with `//` or `..` is assumed to be `xpath=selector`;
- selector starting and ending with a quote (either `"` or `'`) is assumed to be `text=selector`;
Expand Down
6 changes: 6 additions & 0 deletions docs/api.md
Expand Up @@ -5456,6 +5456,12 @@ Selector describes an element in the page. It can be used to obtain `ElementHand

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.

Playwright also supports the following CSS extensions:
* `:text("string")` - Matches elements that contain specific text node. Learn more about [text selector](./selectors.md#css-extension-text).
* `:visible` - Matches only visible elements. Learn more about [visible selector](./selectors.md#css-extension-visible).
* `:light(selector)` - Matches in the light DOM only as opposite to piercing open shadow roots. Learn more about [shadow piercing](./selectors.md#shadow-piercing).
* `: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).

For convenience, selectors in the wrong format are heuristically converted to the right format:
- selector starting with `//` or `..` is assumed to be `xpath=selector`;
- selector starting and ending with a quote (either `"` or `'`) is assumed to be `text=selector`;
Expand Down
74 changes: 70 additions & 4 deletions docs/selectors.md
Expand Up @@ -10,7 +10,7 @@ Selectors query elements on the web page for interactions, like [page.click](api
<!-- GEN:stop -->

## Syntax
Selectors are defined by selector engine name and selector body, `engine=body`.
Selectors are defined by selector engine name and selector body, `engine=body`.

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

`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')`.

`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.
Playwright augments standard CSS selectors in two ways, see below for more details:
* `css` engine pierces open shadow DOM by default.
* Playwright adds a few custom pseudo-classes like `:visible`.

`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.
#### Shadow piercing

`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.

#### Examples
`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.

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

#### CSS extension: visible

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.

```js
// Clicks the first button.
await page.click('button');
// Clicks the first visible button. If there are some invisible buttons, this click will just ignore them.
await page.click('button:visible');
```

Use `:visible` with caution, because it has two major drawbacks:
* When elements change their visibility dynamically, `:visible` will give upredictable 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.

#### CSS extension: text

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:

* `:text("exact match")` - Only matches when element's text exactly equals to passed string.
* `:text("substring", "g")` - Matches when element's text contains "substring" somewhere.
* `:text("String", "i")` - Performs case-insensitive match.
* `:text("string with spaces", "s")` - Normalizes whitespace when matching, for example turns multiple spaces into one and line breaks into spaces.
* `:text("substring", "sgi")` - Different flags may be combined. For example, pass `"sgi"` to match by case-insensitive substring with normalized whitespace.
* `button:text("Sign in")` - Text selector may be combined with regular CSS.
* `:matches-text("[+-]?\\d+")` - Matches text against a regular expression. Note that back-slash `\` and quotes `"` must be escaped.
* `: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).

```js
// Click a button with text "Sign in".
await page.click('button:text("Sign in")');
```

#### CSS extension: light

`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.

```js
await page.click(':light(.article > .header)');
```

#### CSS extension: proximity

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

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.

* `:right-of(css > selector)` - Matches elements that are to the right of any element matching the inner selector.
* `:left-of(css > selector)` - Matches elements that are to the left of any element matching the inner selector.
* `:above(css > selector)` - Matches elements that are above any of the elements matching the inner selector.
* `:below(css > selector)` - Matches elements that are below any of the elements matching the inner selector.
* `:near(css > selector)` - Matches elements that are near any of the elements matching the inner selector.
* `:within(css > selector)` - Matches elements that are within any of the elements matching the inner selector.

```js
// Fill an input to the right of "Username".
await page.fill('input:right-of(:text("Username"))');

// Click a button near the promo card.
await page.click('button:near(.promo-card)');
```

### xpath

XPath engine is equivalent to [`Document.evaluate`](https://developer.mozilla.org/en/docs/Web/API/Document/evaluate). Example: `xpath=//html/body`.
Expand Down
2 changes: 1 addition & 1 deletion src/server/common/selectorParser.ts
Expand Up @@ -35,7 +35,7 @@ export function selectorsV2Enabled() {
}

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

export function parseSelector(selector: string, customNames: Set<string>): ParsedSelector {
Expand Down
11 changes: 0 additions & 11 deletions src/server/injected/selectorEvaluator.ts
Expand Up @@ -53,7 +53,6 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
this._engines.set('has', hasEngine);
this._engines.set('scope', scopeEngine);
this._engines.set('light', lightEngine);
this._engines.set('index', indexEngine);
this._engines.set('visible', visibleEngine);
this._engines.set('text', textEngine);
this._engines.set('matches-text', matchesTextEngine);
Expand Down Expand Up @@ -353,16 +352,6 @@ const lightEngine: SelectorEngine = {
}
};

const indexEngine: SelectorEngine = {
query(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[] {
if (args.length < 2 || typeof args[0] !== 'number')
throw new Error(`"index" engine expects a number and non-empty selector list`);
const list = evaluator.query(context, args.slice(1));
const index = (args[0] as number) - 1;
return [list[index]];
},
};

const visibleEngine: SelectorEngine = {
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
if (args.length)
Expand Down
24 changes: 0 additions & 24 deletions test/selectors-misc.spec.ts
Expand Up @@ -30,30 +30,6 @@ it('should work for open shadow roots', async ({page, server}) => {
expect(await page.$$(`data-testid:light=foo`)).toEqual([]);
});

it('should work with :index', async ({page}) => {
if (!selectorsV2Enabled())
return; // Selectors v1 do not support this.
await page.setContent(`
<section>
<div id=target1></div>
<div id=target2></div>
<span id=target3></span>
<div id=target4></div>
</section>
`);
expect(await page.$$eval(`:index(1, div, span)`, els => els.map(e => e.id).join(';'))).toBe('target1');
expect(await page.$$eval(`:index(2, div, span)`, els => els.map(e => e.id).join(';'))).toBe('target2');
expect(await page.$$eval(`:index(3, div, span)`, els => els.map(e => e.id).join(';'))).toBe('target3');

const error = await page.waitForSelector(`:index(5, div, span)`, { timeout: 100 }).catch(e => e);
expect(error.message).toContain('100ms');

const promise = page.waitForSelector(`:index(5, div, span)`, { state: 'attached' });
await page.$eval('section', section => section.appendChild(document.createElement('span')));
const element = await promise;
expect(await element.evaluate(e => e.tagName)).toBe('SPAN');
});

it('should work with :visible', async ({page}) => {
if (!selectorsV2Enabled())
return; // Selectors v1 do not support this.
Expand Down

0 comments on commit ab44d68

Please sign in to comment.