Skip to content

Commit 8e4a1e7

Browse files
authored
fix(text selector): do not match text inside <head> (#2413)
We already skip <script> and <style> tags because they are not the page content. Similar reasoning applies to <head> that has content that is never rendered on the page.
1 parent 084d5ff commit 8e4a1e7

File tree

2 files changed

+47
-4
lines changed

2 files changed

+47
-4
lines changed

src/injected/textSelectorEngine.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,24 @@ function createMatcher(selector: string): Matcher {
8080
return text => text.toLowerCase().includes(selector);
8181
}
8282

83+
// Skips <head>, <script> and <style> elements and all their children.
84+
const nodeFilter: NodeFilter = {
85+
acceptNode: node => {
86+
return node.nodeName === 'HEAD' || node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE' ?
87+
NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
88+
}
89+
};
90+
91+
// If we are querying inside a filtered element, nodeFilter is never called, so we need a separate check.
92+
function isFilteredNode(root: SelectorRoot, document: Document) {
93+
return root.nodeName === 'SCRIPT' || root.nodeName === 'STYLE' || document.head && document.head.contains(root);
94+
}
95+
8396
function queryInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean): Element | undefined {
8497
const document = root instanceof Document ? root : root.ownerDocument!;
85-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
98+
if (isFilteredNode(root, document))
99+
return;
100+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, nodeFilter);
86101
const shadowRoots: ShadowRoot[] = [];
87102
if (shadow && (root as Element).shadowRoot)
88103
shadowRoots.push((root as Element).shadowRoot!);
@@ -94,7 +109,7 @@ function queryInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean): E
94109

95110
const textParent = (node && node.nodeType === Node.TEXT_NODE) ? node.parentElement : null;
96111
if (lastTextParent && textParent !== lastTextParent) {
97-
if (lastTextParent.nodeName !== 'SCRIPT' && lastTextParent.nodeName !== 'STYLE' && matcher(lastText))
112+
if (matcher(lastText))
98113
return lastTextParent;
99114
lastText = '';
100115
}
@@ -122,7 +137,9 @@ function queryInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean): E
122137

123138
function queryAllInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean, result: Element[]) {
124139
const document = root instanceof Document ? root : root.ownerDocument!;
125-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
140+
if (isFilteredNode(root, document))
141+
return;
142+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, nodeFilter);
126143
const shadowRoots: ShadowRoot[] = [];
127144
if (shadow && (root as Element).shadowRoot)
128145
shadowRoots.push((root as Element).shadowRoot!);
@@ -134,7 +151,7 @@ function queryAllInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean,
134151

135152
const textParent = (node && node.nodeType === Node.TEXT_NODE) ? node.parentElement : null;
136153
if (lastTextParent && textParent !== lastTextParent) {
137-
if (lastTextParent.nodeName !== 'SCRIPT' && lastTextParent.nodeName !== 'STYLE' && matcher(lastText))
154+
if (matcher(lastText))
138155
result.push(lastTextParent);
139156
lastText = '';
140157
}

test/queryselector.spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,32 @@ describe('text selector', () => {
551551
expect(await page.$(`text="with"`)).toBe(null);
552552
});
553553

554+
it('should skip head, script and style', async({page}) => {
555+
await page.setContent(`
556+
<head>
557+
<title>title</title>
558+
<script>var script</script>
559+
<style>.style {}</style>
560+
</head>
561+
<body>
562+
<script>var script</script>
563+
<style>.style {}</style>
564+
<div>title script style</div>
565+
</body>`);
566+
const head = await page.$('head');
567+
const title = await page.$('title');
568+
const script = await page.$('body script');
569+
const style = await page.$('body style');
570+
for (const text of ['title', 'script', 'style']) {
571+
expect(await page.$eval(`text=${text}`, e => e.nodeName)).toBe('DIV');
572+
expect(await page.$$eval(`text=${text}`, els => els.map(e => e.nodeName).join('|'))).toBe('DIV');
573+
for (const root of [head, title, script, style]) {
574+
expect(await root.$(`text=${text}`)).toBe(null);
575+
expect(await root.$$eval(`text=${text}`, els => els.length)).toBe(0);
576+
}
577+
}
578+
});
579+
554580
it('should match input[type=button|submit]', async({page}) => {
555581
await page.setContent(`<input type="submit" value="hello"><input type="button" value="world">`);
556582
expect(await page.$eval(`text=hello`, e => e.outerHTML)).toBe('<input type="submit" value="hello">');

0 commit comments

Comments
 (0)