mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-06 03:16:17 +03:00
feat(selectors): update new text selector (#4654)
We now default to `text` that does substring case-insensitive match with normalized whitespace. `text-is` matches the whole string. `matches-text` is renamed to `text-matches`.
This commit is contained in:
parent
aacd8e633c
commit
b67e022111
@ -194,14 +194,11 @@ Use `:visible` with caution, because it has two major drawbacks:
|
||||
|
||||
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.
|
||||
* `: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-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.
|
||||
* `: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).
|
||||
* `: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).
|
||||
* `:text-matches("value", "i")` - Matches text against a regular expression with specified flags.
|
||||
|
||||
```js
|
||||
// Click a button with text "Sign in".
|
||||
|
@ -35,7 +35,7 @@ export function selectorsV2Enabled() {
|
||||
}
|
||||
|
||||
export function selectorsV2EngineNames() {
|
||||
return ['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'matches-text', 'above', 'below', 'right-of', 'left-of', 'near', 'within'];
|
||||
return ['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text-matches', 'text-is', 'above', 'below', 'right-of', 'left-of', 'near', 'within'];
|
||||
}
|
||||
|
||||
export function parseSelector(selector: string, customNames: Set<string>): ParsedSelector {
|
||||
@ -128,18 +128,22 @@ function textSelectorToSimple(selector: string): CSSSimpleSelector {
|
||||
return r.join('');
|
||||
}
|
||||
|
||||
let functionName = 'text';
|
||||
function escapeRegExp(s: string) {
|
||||
return s.replace(/[.*+\?^${}()|[\]\\]/g, '\\$&').replace(/-/g, '\\x2d');
|
||||
}
|
||||
|
||||
let functionName = 'text-matches';
|
||||
let args: string[];
|
||||
if (selector.length > 1 && selector[0] === '"' && selector[selector.length - 1] === '"') {
|
||||
args = [unescape(selector.substring(1, selector.length - 1))];
|
||||
args = ['^' + escapeRegExp(unescape(selector.substring(1, selector.length - 1))) + '$'];
|
||||
} else if (selector.length > 1 && selector[0] === "'" && selector[selector.length - 1] === "'") {
|
||||
args = [unescape(selector.substring(1, selector.length - 1))];
|
||||
args = ['^' + escapeRegExp(unescape(selector.substring(1, selector.length - 1))) + '$'];
|
||||
} else if (selector[0] === '/' && selector.lastIndexOf('/') > 0) {
|
||||
functionName = 'matches-text';
|
||||
const lastSlash = selector.lastIndexOf('/');
|
||||
args = [selector.substring(1, lastSlash), selector.substring(lastSlash + 1)];
|
||||
} else {
|
||||
args = [selector, 'sgi'];
|
||||
functionName = 'text';
|
||||
args = [selector];
|
||||
}
|
||||
return callWith(functionName, args);
|
||||
}
|
||||
|
@ -55,7 +55,8 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
|
||||
this._engines.set('light', lightEngine);
|
||||
this._engines.set('visible', visibleEngine);
|
||||
this._engines.set('text', textEngine);
|
||||
this._engines.set('matches-text', matchesTextEngine);
|
||||
this._engines.set('text-is', textIsEngine);
|
||||
this._engines.set('text-matches', textMatchesEngine);
|
||||
this._engines.set('xpath', xpathEngine);
|
||||
for (const attr of ['id', 'data-testid', 'data-test-id', 'data-test'])
|
||||
this._engines.set(attr, createAttributeEngine(attr));
|
||||
@ -362,37 +363,35 @@ const visibleEngine: SelectorEngine = {
|
||||
|
||||
const textEngine: SelectorEngine = {
|
||||
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
||||
if (args.length === 0 || typeof args[0] !== 'string' || args.length > 2 || (args.length === 2 && typeof args[1] !== 'string'))
|
||||
throw new Error(`"text" engine expects a string and an optional flags string`);
|
||||
const text = args[0];
|
||||
const flags = args.length === 2 ? args[1] : '';
|
||||
const matcher = textMatcher(text, flags);
|
||||
return elementMatchesText(element, context, matcher);
|
||||
if (args.length === 0 || typeof args[0] !== 'string')
|
||||
throw new Error(`"text" engine expects a single string`);
|
||||
return elementMatchesText(element, context, textMatcher(args[0], true));
|
||||
},
|
||||
};
|
||||
|
||||
const matchesTextEngine: SelectorEngine = {
|
||||
const textIsEngine: SelectorEngine = {
|
||||
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
||||
if (args.length === 0 || typeof args[0] !== 'string')
|
||||
throw new Error(`"text-is" engine expects a single string`);
|
||||
return elementMatchesText(element, context, textMatcher(args[0], false));
|
||||
},
|
||||
};
|
||||
|
||||
const textMatchesEngine: SelectorEngine = {
|
||||
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
||||
if (args.length === 0 || typeof args[0] !== 'string' || args.length > 2 || (args.length === 2 && typeof args[1] !== 'string'))
|
||||
throw new Error(`"matches-text" engine expects a regexp body and optional regexp flags`);
|
||||
throw new Error(`"text-matches" engine expects a regexp body and optional regexp flags`);
|
||||
const re = new RegExp(args[0], args.length === 2 ? args[1] : undefined);
|
||||
return elementMatchesText(element, context, s => re.test(s));
|
||||
},
|
||||
};
|
||||
|
||||
function textMatcher(text: string, flags: string): (s: string) => boolean {
|
||||
const normalizeSpace = flags.includes('s');
|
||||
const lowerCase = flags.includes('i');
|
||||
const substring = flags.includes('g');
|
||||
if (normalizeSpace)
|
||||
text = text.trim().replace(/\s+/g, ' ');
|
||||
if (lowerCase)
|
||||
text = text.toLowerCase();
|
||||
function textMatcher(text: string, substring: boolean): (s: string) => boolean {
|
||||
text = text.trim().replace(/\s+/g, ' ');
|
||||
text = text.toLowerCase();
|
||||
return (s: string) => {
|
||||
if (normalizeSpace)
|
||||
s = s.trim().replace(/\s+/g, ' ');
|
||||
if (lowerCase)
|
||||
s = s.toLowerCase();
|
||||
s = s.trim().replace(/\s+/g, ' ');
|
||||
s = s.toLowerCase();
|
||||
return substring ? s.includes(text) : s === text;
|
||||
};
|
||||
}
|
||||
|
@ -16,8 +16,11 @@
|
||||
*/
|
||||
|
||||
import { it, expect } from './fixtures';
|
||||
import * as path from 'path';
|
||||
|
||||
it('query', async ({page}) => {
|
||||
const { selectorsV2Enabled } = require(path.join(__dirname, '..', 'lib', 'server', 'common', 'selectorParser'));
|
||||
|
||||
it('should work', async ({page}) => {
|
||||
await page.setContent(`<div>yo</div><div>ya</div><div>\nye </div>`);
|
||||
expect(await page.$eval(`text=ya`, e => e.outerHTML)).toBe('<div>ya</div>');
|
||||
expect(await page.$eval(`text="ya"`, e => e.outerHTML)).toBe('<div>ya</div>');
|
||||
@ -106,6 +109,24 @@ it('query', async ({page}) => {
|
||||
expect((await page.$$(`text="lo wo"`)).length).toBe(0);
|
||||
});
|
||||
|
||||
it('should work in v2', async ({page}) => {
|
||||
if (!selectorsV2Enabled())
|
||||
return; // Selectors v1 do not support this.
|
||||
await page.setContent(`<div>yo</div><div>ya</div><div>\nHELLO \n world </div>`);
|
||||
expect(await page.$eval(`:text("ya")`, e => e.outerHTML)).toBe('<div>ya</div>');
|
||||
expect(await page.$eval(`:text-is("ya")`, e => e.outerHTML)).toBe('<div>ya</div>');
|
||||
expect(await page.$eval(`:text("y")`, e => e.outerHTML)).toBe('<div>yo</div>');
|
||||
expect(await page.$(`:text-is("y")`)).toBe(null);
|
||||
expect(await page.$eval(`:text("hello world")`, e => e.outerHTML)).toBe('<div>\nHELLO \n world </div>');
|
||||
expect(await page.$eval(`:text-is("hello world")`, e => e.outerHTML)).toBe('<div>\nHELLO \n world </div>');
|
||||
expect(await page.$eval(`:text("lo wo")`, e => e.outerHTML)).toBe('<div>\nHELLO \n world </div>');
|
||||
expect(await page.$(`:text-is("lo wo")`)).toBe(null);
|
||||
expect(await page.$eval(`:text-matches("^[ay]+$")`, e => e.outerHTML)).toBe('<div>ya</div>');
|
||||
expect(await page.$eval(`:text-matches("y", "g")`, e => e.outerHTML)).toBe('<div>yo</div>');
|
||||
expect(await page.$eval(`:text-matches("Y", "i")`, e => e.outerHTML)).toBe('<div>yo</div>');
|
||||
expect(await page.$(`:text-matches("^y$")`)).toBe(null);
|
||||
});
|
||||
|
||||
it('should be case sensitive if quotes are specified', async ({page}) => {
|
||||
await page.setContent(`<div>yo</div><div>ya</div><div>\nye </div>`);
|
||||
expect(await page.$eval(`text=yA`, e => e.outerHTML)).toBe('<div>ya</div>');
|
||||
|
Loading…
Reference in New Issue
Block a user