chore: update deep implementation for P selectors (#9908)
This commit is contained in:
parent
f6ef167b0f
commit
6c018acf5d
@ -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,33 +83,44 @@ 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(
|
// The regular expression tests if the selector is a type/universal
|
||||||
this.elements,
|
// selector. Any other case means we want to apply the selector onto
|
||||||
async function* (element) {
|
// the element itself (e.g. `element.class`, `element>div`,
|
||||||
if (!selector[0]) {
|
// `element:hover`, etc.).
|
||||||
return;
|
if (selector[0] && IDENT_TOKEN_START.test(selector[0])) {
|
||||||
}
|
this.elements = AsyncIterableUtil.flatMap(
|
||||||
// The regular expression tests if the selector is a type/universal
|
this.elements,
|
||||||
// selector. Any other case means we want to apply the selector onto
|
async function* (element) {
|
||||||
// the element itself (e.g. `element.class`, `element>div`,
|
if (isQueryableNode(element)) {
|
||||||
// `element:hover`, etc.).
|
yield* element.querySelectorAll(selector);
|
||||||
if (IDENT_TOKEN_START.test(selector[0]) || !element.parentElement) {
|
|
||||||
yield* (element as Element).querySelectorAll(selector);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
for (const child of element.parentElement.children) {
|
|
||||||
++index;
|
|
||||||
if (child === element) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield* element.parentElement.querySelectorAll(
|
);
|
||||||
`:scope>:nth-child(${index})${selector}`
|
} else {
|
||||||
);
|
this.elements = AsyncIterableUtil.flatMap(
|
||||||
}
|
this.elements,
|
||||||
);
|
async function* (element) {
|
||||||
|
if (!element.parentElement) {
|
||||||
|
if (!isQueryableNode(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
yield* element.querySelectorAll(selector);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
for (const child of element.parentElement.children) {
|
||||||
|
++index;
|
||||||
|
if (child === element) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield* element.parentElement.querySelectorAll(
|
||||||
|
`:scope>:nth-child(${index})${selector}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
} 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.
|
||||||
*
|
*
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
test/assets/p-selectors.html
Normal file
13
test/assets/p-selectors.html
Normal 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>
|
@ -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');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user