mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
fix: sort elements based on selector matching algorithm (#9836)
This commit is contained in:
parent
8aea8e0471
commit
9044609be3
@ -170,6 +170,67 @@ class PQueryEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DepthCalculator {
|
||||||
|
#cache = new Map<Node, number[]>();
|
||||||
|
|
||||||
|
calculate(node: Node, depth: number[] = []): number[] {
|
||||||
|
if (node instanceof Document) {
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
if (node instanceof ShadowRoot) {
|
||||||
|
node = node.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedDepth = this.#cache.get(node);
|
||||||
|
if (cachedDepth) {
|
||||||
|
return [...cachedDepth, ...depth];
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
for (
|
||||||
|
let prevSibling = node.previousSibling;
|
||||||
|
prevSibling;
|
||||||
|
prevSibling = prevSibling.previousSibling
|
||||||
|
) {
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = this.calculate(node.parentNode as Node, [index]);
|
||||||
|
this.#cache.set(node, value);
|
||||||
|
return [...value, ...depth];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareDepths = (a: number[], b: number[]): -1 | 0 | 1 => {
|
||||||
|
if (a.length + b.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const [i = Infinity, ...otherA] = a;
|
||||||
|
const [j = Infinity, ...otherB] = b;
|
||||||
|
if (i === j) {
|
||||||
|
return compareDepths(otherA, otherB);
|
||||||
|
}
|
||||||
|
return i < j ? 1 : -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const domSort = async function* (elements: AwaitableIterable<Node>) {
|
||||||
|
const results = new Set<Node>();
|
||||||
|
for await (const element of elements) {
|
||||||
|
results.add(element);
|
||||||
|
}
|
||||||
|
const calculator = new DepthCalculator();
|
||||||
|
yield* [...results.values()]
|
||||||
|
.map(result => {
|
||||||
|
return [result, calculator.calculate(result)] as const;
|
||||||
|
})
|
||||||
|
.sort(([, a], [, b]) => {
|
||||||
|
return compareDepths(a, b);
|
||||||
|
})
|
||||||
|
.map(([result]) => {
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
type QueryableNode = {
|
type QueryableNode = {
|
||||||
querySelectorAll: typeof Document.prototype.querySelectorAll;
|
querySelectorAll: typeof Document.prototype.querySelectorAll;
|
||||||
};
|
};
|
||||||
@ -179,7 +240,7 @@ type QueryableNode = {
|
|||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const pQuerySelectorAll = async function* (
|
export const pQuerySelectorAll = function (
|
||||||
root: Node,
|
root: Node,
|
||||||
selector: string
|
selector: string
|
||||||
): AwaitableIterable<Node> {
|
): AwaitableIterable<Node> {
|
||||||
@ -195,10 +256,8 @@ export const pQuerySelectorAll = async function* (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isPureCSS) {
|
if (isPureCSS) {
|
||||||
yield* (root as unknown as QueryableNode).querySelectorAll(selector);
|
return (root as unknown as QueryableNode).querySelectorAll(selector);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are any empty elements, then this implies the selector has
|
// If there are any empty elements, then this implies the selector has
|
||||||
// contiguous combinators (e.g. `>>> >>>>`) or starts/ends with one which we
|
// contiguous combinators (e.g. `>>> >>>>`) or starts/ends with one which we
|
||||||
// treat as illegal, similar to existing behavior.
|
// treat as illegal, similar to existing behavior.
|
||||||
@ -221,11 +280,13 @@ export const pQuerySelectorAll = async function* (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const selectorParts of selectors) {
|
return domSort(
|
||||||
const query = new PQueryEngine(root, selector, selectorParts);
|
AsyncIterableUtil.flatMap(selectors, selectorParts => {
|
||||||
query.run();
|
const query = new PQueryEngine(root, selector, selectorParts);
|
||||||
yield* query.elements;
|
query.run();
|
||||||
}
|
return query.elements;
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,10 +28,10 @@ export class AsyncIterableUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async *flatMap<T>(
|
static async *flatMap<T, U>(
|
||||||
iterable: AwaitableIterable<T>,
|
iterable: AwaitableIterable<T>,
|
||||||
map: (item: T) => AwaitableIterable<T>
|
map: (item: T) => AwaitableIterable<U>
|
||||||
): AsyncIterable<T> {
|
): AsyncIterable<U> {
|
||||||
for await (const value of iterable) {
|
for await (const value of iterable) {
|
||||||
yield* map(value);
|
yield* map(value);
|
||||||
}
|
}
|
||||||
|
@ -359,7 +359,9 @@ describe('Query handler tests', function () {
|
|||||||
describe('P selectors', () => {
|
describe('P selectors', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
await page.setContent('<div>hello <button>world</button></div>');
|
await page.setContent(
|
||||||
|
'<div>hello <button>world<span></span></button></div>'
|
||||||
|
);
|
||||||
Puppeteer.clearCustomQueryHandlers();
|
Puppeteer.clearCustomQueryHandlers();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -489,10 +491,60 @@ describe('Query handler tests', function () {
|
|||||||
expect(value).toMatchObject({textContent: 'world', tagName: 'BUTTON'});
|
expect(value).toMatchObject({textContent: 'world', tagName: 'BUTTON'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with commas', 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(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const permute = <T>(inputs: T[]): T[][] => {
|
||||||
|
const results: T[][] = [];
|
||||||
|
for (let i = 0; i < inputs.length; ++i) {
|
||||||
|
const permutation = permute(
|
||||||
|
inputs.slice(0, i).concat(inputs.slice(i + 1))
|
||||||
|
);
|
||||||
|
const value = inputs[i] as T;
|
||||||
|
if (permutation.length === 0) {
|
||||||
|
results.push([value]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const part of permutation) {
|
||||||
|
results.push([value].concat(part));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should match querySelector* ordering', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
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.$$(
|
||||||
|
list
|
||||||
|
.map(selector => {
|
||||||
|
return selector === 'button' ? '::-p-text(world)' : selector;
|
||||||
|
})
|
||||||
|
.join(',')
|
||||||
|
);
|
||||||
|
const actual = await Promise.all(
|
||||||
|
elements.map(element => {
|
||||||
|
return element.evaluate(element => {
|
||||||
|
return element.tagName;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(actual.join()).toStrictEqual(expected.join());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have duplicate elements from selector lists', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
const elements = await page.$$('::-p-text(world), button');
|
||||||
|
expect(elements.length).toStrictEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user