Skip to content

Commit 7ecf252

Browse files
authored
feat(text selector): concat sibling text nodes when calculating text (#1969)
Text that is split into multiple text nodes now matches.
1 parent b60c006 commit 7ecf252

File tree

2 files changed

+60
-16
lines changed

2 files changed

+60
-16
lines changed

src/injected/textSelectorEngine.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,21 +86,33 @@ function queryInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean): E
8686
const shadowRoots: ShadowRoot[] = [];
8787
if (shadow && (root as Element).shadowRoot)
8888
shadowRoots.push((root as Element).shadowRoot!);
89-
while (walker.nextNode()) {
90-
const node = walker.currentNode;
91-
if (node.nodeType === Node.ELEMENT_NODE) {
89+
90+
let lastTextParent: Element | null = null;
91+
let lastText = '';
92+
while (true) {
93+
const node = walker.nextNode();
94+
95+
const textParent = (node && node.nodeType === Node.TEXT_NODE) ? node.parentElement : null;
96+
if (lastTextParent && textParent !== lastTextParent) {
97+
if (lastTextParent.nodeName !== 'SCRIPT' && lastTextParent.nodeName !== 'STYLE' && matcher(lastText))
98+
return lastTextParent;
99+
lastText = '';
100+
}
101+
lastTextParent = textParent;
102+
103+
if (!node)
104+
break;
105+
if (node.nodeType === Node.TEXT_NODE) {
106+
lastText += node.nodeValue;
107+
} else {
92108
const element = node as Element;
93109
if ((element instanceof HTMLInputElement) && (element.type === 'submit' || element.type === 'button') && matcher(element.value))
94110
return element;
95111
if (shadow && element.shadowRoot)
96112
shadowRoots.push(element.shadowRoot);
97-
} else {
98-
const element = node.parentElement;
99-
const text = node.nodeValue;
100-
if (element && element.nodeName !== 'SCRIPT' && element.nodeName !== 'STYLE' && text && matcher(text))
101-
return element;
102113
}
103114
}
115+
104116
for (const shadowRoot of shadowRoots) {
105117
const element = queryInternal(shadowRoot, matcher, shadow);
106118
if (element)
@@ -114,21 +126,33 @@ function queryAllInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean,
114126
const shadowRoots: ShadowRoot[] = [];
115127
if (shadow && (root as Element).shadowRoot)
116128
shadowRoots.push((root as Element).shadowRoot!);
117-
while (walker.nextNode()) {
118-
const node = walker.currentNode;
119-
if (node.nodeType === Node.ELEMENT_NODE) {
129+
130+
let lastTextParent: Element | null = null;
131+
let lastText = '';
132+
while (true) {
133+
const node = walker.nextNode();
134+
135+
const textParent = (node && node.nodeType === Node.TEXT_NODE) ? node.parentElement : null;
136+
if (lastTextParent && textParent !== lastTextParent) {
137+
if (lastTextParent.nodeName !== 'SCRIPT' && lastTextParent.nodeName !== 'STYLE' && matcher(lastText))
138+
result.push(lastTextParent);
139+
lastText = '';
140+
}
141+
lastTextParent = textParent;
142+
143+
if (!node)
144+
break;
145+
if (node.nodeType === Node.TEXT_NODE) {
146+
lastText += node.nodeValue;
147+
} else {
120148
const element = node as Element;
121149
if ((element instanceof HTMLInputElement) && (element.type === 'submit' || element.type === 'button') && matcher(element.value))
122150
result.push(element);
123151
if (shadow && element.shadowRoot)
124152
shadowRoots.push(element.shadowRoot);
125-
} else {
126-
const element = node.parentElement;
127-
const text = node.nodeValue;
128-
if (element && element.nodeName !== 'SCRIPT' && element.nodeName !== 'STYLE' && text && matcher(text))
129-
result.push(element);
130153
}
131154
}
155+
132156
for (const shadowRoot of shadowRoots)
133157
queryAllInternal(shadowRoot, matcher, shadow, result);
134158
}

test/queryselector.spec.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,26 @@ describe('text selector', () => {
540540
await page.setContent(`<div> ' </div><div> " </div>`);
541541
expect(await page.$eval(`text="`, e => e.outerHTML)).toBe('<div> " </div>');
542542
expect(await page.$eval(`text='`, e => e.outerHTML)).toBe('<div> \' </div>');
543+
544+
await page.setContent(`<div>a<br>b</div><div>a</div>`);
545+
expect(await page.$eval(`text=a`, e => e.outerHTML)).toBe('<div>a<br>b</div>');
546+
expect(await page.$eval(`text=b`, e => e.outerHTML)).toBe('<div>a<br>b</div>');
547+
expect(await page.$(`text=ab`)).toBe(null);
548+
expect(await page.$$eval(`text=a`, els => els.length)).toBe(2);
549+
expect(await page.$$eval(`text=b`, els => els.length)).toBe(1);
550+
expect(await page.$$eval(`text=ab`, els => els.length)).toBe(0);
551+
552+
await page.setContent(`<div></div><span></span>`);
553+
await page.$eval('div', div => {
554+
div.appendChild(document.createTextNode('hello'));
555+
div.appendChild(document.createTextNode('world'));
556+
});
557+
await page.$eval('span', span => {
558+
span.appendChild(document.createTextNode('hello'));
559+
span.appendChild(document.createTextNode('world'));
560+
});
561+
expect(await page.$eval(`text=lowo`, e => e.outerHTML)).toBe('<div>helloworld</div>');
562+
expect(await page.$$eval(`text=lowo`, els => els.map(e => e.outerHTML).join(''))).toBe('<div>helloworld</div><span>helloworld</span>');
543563
});
544564

545565
it('create', async ({page}) => {

0 commit comments

Comments
 (0)