diff options
Diffstat (limited to '')
-rw-r--r-- | remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts new file mode 100644 index 0000000000..844120d7ff --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/Frame.ts @@ -0,0 +1,351 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Protocol} from 'devtools-protocol'; + +import type {CDPSession} from '../api/CDPSession.js'; +import {Frame, FrameEvent, throwIfDetached} from '../api/Frame.js'; +import type {HTTPResponse} from '../api/HTTPResponse.js'; +import type {WaitTimeoutOptions} from '../api/Page.js'; +import {UnsupportedOperation} from '../common/Errors.js'; +import {Deferred} from '../util/Deferred.js'; +import {disposeSymbol} from '../util/disposable.js'; +import {isErrorLike} from '../util/ErrorLike.js'; + +import type { + DeviceRequestPrompt, + DeviceRequestPromptManager, +} from './DeviceRequestPrompt.js'; +import type {FrameManager} from './FrameManager.js'; +import {IsolatedWorld} from './IsolatedWorld.js'; +import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; +import { + LifecycleWatcher, + type PuppeteerLifeCycleEvent, +} from './LifecycleWatcher.js'; +import type {CdpPage} from './Page.js'; + +/** + * @internal + */ +export class CdpFrame extends Frame { + #url = ''; + #detached = false; + #client!: CDPSession; + + _frameManager: FrameManager; + override _id: string; + _loaderId = ''; + _lifecycleEvents = new Set<string>(); + override _parentId?: string; + + constructor( + frameManager: FrameManager, + frameId: string, + parentFrameId: string | undefined, + client: CDPSession + ) { + super(); + this._frameManager = frameManager; + this.#url = ''; + this._id = frameId; + this._parentId = parentFrameId; + this.#detached = false; + + this._loaderId = ''; + + this.updateClient(client); + + this.on(FrameEvent.FrameSwappedByActivation, () => { + // Emulate loading process for swapped frames. + this._onLoadingStarted(); + this._onLoadingStopped(); + }); + } + + /** + * This is used internally in DevTools. + * + * @internal + */ + _client(): CDPSession { + return this.#client; + } + + /** + * Updates the frame ID with the new ID. This happens when the main frame is + * replaced by a different frame. + */ + updateId(id: string): void { + this._id = id; + } + + updateClient(client: CDPSession, keepWorlds = false): void { + this.#client = client; + if (!keepWorlds) { + // Clear the current contexts on previous world instances. + if (this.worlds) { + this.worlds[MAIN_WORLD].clearContext(); + this.worlds[PUPPETEER_WORLD].clearContext(); + } + this.worlds = { + [MAIN_WORLD]: new IsolatedWorld( + this, + this._frameManager.timeoutSettings + ), + [PUPPETEER_WORLD]: new IsolatedWorld( + this, + this._frameManager.timeoutSettings + ), + }; + } else { + this.worlds[MAIN_WORLD].frameUpdated(); + this.worlds[PUPPETEER_WORLD].frameUpdated(); + } + } + + override page(): CdpPage { + return this._frameManager.page(); + } + + override isOOPFrame(): boolean { + return this.#client !== this._frameManager.client; + } + + @throwIfDetached + override async goto( + url: string, + options: { + referer?: string; + referrerPolicy?: string; + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<HTTPResponse | null> { + const { + referer = this._frameManager.networkManager.extraHTTPHeaders()['referer'], + referrerPolicy = this._frameManager.networkManager.extraHTTPHeaders()[ + 'referer-policy' + ], + waitUntil = ['load'], + timeout = this._frameManager.timeoutSettings.navigationTimeout(), + } = options; + + let ensureNewDocumentNavigation = false; + const watcher = new LifecycleWatcher( + this._frameManager.networkManager, + this, + waitUntil, + timeout + ); + let error = await Deferred.race([ + navigate( + this.#client, + url, + referer, + referrerPolicy as Protocol.Page.ReferrerPolicy, + this._id + ), + watcher.terminationPromise(), + ]); + if (!error) { + error = await Deferred.race([ + watcher.terminationPromise(), + ensureNewDocumentNavigation + ? watcher.newDocumentNavigationPromise() + : watcher.sameDocumentNavigationPromise(), + ]); + } + + try { + if (error) { + throw error; + } + return await watcher.navigationResponse(); + } finally { + watcher.dispose(); + } + + async function navigate( + client: CDPSession, + url: string, + referrer: string | undefined, + referrerPolicy: Protocol.Page.ReferrerPolicy | undefined, + frameId: string + ): Promise<Error | null> { + try { + const response = await client.send('Page.navigate', { + url, + referrer, + frameId, + referrerPolicy, + }); + ensureNewDocumentNavigation = !!response.loaderId; + if (response.errorText === 'net::ERR_HTTP_RESPONSE_CODE_FAILURE') { + return null; + } + return response.errorText + ? new Error(`${response.errorText} at ${url}`) + : null; + } catch (error) { + if (isErrorLike(error)) { + return error; + } + throw error; + } + } + } + + @throwIfDetached + override async waitForNavigation( + options: { + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<HTTPResponse | null> { + const { + waitUntil = ['load'], + timeout = this._frameManager.timeoutSettings.navigationTimeout(), + } = options; + const watcher = new LifecycleWatcher( + this._frameManager.networkManager, + this, + waitUntil, + timeout + ); + const error = await Deferred.race([ + watcher.terminationPromise(), + watcher.sameDocumentNavigationPromise(), + watcher.newDocumentNavigationPromise(), + ]); + try { + if (error) { + throw error; + } + return await watcher.navigationResponse(); + } finally { + watcher.dispose(); + } + } + + override get client(): CDPSession { + return this.#client; + } + + override mainRealm(): IsolatedWorld { + return this.worlds[MAIN_WORLD]; + } + + override isolatedRealm(): IsolatedWorld { + return this.worlds[PUPPETEER_WORLD]; + } + + @throwIfDetached + override async setContent( + html: string, + options: { + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<void> { + const { + waitUntil = ['load'], + timeout = this._frameManager.timeoutSettings.navigationTimeout(), + } = options; + + // We rely upon the fact that document.open() will reset frame lifecycle with "init" + // lifecycle event. @see https://crrev.com/608658 + await this.setFrameContent(html); + + const watcher = new LifecycleWatcher( + this._frameManager.networkManager, + this, + waitUntil, + timeout + ); + const error = await Deferred.race<void | Error | undefined>([ + watcher.terminationPromise(), + watcher.lifecyclePromise(), + ]); + watcher.dispose(); + if (error) { + throw error; + } + } + + override url(): string { + return this.#url; + } + + override parentFrame(): CdpFrame | null { + return this._frameManager._frameTree.parentFrame(this._id) || null; + } + + override childFrames(): CdpFrame[] { + return this._frameManager._frameTree.childFrames(this._id); + } + + #deviceRequestPromptManager(): DeviceRequestPromptManager { + const rootFrame = this.page().mainFrame(); + if (this.isOOPFrame() || rootFrame === null) { + return this._frameManager._deviceRequestPromptManager(this.#client); + } else { + return rootFrame._frameManager._deviceRequestPromptManager(this.#client); + } + } + + @throwIfDetached + override async waitForDevicePrompt( + options: WaitTimeoutOptions = {} + ): Promise<DeviceRequestPrompt> { + return await this.#deviceRequestPromptManager().waitForDevicePrompt( + options + ); + } + + _navigated(framePayload: Protocol.Page.Frame): void { + this._name = framePayload.name; + this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`; + } + + _navigatedWithinDocument(url: string): void { + this.#url = url; + } + + _onLifecycleEvent(loaderId: string, name: string): void { + if (name === 'init') { + this._loaderId = loaderId; + this._lifecycleEvents.clear(); + } + this._lifecycleEvents.add(name); + } + + _onLoadingStopped(): void { + this._lifecycleEvents.add('DOMContentLoaded'); + this._lifecycleEvents.add('load'); + } + + _onLoadingStarted(): void { + this._hasStartedLoading = true; + } + + override get detached(): boolean { + return this.#detached; + } + + [disposeSymbol](): void { + if (this.#detached) { + return; + } + this.#detached = true; + this.worlds[MAIN_WORLD][disposeSymbol](); + this.worlds[PUPPETEER_WORLD][disposeSymbol](); + } + + exposeFunction(): never { + throw new UnsupportedOperation(); + } +} |