fix: waitForSelector should work for pseudo classes (#12545)

Co-authored-by: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com>
This commit is contained in:
Alex Rudenko 2024-06-10 14:31:07 +02:00 committed by GitHub
parent 80bbd76626
commit 0b2999f7b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 60 additions and 19 deletions

View File

@ -9,6 +9,7 @@ import type {Protocol} from 'devtools-protocol';
import type {Frame} from '../api/Frame.js'; import type {Frame} from '../api/Frame.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js'; import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
import {LazyArg} from '../common/LazyArg.js'; import {LazyArg} from '../common/LazyArg.js';
import {PollingOptions} from '../common/QueryHandler.js';
import type { import type {
AwaitableIterable, AwaitableIterable,
ElementFor, ElementFor,
@ -534,13 +535,12 @@ export abstract class ElementHandle<
selector: Selector, selector: Selector,
options: WaitForSelectorOptions = {} options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> { ): Promise<ElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, QueryHandler} = const {updatedSelector, QueryHandler, selectorHasPseudoClasses} =
getQueryHandlerAndSelector(selector); getQueryHandlerAndSelector(selector);
return (await QueryHandler.waitFor( return (await QueryHandler.waitFor(this, updatedSelector, {
this, polling: selectorHasPseudoClasses ? PollingOptions.RAF : undefined,
updatedSelector, ...options,
options })) as ElementHandle<NodeFor<Selector>> | null;
)) as ElementHandle<NodeFor<Selector>> | null;
} }
async #checkVisibility(visibility: boolean): Promise<boolean> { async #checkVisibility(visibility: boolean): Promise<boolean> {

View File

@ -18,6 +18,7 @@ import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js'; import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js'; import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
import {transposeIterableHandle} from '../common/HandleIterator.js'; import {transposeIterableHandle} from '../common/HandleIterator.js';
import {PollingOptions} from '../common/QueryHandler.js';
import type { import type {
Awaitable, Awaitable,
EvaluateFunc, EvaluateFunc,
@ -716,13 +717,12 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
selector: Selector, selector: Selector,
options: WaitForSelectorOptions = {} options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> { ): Promise<ElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, QueryHandler} = const {updatedSelector, QueryHandler, selectorHasPseudoClasses} =
getQueryHandlerAndSelector(selector); getQueryHandlerAndSelector(selector);
return (await QueryHandler.waitFor( return (await QueryHandler.waitFor(this, updatedSelector, {
this, polling: selectorHasPseudoClasses ? PollingOptions.RAF : undefined,
updatedSelector, ...options,
options })) as ElementHandle<NodeFor<Selector>> | null;
)) as ElementHandle<NodeFor<Selector>> | null;
} }
/** /**

View File

@ -29,6 +29,7 @@ const QUERY_SEPARATORS = ['=', '/'];
*/ */
export function getQueryHandlerAndSelector(selector: string): { export function getQueryHandlerAndSelector(selector: string): {
updatedSelector: string; updatedSelector: string;
selectorHasPseudoClasses: boolean;
QueryHandler: typeof QueryHandler; QueryHandler: typeof QueryHandler;
} { } {
for (const handlerMap of [ for (const handlerMap of [
@ -42,20 +43,26 @@ export function getQueryHandlerAndSelector(selector: string): {
const prefix = `${name}${separator}`; const prefix = `${name}${separator}`;
if (selector.startsWith(prefix)) { if (selector.startsWith(prefix)) {
selector = selector.slice(prefix.length); selector = selector.slice(prefix.length);
return {updatedSelector: selector, QueryHandler}; return {
updatedSelector: selector,
selectorHasPseudoClasses: false,
QueryHandler,
};
} }
} }
} }
} }
const [pSelector, isPureCSS] = parsePSelectors(selector); const [pSelector, isPureCSS, hasPseudoClasses] = parsePSelectors(selector);
if (isPureCSS) { if (isPureCSS) {
return { return {
updatedSelector: selector, updatedSelector: selector,
selectorHasPseudoClasses: hasPseudoClasses,
QueryHandler: CSSQueryHandler, QueryHandler: CSSQueryHandler,
}; };
} }
return { return {
updatedSelector: JSON.stringify(pSelector), updatedSelector: JSON.stringify(pSelector),
selectorHasPseudoClasses: hasPseudoClasses,
QueryHandler: PQueryHandler, QueryHandler: PQueryHandler,
}; };
} }

