WaitTask.ts (7039B)
1 /** 2 * @license 3 * Copyright 2022 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {ElementHandle} from '../api/ElementHandle.js'; 8 import type {JSHandle} from '../api/JSHandle.js'; 9 import type {Realm} from '../api/Realm.js'; 10 import type {Poller} from '../injected/Poller.js'; 11 import {Deferred} from '../util/Deferred.js'; 12 import {isErrorLike} from '../util/ErrorLike.js'; 13 import {stringifyFunction} from '../util/Function.js'; 14 15 import {TimeoutError} from './Errors.js'; 16 import {LazyArg} from './LazyArg.js'; 17 import type {HandleFor} from './types.js'; 18 19 /** 20 * @internal 21 */ 22 export interface WaitTaskOptions { 23 polling: 'raf' | 'mutation' | number; 24 root?: ElementHandle<Node>; 25 timeout: number; 26 signal?: AbortSignal; 27 } 28 29 /** 30 * @internal 31 */ 32 export class WaitTask<T = unknown> { 33 #world: Realm; 34 #polling: 'raf' | 'mutation' | number; 35 #root?: ElementHandle<Node>; 36 37 #fn: string; 38 #args: unknown[]; 39 40 #timeout?: NodeJS.Timeout; 41 #timeoutError?: TimeoutError; 42 43 #result = Deferred.create<HandleFor<T>>(); 44 45 #poller?: JSHandle<Poller<T>>; 46 #signal?: AbortSignal; 47 #reruns: AbortController[] = []; 48 49 constructor( 50 world: Realm, 51 options: WaitTaskOptions, 52 fn: ((...args: unknown[]) => Promise<T>) | string, 53 ...args: unknown[] 54 ) { 55 this.#world = world; 56 this.#polling = options.polling; 57 this.#root = options.root; 58 this.#signal = options.signal; 59 this.#signal?.addEventListener('abort', this.#onAbortSignal, { 60 once: true, 61 }); 62 63 switch (typeof fn) { 64 case 'string': 65 this.#fn = `() => {return (${fn});}`; 66 break; 67 default: 68 this.#fn = stringifyFunction(fn); 69 break; 70 } 71 this.#args = args; 72 73 this.#world.taskManager.add(this); 74 75 if (options.timeout) { 76 this.#timeoutError = new TimeoutError( 77 `Waiting failed: ${options.timeout}ms exceeded`, 78 ); 79 this.#timeout = setTimeout(() => { 80 void this.terminate(this.#timeoutError); 81 }, options.timeout); 82 } 83 84 void this.rerun(); 85 } 86 87 get result(): Promise<HandleFor<T>> { 88 return this.#result.valueOrThrow(); 89 } 90 91 async rerun(): Promise<void> { 92 for (const prev of this.#reruns) { 93 prev.abort(); 94 } 95 this.#reruns.length = 0; 96 const controller = new AbortController(); 97 this.#reruns.push(controller); 98 try { 99 switch (this.#polling) { 100 case 'raf': 101 this.#poller = await this.#world.evaluateHandle( 102 ({RAFPoller, createFunction}, fn, ...args) => { 103 const fun = createFunction(fn); 104 return new RAFPoller(() => { 105 return fun(...args) as Promise<T>; 106 }); 107 }, 108 LazyArg.create(context => { 109 return context.puppeteerUtil; 110 }), 111 this.#fn, 112 ...this.#args, 113 ); 114 break; 115 case 'mutation': 116 this.#poller = await this.#world.evaluateHandle( 117 ({MutationPoller, createFunction}, root, fn, ...args) => { 118 const fun = createFunction(fn); 119 return new MutationPoller(() => { 120 return fun(...args) as Promise<T>; 121 }, root || document); 122 }, 123 LazyArg.create(context => { 124 return context.puppeteerUtil; 125 }), 126 this.#root, 127 this.#fn, 128 ...this.#args, 129 ); 130 break; 131 default: 132 this.#poller = await this.#world.evaluateHandle( 133 ({IntervalPoller, createFunction}, ms, fn, ...args) => { 134 const fun = createFunction(fn); 135 return new IntervalPoller(() => { 136 return fun(...args) as Promise<T>; 137 }, ms); 138 }, 139 LazyArg.create(context => { 140 return context.puppeteerUtil; 141 }), 142 this.#polling, 143 this.#fn, 144 ...this.#args, 145 ); 146 break; 147 } 148 149 await this.#poller.evaluate(poller => { 150 void poller.start(); 151 }); 152 153 const result = await this.#poller.evaluateHandle(poller => { 154 return poller.result(); 155 }); 156 this.#result.resolve(result); 157 158 await this.terminate(); 159 } catch (error) { 160 if (controller.signal.aborted) { 161 return; 162 } 163 const badError = this.getBadError(error); 164 if (badError) { 165 await this.terminate(badError); 166 } 167 } 168 } 169 170 async terminate(error?: Error): Promise<void> { 171 this.#world.taskManager.delete(this); 172 173 this.#signal?.removeEventListener('abort', this.#onAbortSignal); 174 175 clearTimeout(this.#timeout); 176 177 if (error && !this.#result.finished()) { 178 this.#result.reject(error); 179 } 180 181 if (this.#poller) { 182 try { 183 await this.#poller.evaluate(async poller => { 184 await poller.stop(); 185 }); 186 if (this.#poller) { 187 await this.#poller.dispose(); 188 this.#poller = undefined; 189 } 190 } catch { 191 // Ignore errors since they most likely come from low-level cleanup. 192 } 193 } 194 } 195 196 /** 197 * Not all errors lead to termination. They usually imply we need to rerun the task. 198 */ 199 getBadError(error: unknown): Error | undefined { 200 if (isErrorLike(error)) { 201 // When frame is detached the task should have been terminated by the IsolatedWorld. 202 // This can fail if we were adding this task while the frame was detached, 203 // so we terminate here instead. 204 if ( 205 error.message.includes( 206 'Execution context is not available in detached frame', 207 ) 208 ) { 209 return new Error('Waiting failed: Frame detached'); 210 } 211 212 // When the page is navigated, the promise is rejected. 213 // We will try again in the new execution context. 214 if (error.message.includes('Execution context was destroyed')) { 215 return; 216 } 217 218 // We could have tried to evaluate in a context which was already 219 // destroyed. 220 if (error.message.includes('Cannot find context with specified id')) { 221 return; 222 } 223 224 // Errors coming from WebDriver BiDi. TODO: Adjust messages after 225 // https://github.com/w3c/webdriver-bidi/issues/540 is resolved. 226 if (error.message.includes('DiscardedBrowsingContextError')) { 227 return; 228 } 229 230 return error; 231 } 232 233 return new Error('WaitTask failed with an error', { 234 cause: error, 235 }); 236 } 237 238 #onAbortSignal = () => { 239 void this.terminate(this.#signal?.reason); 240 }; 241 } 242 243 /** 244 * @internal 245 */ 246 export class TaskManager { 247 #tasks: Set<WaitTask> = new Set<WaitTask>(); 248 249 add(task: WaitTask<any>): void { 250 this.#tasks.add(task); 251 } 252 253 delete(task: WaitTask<any>): void { 254 this.#tasks.delete(task); 255 } 256 257 terminateAll(error?: Error): void { 258 for (const task of this.#tasks) { 259 void task.terminate(error); 260 } 261 this.#tasks.clear(); 262 } 263 264 async rerunAll(): Promise<void> { 265 await Promise.all( 266 [...this.#tasks].map(task => { 267 return task.rerun(); 268 }), 269 ); 270 } 271 }