fix(text selector): make quoted selector match by text nodes (#5603)

This change turns quoted match to be case-sensitive (as before),
but not strictly full-string for the whole element's text.

This is a fix for a case where element contains text nodes and child elements:
```html
<div>text1<span>child node</span>text2</div>
```
We now match this div by `text="text1"` and `text="text2"`.
This commit is contained in:
Dmitry Gozman 2021-02-24 16:32:38 -08:00 committed by GitHub
parent 8906ba332c
commit 0102e080f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 31 additions and 15 deletions

View File

@ -148,7 +148,7 @@ Text selector has a few variations:
page.click("text=Log in")
```
- `text="Log in"` - text body can be escaped with single or double quotes for full-string case-sensitive match. For example `text="Log"` does not match `<button>Log in</button>` but instead matches `<span>Log</span>`.
- `text="Log in"` - text body can be escaped with single or double quotes for case-sensitive match. For example `text="Log"` does not match `<button>log in</button>` but instead matches `<span>Log in</span>`.
Quoted body follows the usual escaping rules, e.g. use `\"` to escape double quote in a double-quoted string: `text="foo\"bar"`.

View File

@ -780,8 +780,8 @@ function createTextMatcher(selector: string): { matcher: Matcher, strict: boolea
const matcher = (text: string) => {
text = text.trim().replace(/\s+/g, ' ');
if (!strict)
return text.toLowerCase().includes(selector);
return text === selector;
text = text.toLowerCase();
return text.includes(selector);
};
return { matcher, strict };
}

View File

@ -462,13 +462,15 @@ const hasTextEngine: SelectorEngine = {
},
};
function textMatcher(text: string, substring: boolean): (s: string) => boolean {
function textMatcher(text: string, caseInsensitive: boolean): (s: string) => boolean {
text = text.trim().replace(/\s+/g, ' ');
text = text.toLowerCase();
if (caseInsensitive)
text = text.toLowerCase();
return (s: string) => {
s = s.trim().replace(/\s+/g, ' ');
s = s.toLowerCase();
return substring ? s.includes(text) : s === text;
if (caseInsensitive)
s = s.toLowerCase();
return s.includes(text);
};
}

View File

@ -103,9 +103,9 @@ it('should work', async ({page}) => {
expect((await page.$$(`text="Sign in"`)).length).toBe(1);
expect(await page.$eval(`text=lo wo`, e => e.outerHTML)).toBe('<span>Hello\n \nworld</span>');
expect(await page.$eval(`text="Hello world"`, e => e.outerHTML)).toBe('<span>Hello\n \nworld</span>');
expect(await page.$(`text="lo wo"`)).toBe(null);
expect(await page.$eval(`text="lo wo"`, e => e.outerHTML)).toBe('<span>Hello\n \nworld</span>');
expect((await page.$$(`text=lo \nwo`)).length).toBe(1);
expect((await page.$$(`text="lo wo"`)).length).toBe(0);
expect((await page.$$(`text="lo \nwo"`)).length).toBe(1);
});
it('should work with :text', async ({page}) => {
@ -113,9 +113,9 @@ it('should work with :text', async ({page}) => {
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.$(`: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-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>');
@ -145,11 +145,11 @@ it('should work across nodes', async ({page}) => {
expect(await page.$(`text=hello world`)).toBe(null);
expect(await page.$eval(`:text-is("Hello, world!")`, e => e.id)).toBe('target1');
expect(await page.$(`:text-is("Hello")`)).toBe(null);
expect(await page.$eval(`:text-is("Hello")`, e => e.id)).toBe('target1');
expect(await page.$eval(`:text-is("world")`, e => e.id)).toBe('target2');
expect(await page.$$eval(`:text-is("world")`, els => els.length)).toBe(1);
expect(await page.$eval(`text="Hello, world!"`, e => e.id)).toBe('target1');
expect(await page.$(`text="Hello"`)).toBe(null);
expect(await page.$eval(`text="Hello"`, e => e.id)).toBe('target1');
expect(await page.$eval(`text="world"`, e => e.id)).toBe('target2');
expect(await page.$$eval(`text="world"`, els => els.length)).toBe(1);
@ -162,6 +162,20 @@ it('should work across nodes', async ({page}) => {
expect(await page.$$eval(`text=/world/`, els => els.length)).toBe(1);
});
it('should work with text nodes in quoted mode', async ({page}) => {
await page.setContent(`<div id=target1>Hello<span id=target2>wo rld </span> Hi again </div>`);
expect(await page.$eval(`text="Hello"`, e => e.id)).toBe('target1');
expect(await page.$eval(`text="Hi again"`, e => e.id)).toBe('target1');
expect(await page.$eval(`text="wo rld"`, e => e.id)).toBe('target2');
expect(await page.$eval(`text="Hellowo rld Hi again"`, e => e.id)).toBe('target1');
expect(await page.$eval(`text="Hellowo"`, e => e.id)).toBe('target1');
expect(await page.$eval(`text="Hellowo rld"`, e => e.id)).toBe('target1');
expect(await page.$eval(`text="wo rld Hi ag"`, e => e.id)).toBe('target1');
expect(await page.$eval(`text="again"`, e => e.id)).toBe('target1');
expect(await page.$(`text="hi again"`)).toBe(null);
expect(await page.$eval(`text=hi again`, e => e.id)).toBe('target1');
});
it('should clear caches', async ({page}) => {
await page.setContent(`<div id=target1>text</div><div id=target2>text</div>`);
const div = await page.$('#target1');
@ -277,10 +291,10 @@ it('should be case sensitive if quotes are specified', async ({page}) => {
expect(await page.$(`text="yA"`)).toBe(null);
});
it('should search for a substring without quotes', async ({page}) => {
it('should search for a substring', async ({page}) => {
await page.setContent(`<div>textwithsubstring</div>`);
expect(await page.$eval(`text=with`, e => e.outerHTML)).toBe('<div>textwithsubstring</div>');
expect(await page.$(`text="with"`)).toBe(null);
expect(await page.$eval(`text="with"`, e => e.outerHTML)).toBe('<div>textwithsubstring</div>');
});
it('should skip head, script and style', async ({page}) => {