View File

@ -37,11 +37,16 @@ const unquote = (text: string): string => {
*/ */
export function parsePSelectors( export function parsePSelectors(
selector: string selector: string
): [selector: ComplexPSelectorList, isPureCSS: boolean] { ): [
selector: ComplexPSelectorList,
isPureCSS: boolean,
hasPseudoClasses: boolean,
] {
let isPureCSS = true; let isPureCSS = true;
let hasPseudoClasses = false;
const tokens = tokenize(selector); const tokens = tokenize(selector);
if (tokens.length === 0) { if (tokens.length === 0) {
return [[], isPureCSS]; return [[], isPureCSS, hasPseudoClasses];
} }
let compoundSelector: CompoundPSelector = []; let compoundSelector: CompoundPSelector = [];
let complexSelector: ComplexPSelector = [compoundSelector]; let complexSelector: ComplexPSelector = [compoundSelector];
@ -87,6 +92,9 @@ export function parsePSelectors(
value: unquote(token.argument ?? ''), value: unquote(token.argument ?? ''),
}); });
continue; continue;
case 'pseudo-class':
hasPseudoClasses = true;
continue;
case 'comma': case 'comma':
if (storage.length) { if (storage.length) {
compoundSelector.push(stringify(storage)); compoundSelector.push(stringify(storage));
@ -102,5 +110,5 @@ export function parsePSelectors(
if (storage.length) { if (storage.length) {
compoundSelector.push(stringify(storage)); compoundSelector.push(stringify(storage));
} }
return [selectors, isPureCSS]; return [selectors, isPureCSS, hasPseudoClasses];
} }

View File

@ -34,6 +34,14 @@ export type QuerySelector = (
PuppeteerUtil: PuppeteerUtil PuppeteerUtil: PuppeteerUtil
) => Awaitable<Node | null>; ) => Awaitable<Node | null>;
/**
* @internal
*/
export const enum PollingOptions {
RAF = 'raf',
MUTATION = 'mutation',
}
/** /**
* @internal * @internal
*/ */
@ -139,7 +147,9 @@ export class QueryHandler {
static async waitFor( static async waitFor(
elementOrFrame: ElementHandle<Node> | Frame, elementOrFrame: ElementHandle<Node> | Frame,
selector: string, selector: string,
options: WaitForSelectorOptions options: WaitForSelectorOptions & {
polling?: PollingOptions;
}
): Promise<ElementHandle<Node> | null> { ): Promise<ElementHandle<Node> | null> {
let frame!: Frame; let frame!: Frame;
using element = await (async () => { using element = await (async () => {
@ -152,6 +162,9 @@ export class QueryHandler {
})(); })();
const {visible = false, hidden = false, timeout, signal} = options; const {visible = false, hidden = false, timeout, signal} = options;
const polling =
options.polling ??
(visible || hidden ? PollingOptions.RAF : PollingOptions.MUTATION);
try { try {
signal?.throwIfAborted(); signal?.throwIfAborted();
@ -169,7 +182,7 @@ export class QueryHandler {
return PuppeteerUtil.checkVisibility(node, visible); return PuppeteerUtil.checkVisibility(node, visible);
}, },
{ {
polling: visible || hidden ? 'raf' : 'mutation', polling,
root: element, root: element,
timeout, timeout,
signal, signal,

View File

@ -407,6 +407,19 @@ describe('waittask specs', function () {
await watchdog; await watchdog;
}); });
it('should work for selector with a pseudo class', async () => {
const {page, server} = await getTestState();
await page.goto(server.EMPTY_PAGE);
const watchdog = page.waitForSelector('input:focus');
await expect(
Promise.race([watchdog, createTimeout(40)])
).resolves.toBeFalsy();
await page.setContent(`<input></input>`);
await page.click('input');
await watchdog;
});
it('Page.waitForSelector is shortcut for main frame', async () => { it('Page.waitForSelector is shortcut for main frame', async () => {
const {page, server} = await getTestState(); const {page, server} = await getTestState();