QueryHandler.ts (5972B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {ElementHandle} from '../api/ElementHandle.js'; 8 import {_isElementHandle} from '../api/ElementHandleSymbol.js'; 9 import type {Frame} from '../api/Frame.js'; 10 import type {WaitForSelectorOptions} from '../api/Page.js'; 11 import type PuppeteerUtil from '../injected/injected.js'; 12 import {isErrorLike} from '../util/ErrorLike.js'; 13 import {interpolateFunction, stringifyFunction} from '../util/Function.js'; 14 15 import {transposeIterableHandle} from './HandleIterator.js'; 16 import {LazyArg} from './LazyArg.js'; 17 import type {Awaitable, AwaitableIterable} from './types.js'; 18 19 /** 20 * @internal 21 */ 22 export type QuerySelectorAll = ( 23 node: Node, 24 selector: string, 25 PuppeteerUtil: PuppeteerUtil, 26 ) => AwaitableIterable<Node>; 27 28 /** 29 * @internal 30 */ 31 export type QuerySelector = ( 32 node: Node, 33 selector: string, 34 PuppeteerUtil: PuppeteerUtil, 35 ) => Awaitable<Node | null>; 36 37 /** 38 * @internal 39 */ 40 export const enum PollingOptions { 41 RAF = 'raf', 42 MUTATION = 'mutation', 43 } 44 45 /** 46 * @internal 47 */ 48 export class QueryHandler { 49 // Either one of these may be implemented, but at least one must be. 50 static querySelectorAll?: QuerySelectorAll; 51 static querySelector?: QuerySelector; 52 53 static get _querySelector(): QuerySelector { 54 if (this.querySelector) { 55 return this.querySelector; 56 } 57 if (!this.querySelectorAll) { 58 throw new Error('Cannot create default `querySelector`.'); 59 } 60 61 return (this.querySelector = interpolateFunction( 62 async (node, selector, PuppeteerUtil) => { 63 const querySelectorAll: QuerySelectorAll = 64 PLACEHOLDER('querySelectorAll'); 65 const results = querySelectorAll(node, selector, PuppeteerUtil); 66 for await (const result of results) { 67 return result; 68 } 69 return null; 70 }, 71 { 72 querySelectorAll: stringifyFunction(this.querySelectorAll), 73 }, 74 )); 75 } 76 77 static get _querySelectorAll(): QuerySelectorAll { 78 if (this.querySelectorAll) { 79 return this.querySelectorAll; 80 } 81 if (!this.querySelector) { 82 throw new Error('Cannot create default `querySelectorAll`.'); 83 } 84 85 return (this.querySelectorAll = interpolateFunction( 86 async function* (node, selector, PuppeteerUtil) { 87 const querySelector: QuerySelector = PLACEHOLDER('querySelector'); 88 const result = await querySelector(node, selector, PuppeteerUtil); 89 if (result) { 90 yield result; 91 } 92 }, 93 { 94 querySelector: stringifyFunction(this.querySelector), 95 }, 96 )); 97 } 98 99 /** 100 * Queries for multiple nodes given a selector and {@link ElementHandle}. 101 * 102 * Akin to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll | Document.querySelectorAll()}. 103 */ 104 static async *queryAll( 105 element: ElementHandle<Node>, 106 selector: string, 107 ): AwaitableIterable<ElementHandle<Node>> { 108 using handle = await element.evaluateHandle( 109 this._querySelectorAll, 110 selector, 111 LazyArg.create(context => { 112 return context.puppeteerUtil; 113 }), 114 ); 115 yield* transposeIterableHandle(handle); 116 } 117 118 /** 119 * Queries for a single node given a selector and {@link ElementHandle}. 120 * 121 * Akin to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector}. 122 */ 123 static async queryOne( 124 element: ElementHandle<Node>, 125 selector: string, 126 ): Promise<ElementHandle<Node> | null> { 127 using result = await element.evaluateHandle( 128 this._querySelector, 129 selector, 130 LazyArg.create(context => { 131 return context.puppeteerUtil; 132 }), 133 ); 134 if (!(_isElementHandle in result)) { 135 return null; 136 } 137 return result.move(); 138 } 139 140 /** 141 * Waits until a single node appears for a given selector and 142 * {@link ElementHandle}. 143 * 144 * This will always query the handle in the Puppeteer world and migrate the 145 * result to the main world. 146 */ 147 static async waitFor( 148 elementOrFrame: ElementHandle<Node> | Frame, 149 selector: string, 150 options: WaitForSelectorOptions & { 151 polling?: PollingOptions; 152 }, 153 ): Promise<ElementHandle<Node> | null> { 154 let frame!: Frame; 155 using element = await (async () => { 156 if (!(_isElementHandle in elementOrFrame)) { 157 frame = elementOrFrame; 158 return; 159 } 160 frame = elementOrFrame.frame; 161 return await frame.isolatedRealm().adoptHandle(elementOrFrame); 162 })(); 163 164 const {visible = false, hidden = false, timeout, signal} = options; 165 const polling = visible || hidden ? PollingOptions.RAF : options.polling; 166 167 try { 168 signal?.throwIfAborted(); 169 170 using handle = await frame.isolatedRealm().waitForFunction( 171 async (PuppeteerUtil, query, selector, root, visible) => { 172 const querySelector = PuppeteerUtil.createFunction( 173 query, 174 ) as QuerySelector; 175 const node = await querySelector( 176 root ?? document, 177 selector, 178 PuppeteerUtil, 179 ); 180 return PuppeteerUtil.checkVisibility(node, visible); 181 }, 182 { 183 polling, 184 root: element, 185 timeout, 186 signal, 187 }, 188 LazyArg.create(context => { 189 return context.puppeteerUtil; 190 }), 191 stringifyFunction(this._querySelector), 192 selector, 193 element, 194 visible ? true : hidden ? false : undefined, 195 ); 196 197 if (signal?.aborted) { 198 throw signal.reason; 199 } 200 201 if (!(_isElementHandle in handle)) { 202 return null; 203 } 204 return await frame.mainRealm().transferHandle(handle); 205 } catch (error) { 206 if (!isErrorLike(error)) { 207 throw error; 208 } 209 if (error.name === 'AbortError') { 210 throw error; 211 } 212 error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`; 213 throw error; 214 } 215 } 216 }