Skip to content

Commit

Permalink
fix(text selector): make quoted selector match by text nodes (#5603)
Browse files Browse the repository at this point in the history
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"`.
  • Loading branch information
dgozman authored Feb 25, 2021
1 parent 8906ba3 commit 0102e08
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 15 deletions.
2 changes: 1 addition & 1 deletion docs/src/selectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`.

Expand Down
4 changes: 2 additions & 2 deletions src/server/injected/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down
10 changes: 6 additions & 4 deletions src/server/injected/selectorEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}

Expand Down
30 changes: 22 additions & 8 deletions test/selectors-text.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,19 +103,19 @@ 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}) => {
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.$(`: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>');
Expand Down Expand Up @@ -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);

Expand All @@ -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');
Expand Down Expand Up @@ -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}) => {
Expand Down

0 comments on commit 0102e08

Please sign in to comment.