chore: update deep implementation for P selectors (#9908)

This commit is contained in:
jrandolf 2023-03-23 15:46:33 +01:00 committed by GitHub
parent f6ef167b0f
commit 6c018acf5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 114 additions and 85 deletions

View File

@ -29,11 +29,19 @@ import {
PPseudoSelector, PPseudoSelector,
} from './PSelectorParser.js'; } from './PSelectorParser.js';
import {textQuerySelectorAll} from './TextQuerySelector.js'; import {textQuerySelectorAll} from './TextQuerySelector.js';
import {deepChildren, deepDescendents} from './util.js'; import {pierce, pierceAll} from './util.js';
import {xpathQuerySelectorAll} from './XPathQuerySelector.js'; import {xpathQuerySelectorAll} from './XPathQuerySelector.js';
const IDENT_TOKEN_START = /[-\w\P{ASCII}*]/; const IDENT_TOKEN_START = /[-\w\P{ASCII}*]/;
interface QueryableNode extends Node {
querySelectorAll: typeof Document.prototype.querySelectorAll;
}
const isQueryableNode = (node: Node): node is QueryableNode => {
return 'querySelectorAll' in node;
};
class SelectorError extends Error { class SelectorError extends Error {
constructor(selector: string, message: string) { constructor(selector: string, message: string) {
super(`${selector} is not a valid selector: ${message}`); super(`${selector} is not a valid selector: ${message}`);
@ -75,18 +83,28 @@ class PQueryEngine {
const selector = this.#selector; const selector = this.#selector;
const input = this.#input; const input = this.#input;
if (typeof selector === 'string') { if (typeof selector === 'string') {
this.elements = AsyncIterableUtil.flatMap(
this.elements,
async function* (element) {
if (!selector[0]) {
return;
}
// The regular expression tests if the selector is a type/universal // The regular expression tests if the selector is a type/universal
// selector. Any other case means we want to apply the selector onto // selector. Any other case means we want to apply the selector onto
// the element itself (e.g. `element.class`, `element>div`, // the element itself (e.g. `element.class`, `element>div`,
// `element:hover`, etc.). // `element:hover`, etc.).
if (IDENT_TOKEN_START.test(selector[0]) || !element.parentElement) { if (selector[0] && IDENT_TOKEN_START.test(selector[0])) {
yield* (element as Element).querySelectorAll(selector); this.elements = AsyncIterableUtil.flatMap(
this.elements,
async function* (element) {
if (isQueryableNode(element)) {
yield* element.querySelectorAll(selector);
}
}
);
} else {
this.elements = AsyncIterableUtil.flatMap(
this.elements,
async function* (element) {
if (!element.parentElement) {
if (!isQueryableNode(element)) {
return;
}
yield* element.querySelectorAll(selector);
return; return;
} }
@ -102,6 +120,7 @@ class PQueryEngine {
); );
} }
); );
}
} else { } else {
this.elements = AsyncIterableUtil.flatMap( this.elements = AsyncIterableUtil.flatMap(
this.elements, this.elements,
@ -144,22 +163,12 @@ class PQueryEngine {
const selector = this.#complexSelector.shift(); const selector = this.#complexSelector.shift();
switch (selector) { switch (selector) {
case PCombinator.Child: { case PCombinator.Child: {
this.elements = AsyncIterableUtil.flatMap( this.elements = AsyncIterableUtil.flatMap(this.elements, pierce);
this.elements,
function* (element) {
yield* deepChildren(element);
}
);
this.#next(); this.#next();
break; break;
} }
case PCombinator.Descendent: { case PCombinator.Descendent: {
this.elements = AsyncIterableUtil.flatMap( this.elements = AsyncIterableUtil.flatMap(this.elements, pierceAll);
this.elements,
function* (element) {
yield* deepDescendents(element);
}
);
this.#next(); this.#next();
break; break;
} }
@ -206,12 +215,12 @@ const compareDepths = (a: number[], b: number[]): -1 | 0 | 1 => {
if (a.length + b.length === 0) { if (a.length + b.length === 0) {
return 0; return 0;
} }
const [i = Infinity, ...otherA] = a; const [i = -1, ...otherA] = a;
const [j = Infinity, ...otherB] = b; const [j = -1, ...otherB] = b;
if (i === j) { if (i === j) {
return compareDepths(otherA, otherB); return compareDepths(otherA, otherB);
} }
return i < j ? 1 : -1; return i < j ? -1 : 1;
}; };
const domSort = async function* (elements: AwaitableIterable<Node>) { const domSort = async function* (elements: AwaitableIterable<Node>) {
@ -232,10 +241,6 @@ const domSort = async function* (elements: AwaitableIterable<Node>) {
}); });
}; };
type QueryableNode = {
querySelectorAll: typeof Document.prototype.querySelectorAll;
};
/** /**
* Queries the given node for all nodes matching the given text selector. * Queries the given node for all nodes matching the given text selector.
* *

View File

@ -30,41 +30,37 @@ function isBoundingBoxEmpty(element: Element): boolean {
return rect.width === 0 || rect.height === 0; return rect.width === 0 || rect.height === 0;
} }
const hasShadowRoot = (node: Node): node is Node & {shadowRoot: ShadowRoot} => {
return 'shadowRoot' in node && node.shadowRoot instanceof ShadowRoot;
};
/** /**
* @internal * @internal
*/ */
export function* deepChildren( export function* pierce(root: Node): IterableIterator<Node | ShadowRoot> {
root: Node if (hasShadowRoot(root)) {
): IterableIterator<Element | ShadowRoot> { yield root.shadowRoot;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); } else {
let node = walker.nextNode() as Element | null; yield root;
for (; node; node = walker.nextNode() as Element | null) {
yield node.shadowRoot ?? node;
} }
} }
/** /**
* @internal * @internal
*/ */
export function* deepDescendents( export function* pierceAll(root: Node): IterableIterator<Node | ShadowRoot> {
root: Node yield* pierce(root);
): IterableIterator<Element | ShadowRoot> {
const walkers = [document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)]; const walkers = [document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)];
let walker: TreeWalker | undefined; for (const walker of walkers) {
while ((walker = walkers.shift())) { let node: Element | null;
for ( while ((node = walker.nextNode() as Element | null)) {
let node = walker.nextNode() as Element | null;
node;
node = walker.nextNode() as Element | null
) {
if (!node.shadowRoot) { if (!node.shadowRoot) {
yield node;
continue; continue;
} }
yield node.shadowRoot;
walkers.push( walkers.push(
document.createTreeWalker(node.shadowRoot, NodeFilter.SHOW_ELEMENT) document.createTreeWalker(node.shadowRoot, NodeFilter.SHOW_ELEMENT)
); );
yield node.shadowRoot;
} }
} }
} }

View File

@ -0,0 +1,13 @@
<div id="a">hello <button id="b">world</button>
<span id="f"></span>
<div id="c">
<template shadowrootmode="open">
shadow dom
<div id="d">
<template shadowrootmode="open">
<a id="e">deep text</a>
</template>
</div>
</template>
</div>
</div>

View File

@ -358,10 +358,8 @@ describe('Query handler tests', function () {
describe('P selectors', () => { describe('P selectors', () => {
beforeEach(async () => { beforeEach(async () => {
const {page} = getTestState(); const {page, server} = getTestState();
await page.setContent( await page.goto(`${server.PREFIX}/p-selectors.html`);
'<div>hello <button>world<span></span></button></div>'
);
Puppeteer.clearCustomQueryHandlers(); Puppeteer.clearCustomQueryHandlers();
}); });
@ -371,7 +369,7 @@ describe('Query handler tests', function () {
assert(element, 'Could not find element'); assert(element, 'Could not find element');
expect( expect(
await element.evaluate(element => { await element.evaluate(element => {
return element.tagName === 'BUTTON'; return element.id === 'b';
}) })
).toBeTruthy(); ).toBeTruthy();
@ -386,13 +384,35 @@ describe('Query handler tests', function () {
} }
}); });
it('should work with deep combinators', async () => {
const {page} = getTestState();
{
const element = await page.$('div >>>> div');
assert(element, 'Could not find element');
expect(
await element.evaluate(element => {
return element.id === 'c';
})
).toBeTruthy();
}
{
const elements = await page.$$('div >>> div');
assert(elements[1], 'Could not find element');
expect(
await elements[1]?.evaluate(element => {
return element.id === 'd';
})
).toBeTruthy();
}
});
it('should work with text selectors', async () => { it('should work with text selectors', async () => {
const {page} = getTestState(); const {page} = getTestState();
const element = await page.$('div ::-p-text(world)'); const element = await page.$('div ::-p-text(world)');
assert(element, 'Could not find element'); assert(element, 'Could not find element');
expect( expect(
await element.evaluate(element => { await element.evaluate(element => {
return element.tagName === 'BUTTON'; return element.id === 'b';
}) })
).toBeTruthy(); ).toBeTruthy();
}); });
@ -403,7 +423,7 @@ describe('Query handler tests', function () {
assert(element, 'Could not find element'); assert(element, 'Could not find element');
expect( expect(
await element.evaluate(element => { await element.evaluate(element => {
return element.tagName === 'BUTTON'; return element.id === 'b';
}) })
).toBeTruthy(); ).toBeTruthy();
}); });
@ -414,7 +434,7 @@ describe('Query handler tests', function () {
assert(element, 'Could not find element'); assert(element, 'Could not find element');
expect( expect(
await element.evaluate(element => { await element.evaluate(element => {
return element.tagName === 'BUTTON'; return element.id === 'b';
}) })
).toBeTruthy(); ).toBeTruthy();
}); });
@ -431,7 +451,7 @@ describe('Query handler tests', function () {
assert(element, 'Could not find element'); assert(element, 'Could not find element');
expect( expect(
await element.evaluate(element => { await element.evaluate(element => {
return element.tagName === 'DIV'; return element.id === 'a';
}) })
).toBeTruthy(); ).toBeTruthy();
}); });
@ -453,7 +473,7 @@ describe('Query handler tests', function () {
assert(element, 'Could not find element'); assert(element, 'Could not find element');
expect( expect(
await element.evaluate(element => { await element.evaluate(element => {
return element.tagName === 'DIV'; return element.id === 'a';
}) })
).toBeTruthy(); ).toBeTruthy();
} }
@ -462,7 +482,7 @@ describe('Query handler tests', function () {
assert(element, 'Could not find element'); assert(element, 'Could not find element');
expect( expect(
await element.evaluate(element => { await element.evaluate(element => {
return element.tagName === 'DIV'; return element.id === 'a';
}) })
).toBeTruthy(); ).toBeTruthy();
} }
@ -471,7 +491,7 @@ describe('Query handler tests', function () {
assert(element, 'Could not find element'); assert(element, 'Could not find element');
expect( expect(
await element.evaluate(element => { await element.evaluate(element => {
return element.tagName === 'DIV'; return element.id === 'a';
}) })
).toBeTruthy(); ).toBeTruthy();
} }
@ -480,7 +500,7 @@ describe('Query handler tests', function () {
assert(element, 'Could not find element'); assert(element, 'Could not find element');
expect( expect(
await element.evaluate(element => { await element.evaluate(element => {
return element.tagName === 'BUTTON'; return element.id === 'b';
}) })
).toBeTruthy(); ).toBeTruthy();
} }
@ -504,7 +524,7 @@ describe('Query handler tests', function () {
it('should work with selector lists', async () => { it('should work with selector lists', async () => {
const {page} = getTestState(); const {page} = getTestState();
const elements = await page.$$('div, ::-p-text(world)'); const elements = await page.$$('div, ::-p-text(world)');
expect(elements.length).toStrictEqual(2); expect(elements.length).toStrictEqual(3);
}); });
const permute = <T>(inputs: T[]): T[][] => { const permute = <T>(inputs: T[]): T[][] => {
@ -528,11 +548,6 @@ describe('Query handler tests', function () {
it('should match querySelector* ordering', async () => { it('should match querySelector* ordering', async () => {
const {page} = getTestState(); const {page} = getTestState();
for (const list of permute(['div', 'button', 'span'])) { for (const list of permute(['div', 'button', 'span'])) {
const expected = await page.evaluate(selector => {
return [...document.querySelectorAll(selector)].map(element => {
return element.tagName;
});
}, list.join(','));
const elements = await page.$$( const elements = await page.$$(
list list
.map(selector => { .map(selector => {
@ -543,11 +558,11 @@ describe('Query handler tests', function () {
const actual = await Promise.all( const actual = await Promise.all(
elements.map(element => { elements.map(element => {
return element.evaluate(element => { return element.evaluate(element => {
return element.tagName; return element.id;
}); });
}) })
); );
expect(actual.join()).toStrictEqual(expected.join()); expect(actual.join()).toStrictEqual('a,b,f,c');
} }
}); });