Frame.ts (11857B)
1 /** 2 * @license 3 * Copyright 2017 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {Protocol} from 'devtools-protocol'; 8 9 import type {CDPSession} from '../api/CDPSession.js'; 10 import type {ElementHandle} from '../api/ElementHandle.js'; 11 import type {WaitForOptions} from '../api/Frame.js'; 12 import {Frame, FrameEvent, throwIfDetached} from '../api/Frame.js'; 13 import type {HTTPResponse} from '../api/HTTPResponse.js'; 14 import type {WaitTimeoutOptions} from '../api/Page.js'; 15 import {UnsupportedOperation} from '../common/Errors.js'; 16 import {debugError} from '../common/util.js'; 17 import {Deferred} from '../util/Deferred.js'; 18 import {disposeSymbol} from '../util/disposable.js'; 19 import {isErrorLike} from '../util/ErrorLike.js'; 20 21 import {Accessibility} from './Accessibility.js'; 22 import type {Binding} from './Binding.js'; 23 import type {CdpPreloadScript} from './CdpPreloadScript.js'; 24 import type { 25 DeviceRequestPrompt, 26 DeviceRequestPromptManager, 27 } from './DeviceRequestPrompt.js'; 28 import type {FrameManager} from './FrameManager.js'; 29 import {FrameManagerEvent} from './FrameManagerEvents.js'; 30 import type {IsolatedWorldChart} from './IsolatedWorld.js'; 31 import {IsolatedWorld} from './IsolatedWorld.js'; 32 import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; 33 import { 34 LifecycleWatcher, 35 type PuppeteerLifeCycleEvent, 36 } from './LifecycleWatcher.js'; 37 import type {CdpPage} from './Page.js'; 38 import {CDP_BINDING_PREFIX} from './utils.js'; 39 40 /** 41 * @internal 42 */ 43 export class CdpFrame extends Frame { 44 #url = ''; 45 #detached = false; 46 #client: CDPSession; 47 48 _frameManager: FrameManager; 49 _loaderId = ''; 50 _lifecycleEvents = new Set<string>(); 51 52 override _id: string; 53 override _parentId?: string; 54 override accessibility: Accessibility; 55 56 worlds: IsolatedWorldChart; 57 58 constructor( 59 frameManager: FrameManager, 60 frameId: string, 61 parentFrameId: string | undefined, 62 client: CDPSession, 63 ) { 64 super(); 65 this._frameManager = frameManager; 66 this.#url = ''; 67 this._id = frameId; 68 this._parentId = parentFrameId; 69 this.#detached = false; 70 this.#client = client; 71 72 this._loaderId = ''; 73 this.worlds = { 74 [MAIN_WORLD]: new IsolatedWorld(this, this._frameManager.timeoutSettings), 75 [PUPPETEER_WORLD]: new IsolatedWorld( 76 this, 77 this._frameManager.timeoutSettings, 78 ), 79 }; 80 81 this.accessibility = new Accessibility(this.worlds[MAIN_WORLD], frameId); 82 83 this.on(FrameEvent.FrameSwappedByActivation, () => { 84 // Emulate loading process for swapped frames. 85 this._onLoadingStarted(); 86 this._onLoadingStopped(); 87 }); 88 89 this.worlds[MAIN_WORLD].emitter.on( 90 'consoleapicalled', 91 this.#onMainWorldConsoleApiCalled.bind(this), 92 ); 93 this.worlds[MAIN_WORLD].emitter.on( 94 'bindingcalled', 95 this.#onMainWorldBindingCalled.bind(this), 96 ); 97 } 98 99 #onMainWorldConsoleApiCalled( 100 event: Protocol.Runtime.ConsoleAPICalledEvent, 101 ): void { 102 this._frameManager.emit(FrameManagerEvent.ConsoleApiCalled, [ 103 this.worlds[MAIN_WORLD], 104 event, 105 ]); 106 } 107 108 #onMainWorldBindingCalled(event: Protocol.Runtime.BindingCalledEvent) { 109 this._frameManager.emit(FrameManagerEvent.BindingCalled, [ 110 this.worlds[MAIN_WORLD], 111 event, 112 ]); 113 } 114 115 /** 116 * This is used internally in DevTools. 117 * 118 * @internal 119 */ 120 _client(): CDPSession { 121 return this.#client; 122 } 123 124 /** 125 * Updates the frame ID with the new ID. This happens when the main frame is 126 * replaced by a different frame. 127 */ 128 updateId(id: string): void { 129 this._id = id; 130 } 131 132 updateClient(client: CDPSession): void { 133 this.#client = client; 134 } 135 136 override page(): CdpPage { 137 return this._frameManager.page(); 138 } 139 140 @throwIfDetached 141 override async goto( 142 url: string, 143 options: { 144 referer?: string; 145 referrerPolicy?: string; 146 timeout?: number; 147 waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; 148 } = {}, 149 ): Promise<HTTPResponse | null> { 150 const { 151 referer = this._frameManager.networkManager.extraHTTPHeaders()['referer'], 152 referrerPolicy = this._frameManager.networkManager.extraHTTPHeaders()[ 153 'referer-policy' 154 ], 155 waitUntil = ['load'], 156 timeout = this._frameManager.timeoutSettings.navigationTimeout(), 157 } = options; 158 159 let ensureNewDocumentNavigation = false; 160 const watcher = new LifecycleWatcher( 161 this._frameManager.networkManager, 162 this, 163 waitUntil, 164 timeout, 165 ); 166 let error = await Deferred.race([ 167 navigate( 168 this.#client, 169 url, 170 referer, 171 referrerPolicy as Protocol.Page.ReferrerPolicy, 172 this._id, 173 ), 174 watcher.terminationPromise(), 175 ]); 176 if (!error) { 177 error = await Deferred.race([ 178 watcher.terminationPromise(), 179 ensureNewDocumentNavigation 180 ? watcher.newDocumentNavigationPromise() 181 : watcher.sameDocumentNavigationPromise(), 182 ]); 183 } 184 185 try { 186 if (error) { 187 throw error; 188 } 189 return await watcher.navigationResponse(); 190 } finally { 191 watcher.dispose(); 192 } 193 194 async function navigate( 195 client: CDPSession, 196 url: string, 197 referrer: string | undefined, 198 referrerPolicy: Protocol.Page.ReferrerPolicy | undefined, 199 frameId: string, 200 ): Promise<Error | null> { 201 try { 202 const response = await client.send('Page.navigate', { 203 url, 204 referrer, 205 frameId, 206 referrerPolicy, 207 }); 208 ensureNewDocumentNavigation = !!response.loaderId; 209 if (response.errorText === 'net::ERR_HTTP_RESPONSE_CODE_FAILURE') { 210 return null; 211 } 212 return response.errorText 213 ? new Error(`${response.errorText} at ${url}`) 214 : null; 215 } catch (error) { 216 if (isErrorLike(error)) { 217 return error; 218 } 219 throw error; 220 } 221 } 222 } 223 224 @throwIfDetached 225 override async waitForNavigation( 226 options: WaitForOptions = {}, 227 ): Promise<HTTPResponse | null> { 228 const { 229 waitUntil = ['load'], 230 timeout = this._frameManager.timeoutSettings.navigationTimeout(), 231 signal, 232 } = options; 233 const watcher = new LifecycleWatcher( 234 this._frameManager.networkManager, 235 this, 236 waitUntil, 237 timeout, 238 signal, 239 ); 240 const error = await Deferred.race([ 241 watcher.terminationPromise(), 242 ...(options.ignoreSameDocumentNavigation 243 ? [] 244 : [watcher.sameDocumentNavigationPromise()]), 245 watcher.newDocumentNavigationPromise(), 246 ]); 247 try { 248 if (error) { 249 throw error; 250 } 251 const result = await Deferred.race< 252 Error | HTTPResponse | null | undefined 253 >([watcher.terminationPromise(), watcher.navigationResponse()]); 254 if (result instanceof Error) { 255 throw error; 256 } 257 return result || null; 258 } finally { 259 watcher.dispose(); 260 } 261 } 262 263 override get client(): CDPSession { 264 return this.#client; 265 } 266 267 override mainRealm(): IsolatedWorld { 268 return this.worlds[MAIN_WORLD]; 269 } 270 271 override isolatedRealm(): IsolatedWorld { 272 return this.worlds[PUPPETEER_WORLD]; 273 } 274 275 @throwIfDetached 276 override async setContent( 277 html: string, 278 options: { 279 timeout?: number; 280 waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; 281 } = {}, 282 ): Promise<void> { 283 const { 284 waitUntil = ['load'], 285 timeout = this._frameManager.timeoutSettings.navigationTimeout(), 286 } = options; 287 288 // We rely upon the fact that document.open() will reset frame lifecycle with "init" 289 // lifecycle event. @see https://crrev.com/608658 290 await this.setFrameContent(html); 291 292 const watcher = new LifecycleWatcher( 293 this._frameManager.networkManager, 294 this, 295 waitUntil, 296 timeout, 297 ); 298 const error = await Deferred.race<void | Error | undefined>([ 299 watcher.terminationPromise(), 300 watcher.lifecyclePromise(), 301 ]); 302 watcher.dispose(); 303 if (error) { 304 throw error; 305 } 306 } 307 308 override url(): string { 309 return this.#url; 310 } 311 312 override parentFrame(): CdpFrame | null { 313 return this._frameManager._frameTree.parentFrame(this._id) || null; 314 } 315 316 override childFrames(): CdpFrame[] { 317 return this._frameManager._frameTree.childFrames(this._id); 318 } 319 320 #deviceRequestPromptManager(): DeviceRequestPromptManager { 321 return this._frameManager._deviceRequestPromptManager(this.#client); 322 } 323 324 @throwIfDetached 325 async addPreloadScript(preloadScript: CdpPreloadScript): Promise<void> { 326 const parentFrame = this.parentFrame(); 327 if (parentFrame && this.#client === parentFrame.client) { 328 return; 329 } 330 if (preloadScript.getIdForFrame(this)) { 331 return; 332 } 333 const {identifier} = await this.#client.send( 334 'Page.addScriptToEvaluateOnNewDocument', 335 { 336 source: preloadScript.source, 337 }, 338 ); 339 preloadScript.setIdForFrame(this, identifier); 340 } 341 342 @throwIfDetached 343 async addExposedFunctionBinding(binding: Binding): Promise<void> { 344 // If a frame has not started loading, it might never start. Rely on 345 // addScriptToEvaluateOnNewDocument in that case. 346 if (this !== this._frameManager.mainFrame() && !this._hasStartedLoading) { 347 return; 348 } 349 await Promise.all([ 350 this.#client.send('Runtime.addBinding', { 351 name: CDP_BINDING_PREFIX + binding.name, 352 }), 353 this.evaluate(binding.initSource).catch(debugError), 354 ]); 355 } 356 357 @throwIfDetached 358 async removeExposedFunctionBinding(binding: Binding): Promise<void> { 359 // If a frame has not started loading, it might never start. Rely on 360 // addScriptToEvaluateOnNewDocument in that case. 361 if (this !== this._frameManager.mainFrame() && !this._hasStartedLoading) { 362 return; 363 } 364 await Promise.all([ 365 this.#client.send('Runtime.removeBinding', { 366 name: CDP_BINDING_PREFIX + binding.name, 367 }), 368 this.evaluate(name => { 369 // Removes the dangling Puppeteer binding wrapper. 370 // @ts-expect-error: In a different context. 371 globalThis[name] = undefined; 372 }, binding.name).catch(debugError), 373 ]); 374 } 375 376 @throwIfDetached 377 override async waitForDevicePrompt( 378 options: WaitTimeoutOptions = {}, 379 ): Promise<DeviceRequestPrompt> { 380 return await this.#deviceRequestPromptManager().waitForDevicePrompt( 381 options, 382 ); 383 } 384 385 _navigated(framePayload: Protocol.Page.Frame): void { 386 this._name = framePayload.name; 387 this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`; 388 } 389 390 _navigatedWithinDocument(url: string): void { 391 this.#url = url; 392 } 393 394 _onLifecycleEvent(loaderId: string, name: string): void { 395 if (name === 'init') { 396 this._loaderId = loaderId; 397 this._lifecycleEvents.clear(); 398 } 399 this._lifecycleEvents.add(name); 400 } 401 402 _onLoadingStopped(): void { 403 this._lifecycleEvents.add('DOMContentLoaded'); 404 this._lifecycleEvents.add('load'); 405 } 406 407 _onLoadingStarted(): void { 408 this._hasStartedLoading = true; 409 } 410 411 override get detached(): boolean { 412 return this.#detached; 413 } 414 415 override [disposeSymbol](): void { 416 if (this.#detached) { 417 return; 418 } 419 this.#detached = true; 420 this.worlds[MAIN_WORLD][disposeSymbol](); 421 this.worlds[PUPPETEER_WORLD][disposeSymbol](); 422 } 423 424 exposeFunction(): never { 425 throw new UnsupportedOperation(); 426 } 427 428 override async frameElement(): Promise<ElementHandle<HTMLIFrameElement> | null> { 429 const parent = this.parentFrame(); 430 if (!parent) { 431 return null; 432 } 433 const {backendNodeId} = await parent.client.send('DOM.getFrameOwner', { 434 frameId: this._id, 435 }); 436 return (await parent 437 .mainRealm() 438 .adoptBackendNode(backendNodeId)) as ElementHandle<HTMLIFrameElement>; 439 } 440 }