From ab44d682ca75c9b56745072a04de06f9c6e72e33 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 9 Dec 2020 13:08:37 -0800 Subject: [PATCH] feat(selectors): remove index for now, add documentation (#4640) --- docs-src/api-footer.md | 6 ++ docs/api.md | 6 ++ docs/selectors.md | 74 ++++++++++++++++++++++-- src/server/common/selectorParser.ts | 2 +- src/server/injected/selectorEvaluator.ts | 11 ---- test/selectors-misc.spec.ts | 24 -------- 6 files changed, 83 insertions(+), 40 deletions(-) diff --git a/docs-src/api-footer.md b/docs-src/api-footer.md index 6ba2fef88e0bc..7b6b2939740a2 100644 --- a/docs-src/api-footer.md +++ b/docs-src/api-footer.md @@ -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`; diff --git a/docs/api.md b/docs/api.md index 096f4cce0e944..5b26891402f04 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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`; diff --git a/docs/selectors.md b/docs/selectors.md index ff6d6c6e6d8e7..41f369a6641aa 100644 --- a/docs/selectors.md +++ b/docs/selectors.md @@ -10,7 +10,7 @@ Selectors query elements on the web page for interactions, like [page.click](api ## 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] @@ -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
@@ -171,6 +175,68 @@ Note that `` 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 `
  • Deep in the shadow
  • `, 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`. diff --git a/src/server/common/selectorParser.ts b/src/server/common/selectorParser.ts index 029618ab46987..c7f581a3e9804 100644 --- a/src/server/common/selectorParser.ts +++ b/src/server/common/selectorParser.ts @@ -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): ParsedSelector { diff --git a/src/server/injected/selectorEvaluator.ts b/src/server/injected/selectorEvaluator.ts index 192f7b6c1a1b7..7ebe2e6f83852 100644 --- a/src/server/injected/selectorEvaluator.ts +++ b/src/server/injected/selectorEvaluator.ts @@ -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); @@ -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) diff --git a/test/selectors-misc.spec.ts b/test/selectors-misc.spec.ts index bdb54dbf3742d..b8c6fd39bf38d 100644 --- a/test/selectors-misc.spec.ts +++ b/test/selectors-misc.spec.ts @@ -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(` -
    -
    -
    - -
    -
    - `); - 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.