From 0586c2554f7bc0358aae113bcee6b1485a552bce Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 19 Jan 2021 09:30:34 -0800 Subject: [PATCH] feat(text selector): normalize whitespace for quoted match (#5049) This changes quoted text selector like `text="Foo Bar"` to perform normalized whitespace match. Most of the time users want to match some string visible on the page, and that always means normalized whitespace. We keep the case sensitivity and full-string vs substring difference between quoted and unquoted matches. --- docs/src/selectors.md | 11 ++++----- src/server/injected/textSelectorEngine.ts | 28 +++++++++++++++-------- test/selectors-text.spec.ts | 6 ++--- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/docs/src/selectors.md b/docs/src/selectors.md index 96d9b97c1d..b25ec5df34 100644 --- a/docs/src/selectors.md +++ b/docs/src/selectors.md @@ -25,13 +25,10 @@ await page.click("text=Log in") page.click("text=Log in") ``` -By default, the match is case-insensitive, it ignores leading/trailing whitespace and searches for -a substring. This means `text= Login` matches ``. +Matching is case-insensitive and searches for a substring. This means `text=Login` matches ``. Matching also normalizes whitespace, for example it turns multiple spaces into one, turns line breaks into spaces and ignores leading and trailing whitespace. -Text body can be escaped with single or double quotes for precise matching, insisting on exact match, -including specified whitespace and case. This means `text="Login "` will only match -`` with exactly one space after "Login". Quoted text follows the usual escaping -rules, e.g. use `\"` to escape double quote in a double-quoted string: `text="foo\"bar"`. +Text body can be escaped with single or double quotes for full-string case-sensitive match instead. This means `text="Login"` will match ``, but not `` or ``. Quoted text follows the usual escaping +rules, e.g. use `\"` to escape double quote in a double-quoted string: `text="foo\"bar"`. Note that quoted match still normalizes whitespace. Text body can also be a JavaScript-like regex wrapped in `/` symbols. This means `text=/^\\s*Login$/i` will match `` with any number of spaces before "Login" and no spaces after. @@ -156,7 +153,7 @@ The `:text` pseudo-class matches elements that have a text node child with speci It is similar to the [text] engine, but can be used in combination with other `css` selector extensions. There are a few variations that support different arguments: -* `:text("substring")` - Matches when element's text contains "substring" somewhere. Matching is case-insensitive. Matching also normalizes whitespace, for example it turns multiple spaces into one, trusn line breaks into spaces and ignores leading and trailing whitespace. +* `:text("substring")` - Matches when element's text contains "substring" somewhere. Matching is case-insensitive. Matching also normalizes whitespace, for example it turns multiple spaces into one, turns line breaks into spaces and ignores leading and trailing whitespace. * `:text-is("string")` - Matches when element's text equals the "string". Matching is case-insensitive and normalizes whitespace. * `button:text("Sign in")` - Text selector may be combined with regular CSS. * `:text-matches("[+-]?\\d+")` - Matches text against a regular expression. Note that special characters like back-slash `\`, quotes `"`, square brackets `[]` and more should be escaped. Learn more about [regular expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp). diff --git a/src/server/injected/textSelectorEngine.ts b/src/server/injected/textSelectorEngine.ts index 70cb80e86d..823e30de2e 100644 --- a/src/server/injected/textSelectorEngine.ts +++ b/src/server/injected/textSelectorEngine.ts @@ -46,21 +46,29 @@ function unescape(s: string): string { type Matcher = (text: string) => boolean; function createMatcher(selector: string): Matcher { - if (selector.length > 1 && selector[0] === '"' && selector[selector.length - 1] === '"') { - const parsed = unescape(selector.substring(1, selector.length - 1)); - return text => text === parsed; - } - if (selector.length > 1 && selector[0] === "'" && selector[selector.length - 1] === "'") { - const parsed = unescape(selector.substring(1, selector.length - 1)); - return text => text === parsed; - } if (selector[0] === '/' && selector.lastIndexOf('/') > 0) { const lastSlash = selector.lastIndexOf('/'); const re = new RegExp(selector.substring(1, lastSlash), selector.substring(lastSlash + 1)); return text => re.test(text); } - selector = selector.trim().toLowerCase().replace(/\s+/g, ' '); - return text => text.toLowerCase().replace(/\s+/g, ' ').includes(selector); + let strict = false; + if (selector.length > 1 && selector[0] === '"' && selector[selector.length - 1] === '"') { + selector = unescape(selector.substring(1, selector.length - 1)); + strict = true; + } + if (selector.length > 1 && selector[0] === "'" && selector[selector.length - 1] === "'") { + selector = unescape(selector.substring(1, selector.length - 1)); + strict = true; + } + selector = selector.trim().replace(/\s+/g, ' '); + if (!strict) + selector = selector.toLowerCase(); + return text => { + text = text.trim().replace(/\s+/g, ' '); + if (!strict) + return text.toLowerCase().includes(selector); + return text === selector; + }; } // Skips ,