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.
This commit is contained in:
Dmitry Gozman 2020-05-29 15:28:27 -07:00 committed by GitHub
parent 084d5ff48f
commit 8e4a1e7c67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 47 additions and 4 deletions

View File

@ -80,9 +80,24 @@ function createMatcher(selector: string): Matcher {
return text => text.toLowerCase().includes(selector);
}
// Skips <head>, <script> and <style> elements and all their children.
const nodeFilter: NodeFilter = {
acceptNode: node => {
return node.nodeName === 'HEAD' || node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE' ?
NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
}
};
// If we are querying inside a filtered element, nodeFilter is never called, so we need a separate check.
function isFilteredNode(root: SelectorRoot, document: Document) {
return root.nodeName === 'SCRIPT' || root.nodeName === 'STYLE' || document.head && document.head.contains(root);
}
function queryInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean): Element | undefined {
const document = root instanceof Document ? root : root.ownerDocument!;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
if (isFilteredNode(root, document))
return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, nodeFilter);
const shadowRoots: ShadowRoot[] = [];
if (shadow && (root as Element).shadowRoot)
shadowRoots.push((root as Element).shadowRoot!);
@ -94,7 +109,7 @@ function queryInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean): E
const textParent = (node && node.nodeType === Node.TEXT_NODE) ? node.parentElement : null;
if (lastTextParent && textParent !== lastTextParent) {
if (lastTextParent.nodeName !== 'SCRIPT' && lastTextParent.nodeName !== 'STYLE' && matcher(lastText))
if (matcher(lastText))
return lastTextParent;
lastText = '';
}
@ -122,7 +137,9 @@ function queryInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean): E
function queryAllInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean, result: Element[]) {
const document = root instanceof Document ? root : root.ownerDocument!;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
if (isFilteredNode(root, document))
return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, nodeFilter);
const shadowRoots: ShadowRoot[] = [];
if (shadow && (root as Element).shadowRoot)
shadowRoots.push((root as Element).shadowRoot!);
@ -134,7 +151,7 @@ function queryAllInternal(root: SelectorRoot, matcher: Matcher, shadow: boolean,
const textParent = (node && node.nodeType === Node.TEXT_NODE) ? node.parentElement : null;
if (lastTextParent && textParent !== lastTextParent) {
if (lastTextParent.nodeName !== 'SCRIPT' && lastTextParent.nodeName !== 'STYLE' && matcher(lastText))
if (matcher(lastText))
result.push(lastTextParent);
lastText = '';
}

View File

@ -551,6 +551,32 @@ describe('text selector', () => {
expect(await page.$(`text="with"`)).toBe(null);
});
it('should skip head, script and style', async({page}) => {
await page.setContent(`
<head>
<title>title</title>
<script>var script</script>
<style>.style {}</style>
</head>
<body>
<script>var script</script>
<style>.style {}</style>
<div>title script style</div>
</body>`);
const head = await page.$('head');
const title = await page.$('title');
const script = await page.$('body script');
const style = await page.$('body style');
for (const text of ['title', 'script', 'style']) {
expect(await page.$eval(`text=${text}`, e => e.nodeName)).toBe('DIV');
expect(await page.$$eval(`text=${text}`, els => els.map(e => e.nodeName).join('|'))).toBe('DIV');
for (const root of [head, title, script, style]) {
expect(await root.$(`text=${text}`)).toBe(null);
expect(await root.$$eval(`text=${text}`, els => els.length)).toBe(0);
}
}
});
it('should match input[type=button|submit]', async({page}) => {
await page.setContent(`<input type="submit" value="hello"><input type="button" value="world">`);
expect(await page.$eval(`text=hello`, e => e.outerHTML)).toBe('<input type="submit" value="hello">');