/** * Copyright 2017 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {Protocol} from 'devtools-protocol'; import {type ClickOptions, ElementHandle} from '../api/ElementHandle.js'; import { Frame as BaseFrame, FrameAddScriptTagOptions, FrameAddStyleTagOptions, FrameWaitForFunctionOptions, } from '../api/Frame.js'; import {HTTPResponse} from '../api/HTTPResponse.js'; import {Page, WaitTimeoutOptions} from '../api/Page.js'; import {assert} from '../util/assert.js'; import {Deferred} from '../util/Deferred.js'; import {isErrorLike} from '../util/ErrorLike.js'; import {CDPSession} from './Connection.js'; import { DeviceRequestPrompt, DeviceRequestPromptManager, } from './DeviceRequestPrompt.js'; import {ExecutionContext} from './ExecutionContext.js'; import {FrameManager} from './FrameManager.js'; import {getQueryHandlerAndSelector} from './GetQueryHandler.js'; import { IsolatedWorld, IsolatedWorldChart, WaitForSelectorOptions, } from './IsolatedWorld.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; import {LazyArg} from './LazyArg.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {EvaluateFunc, EvaluateFuncWith, HandleFor, NodeFor} from './types.js'; import {importFSPromises, withSourcePuppeteerURLIfNone} from './util.js'; /** * @internal */ export class Frame extends BaseFrame { #url = ''; #detached = false; #client!: CDPSession; override worlds!: IsolatedWorldChart; _frameManager: FrameManager; override _id: string; _loaderId = ''; override _name?: string; override _hasStartedLoading = false; _lifecycleEvents = new Set(); 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); } updateClient(client: CDPSession): void { this.#client = client; this.worlds = { [MAIN_WORLD]: new IsolatedWorld(this), [PUPPETEER_WORLD]: new IsolatedWorld(this), }; } override page(): Page { return this._frameManager.page(); } override isOOPFrame(): boolean { return this.#client !== this._frameManager.client; } override async goto( url: string, options: { referer?: string; referrerPolicy?: string; timeout?: number; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; } = {} ): Promise { 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, this, waitUntil, timeout ); let error = await Deferred.race([ navigate( this.#client, url, referer, referrerPolicy as Protocol.Page.ReferrerPolicy, this._id ), watcher.timeoutOrTerminationPromise(), ]); if (!error) { error = await Deferred.race([ watcher.timeoutOrTerminationPromise(), 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 { 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; } } } override async waitForNavigation( options: { timeout?: number; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; } = {} ): Promise { const { waitUntil = ['load'], timeout = this._frameManager.timeoutSettings.navigationTimeout(), } = options; const watcher = new LifecycleWatcher( this._frameManager, this, waitUntil, timeout ); const error = await Deferred.race([ watcher.timeoutOrTerminationPromise(), watcher.sameDocumentNavigationPromise(), watcher.newDocumentNavigationPromise(), ]); try { if (error) { throw error; } return await watcher.navigationResponse(); } finally { watcher.dispose(); } } override _client(): CDPSession { return this.#client; } override executionContext(): Promise { return this.worlds[MAIN_WORLD].executionContext(); } override async evaluateHandle< Params extends unknown[], Func extends EvaluateFunc = EvaluateFunc >( pageFunction: Func | string, ...args: Params ): Promise>>> { pageFunction = withSourcePuppeteerURLIfNone( this.evaluateHandle.name, pageFunction ); return this.worlds[MAIN_WORLD].evaluateHandle(pageFunction, ...args); } override async evaluate< Params extends unknown[], Func extends EvaluateFunc = EvaluateFunc >( pageFunction: Func | string, ...args: Params ): Promise>> { pageFunction = withSourcePuppeteerURLIfNone( this.evaluate.name, pageFunction ); return this.worlds[MAIN_WORLD].evaluate(pageFunction, ...args); } override async $( selector: Selector ): Promise> | null> { return this.worlds[MAIN_WORLD].$(selector); } override async $$( selector: Selector ): Promise>>> { return this.worlds[MAIN_WORLD].$$(selector); } override async $eval< Selector extends string, Params extends unknown[], Func extends EvaluateFuncWith, Params> = EvaluateFuncWith< NodeFor, Params > >( selector: Selector, pageFunction: Func | string, ...args: Params ): Promise>> { pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction); return this.worlds[MAIN_WORLD].$eval(selector, pageFunction, ...args); } override async $$eval< Selector extends string, Params extends unknown[], Func extends EvaluateFuncWith< Array>, Params > = EvaluateFuncWith>, Params> >( selector: Selector, pageFunction: Func | string, ...args: Params ): Promise>> { pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction); return this.worlds[MAIN_WORLD].$$eval(selector, pageFunction, ...args); } override async $x(expression: string): Promise>> { return this.worlds[MAIN_WORLD].$x(expression); } override async waitForSelector( selector: Selector, options: WaitForSelectorOptions = {} ): Promise> | null> { const {updatedSelector, QueryHandler} = getQueryHandlerAndSelector(selector); return (await QueryHandler.waitFor( this, updatedSelector, options )) as ElementHandle> | null; } override async waitForXPath( xpath: string, options: WaitForSelectorOptions = {} ): Promise | null> { if (xpath.startsWith('//')) { xpath = `.${xpath}`; } return this.waitForSelector(`xpath/${xpath}`, options); } override waitForFunction< Params extends unknown[], Func extends EvaluateFunc = EvaluateFunc >( pageFunction: Func | string, options: FrameWaitForFunctionOptions = {}, ...args: Params ): Promise>>> { return this.worlds[MAIN_WORLD].waitForFunction( pageFunction, options, ...args ) as Promise>>>; } override async content(): Promise { return this.worlds[PUPPETEER_WORLD].content(); } override async setContent( html: string, options: { timeout?: number; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; } = {} ): Promise { return this.worlds[PUPPETEER_WORLD].setContent(html, options); } override name(): string { return this._name || ''; } override url(): string { return this.#url; } override parentFrame(): Frame | null { return this._frameManager._frameTree.parentFrame(this._id) || null; } override childFrames(): Frame[] { return this._frameManager._frameTree.childFrames(this._id); } override isDetached(): boolean { return this.#detached; } override async addScriptTag( options: FrameAddScriptTagOptions ): Promise> { let {content = '', type} = options; const {path} = options; if (+!!options.url + +!!path + +!!content !== 1) { throw new Error( 'Exactly one of `url`, `path`, or `content` must be specified.' ); } if (path) { const fs = await importFSPromises(); content = await fs.readFile(path, 'utf8'); content += `//# sourceURL=${path.replace(/\n/g, '')}`; } type = type ?? 'text/javascript'; return this.worlds[MAIN_WORLD].transferHandle( await this.worlds[PUPPETEER_WORLD].evaluateHandle( async ({Deferred}, {url, id, type, content}) => { const deferred = Deferred.create(); const script = document.createElement('script'); script.type = type; script.text = content; if (url) { script.src = url; script.addEventListener( 'load', () => { return deferred.resolve(); }, {once: true} ); script.addEventListener( 'error', event => { deferred.reject( new Error(event.message ?? 'Could not load script') ); }, {once: true} ); } else { deferred.resolve(); } if (id) { script.id = id; } document.head.appendChild(script); await deferred.valueOrThrow(); return script; }, LazyArg.create(context => { return context.puppeteerUtil; }), {...options, type, content} ) ); } override async addStyleTag( options: Omit ): Promise>; override async addStyleTag( options: FrameAddStyleTagOptions ): Promise>; override async addStyleTag( options: FrameAddStyleTagOptions ): Promise> { let {content = ''} = options; const {path} = options; if (+!!options.url + +!!path + +!!content !== 1) { throw new Error( 'Exactly one of `url`, `path`, or `content` must be specified.' ); } if (path) { const fs = await importFSPromises(); content = await fs.readFile(path, 'utf8'); content += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/'; options.content = content; } return this.worlds[MAIN_WORLD].transferHandle( await this.worlds[PUPPETEER_WORLD].evaluateHandle( async ({Deferred}, {url, content}) => { const deferred = Deferred.create(); let element: HTMLStyleElement | HTMLLinkElement; if (!url) { element = document.createElement('style'); element.appendChild(document.createTextNode(content!)); } else { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; element = link; } element.addEventListener( 'load', () => { deferred.resolve(); }, {once: true} ); element.addEventListener( 'error', event => { deferred.reject( new Error( (event as ErrorEvent).message ?? 'Could not load style' ) ); }, {once: true} ); document.head.appendChild(element); await deferred.valueOrThrow(); return element; }, LazyArg.create(context => { return context.puppeteerUtil; }), options ) ); } override async click( selector: string, options: Readonly = {} ): Promise { return this.worlds[PUPPETEER_WORLD].click(selector, options); } override async focus(selector: string): Promise { return this.worlds[PUPPETEER_WORLD].focus(selector); } override async hover(selector: string): Promise { return this.worlds[PUPPETEER_WORLD].hover(selector); } override select(selector: string, ...values: string[]): Promise { return this.worlds[PUPPETEER_WORLD].select(selector, ...values); } override async tap(selector: string): Promise { return this.worlds[PUPPETEER_WORLD].tap(selector); } override async type( selector: string, text: string, options?: {delay: number} ): Promise { return this.worlds[PUPPETEER_WORLD].type(selector, text, options); } override waitForTimeout(milliseconds: number): Promise { return new Promise(resolve => { setTimeout(resolve, milliseconds); }); } override async title(): Promise { return this.worlds[PUPPETEER_WORLD].title(); } _deviceRequestPromptManager(): DeviceRequestPromptManager { if (this.isOOPFrame()) { return this._frameManager._deviceRequestPromptManager(this.#client); } const parentFrame = this.parentFrame(); assert(parentFrame !== null); return parentFrame._deviceRequestPromptManager(); } override waitForDevicePrompt( options: WaitTimeoutOptions = {} ): Promise { return 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; } _detach(): void { this.#detached = true; this.worlds[MAIN_WORLD]._detach(); this.worlds[PUPPETEER_WORLD]._detach(); } }