summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/src/common/FrameManager.ts
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/src/common/FrameManager.ts')
-rw-r--r--remote/test/puppeteer/src/common/FrameManager.ts1309
1 files changed, 1309 insertions, 0 deletions
diff --git a/remote/test/puppeteer/src/common/FrameManager.ts b/remote/test/puppeteer/src/common/FrameManager.ts
new file mode 100644
index 0000000000..e1017487d3
--- /dev/null
+++ b/remote/test/puppeteer/src/common/FrameManager.ts
@@ -0,0 +1,1309 @@
+/**
+ * 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 { debug } from '../common/Debug.js';
+
+import { EventEmitter } from './EventEmitter.js';
+import { assert } from './assert.js';
+import { helper, debugError } from './helper.js';
+import { ExecutionContext, EVALUATION_SCRIPT_URL } from './ExecutionContext.js';
+import {
+ LifecycleWatcher,
+ PuppeteerLifeCycleEvent,
+} from './LifecycleWatcher.js';
+import { DOMWorld, WaitForSelectorOptions } from './DOMWorld.js';
+import { NetworkManager } from './NetworkManager.js';
+import { TimeoutSettings } from './TimeoutSettings.js';
+import { CDPSession } from './Connection.js';
+import { JSHandle, ElementHandle } from './JSHandle.js';
+import { MouseButton } from './Input.js';
+import { Page } from './Page.js';
+import { HTTPResponse } from './HTTPResponse.js';
+import { Protocol } from 'devtools-protocol';
+import {
+ SerializableOrJSHandle,
+ EvaluateHandleFn,
+ WrapElementHandle,
+ EvaluateFn,
+ EvaluateFnReturnType,
+ UnwrapPromiseLike,
+} from './EvalTypes.js';
+
+const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
+
+/**
+ * We use symbols to prevent external parties listening to these events.
+ * They are internal to Puppeteer.
+ *
+ * @internal
+ */
+export const FrameManagerEmittedEvents = {
+ FrameAttached: Symbol('FrameManager.FrameAttached'),
+ FrameNavigated: Symbol('FrameManager.FrameNavigated'),
+ FrameDetached: Symbol('FrameManager.FrameDetached'),
+ LifecycleEvent: Symbol('FrameManager.LifecycleEvent'),
+ FrameNavigatedWithinDocument: Symbol(
+ 'FrameManager.FrameNavigatedWithinDocument'
+ ),
+ ExecutionContextCreated: Symbol('FrameManager.ExecutionContextCreated'),
+ ExecutionContextDestroyed: Symbol('FrameManager.ExecutionContextDestroyed'),
+};
+
+/**
+ * @internal
+ */
+export class FrameManager extends EventEmitter {
+ _client: CDPSession;
+ private _page: Page;
+ private _networkManager: NetworkManager;
+ _timeoutSettings: TimeoutSettings;
+ private _frames = new Map<string, Frame>();
+ private _contextIdToContext = new Map<number, ExecutionContext>();
+ private _isolatedWorlds = new Set<string>();
+ private _mainFrame: Frame;
+
+ constructor(
+ client: CDPSession,
+ page: Page,
+ ignoreHTTPSErrors: boolean,
+ timeoutSettings: TimeoutSettings
+ ) {
+ super();
+ this._client = client;
+ this._page = page;
+ this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
+ this._timeoutSettings = timeoutSettings;
+ this._client.on('Page.frameAttached', (event) =>
+ this._onFrameAttached(event.frameId, event.parentFrameId)
+ );
+ this._client.on('Page.frameNavigated', (event) =>
+ this._onFrameNavigated(event.frame)
+ );
+ this._client.on('Page.navigatedWithinDocument', (event) =>
+ this._onFrameNavigatedWithinDocument(event.frameId, event.url)
+ );
+ this._client.on('Page.frameDetached', (event) =>
+ this._onFrameDetached(event.frameId)
+ );
+ this._client.on('Page.frameStoppedLoading', (event) =>
+ this._onFrameStoppedLoading(event.frameId)
+ );
+ this._client.on('Runtime.executionContextCreated', (event) =>
+ this._onExecutionContextCreated(event.context)
+ );
+ this._client.on('Runtime.executionContextDestroyed', (event) =>
+ this._onExecutionContextDestroyed(event.executionContextId)
+ );
+ this._client.on('Runtime.executionContextsCleared', () =>
+ this._onExecutionContextsCleared()
+ );
+ this._client.on('Page.lifecycleEvent', (event) =>
+ this._onLifecycleEvent(event)
+ );
+ this._client.on('Target.attachedToTarget', async (event) =>
+ this._onFrameMoved(event)
+ );
+ }
+
+ async initialize(): Promise<void> {
+ const result = await Promise.all([
+ this._client.send('Page.enable'),
+ this._client.send('Page.getFrameTree'),
+ ]);
+
+ const { frameTree } = result[1];
+ this._handleFrameTree(frameTree);
+ await Promise.all([
+ this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
+ this._client
+ .send('Runtime.enable')
+ .then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
+ this._networkManager.initialize(),
+ ]);
+ }
+
+ networkManager(): NetworkManager {
+ return this._networkManager;
+ }
+
+ async navigateFrame(
+ frame: Frame,
+ url: string,
+ options: {
+ referer?: string;
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ } = {}
+ ): Promise<HTTPResponse | null> {
+ assertNoLegacyNavigationOptions(options);
+ const {
+ referer = this._networkManager.extraHTTPHeaders()['referer'],
+ waitUntil = ['load'],
+ timeout = this._timeoutSettings.navigationTimeout(),
+ } = options;
+
+ const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
+ let ensureNewDocumentNavigation = false;
+ let error = await Promise.race([
+ navigate(this._client, url, referer, frame._id),
+ watcher.timeoutOrTerminationPromise(),
+ ]);
+ if (!error) {
+ error = await Promise.race([
+ watcher.timeoutOrTerminationPromise(),
+ ensureNewDocumentNavigation
+ ? watcher.newDocumentNavigationPromise()
+ : watcher.sameDocumentNavigationPromise(),
+ ]);
+ }
+ watcher.dispose();
+ if (error) throw error;
+ return watcher.navigationResponse();
+
+ async function navigate(
+ client: CDPSession,
+ url: string,
+ referrer: string,
+ frameId: string
+ ): Promise<Error | null> {
+ try {
+ const response = await client.send('Page.navigate', {
+ url,
+ referrer,
+ frameId,
+ });
+ ensureNewDocumentNavigation = !!response.loaderId;
+ return response.errorText
+ ? new Error(`${response.errorText} at ${url}`)
+ : null;
+ } catch (error) {
+ return error;
+ }
+ }
+ }
+
+ async waitForFrameNavigation(
+ frame: Frame,
+ options: {
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ } = {}
+ ): Promise<HTTPResponse | null> {
+ assertNoLegacyNavigationOptions(options);
+ const {
+ waitUntil = ['load'],
+ timeout = this._timeoutSettings.navigationTimeout(),
+ } = options;
+ const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
+ const error = await Promise.race([
+ watcher.timeoutOrTerminationPromise(),
+ watcher.sameDocumentNavigationPromise(),
+ watcher.newDocumentNavigationPromise(),
+ ]);
+ watcher.dispose();
+ if (error) throw error;
+ return watcher.navigationResponse();
+ }
+
+ private async _onFrameMoved(event: Protocol.Target.AttachedToTargetEvent) {
+ if (event.targetInfo.type !== 'iframe') {
+ return;
+ }
+
+ // TODO(sadym): Remove debug message once proper OOPIF support is
+ // implemented: https://github.com/puppeteer/puppeteer/issues/2548
+ debug('puppeteer:frame')(
+ `The frame '${event.targetInfo.targetId}' moved to another session. ` +
+ `Out-of-process iframes (OOPIF) are not supported by Puppeteer yet. ` +
+ `https://github.com/puppeteer/puppeteer/issues/2548`
+ );
+ }
+
+ _onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
+ const frame = this._frames.get(event.frameId);
+ if (!frame) return;
+ frame._onLifecycleEvent(event.loaderId, event.name);
+ this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
+ }
+
+ _onFrameStoppedLoading(frameId: string): void {
+ const frame = this._frames.get(frameId);
+ if (!frame) return;
+ frame._onLoadingStopped();
+ this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
+ }
+
+ _handleFrameTree(frameTree: Protocol.Page.FrameTree): void {
+ if (frameTree.frame.parentId)
+ this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId);
+ this._onFrameNavigated(frameTree.frame);
+ if (!frameTree.childFrames) return;
+
+ for (const child of frameTree.childFrames) this._handleFrameTree(child);
+ }
+
+ page(): Page {
+ return this._page;
+ }
+
+ mainFrame(): Frame {
+ return this._mainFrame;
+ }
+
+ frames(): Frame[] {
+ return Array.from(this._frames.values());
+ }
+
+ frame(frameId: string): Frame | null {
+ return this._frames.get(frameId) || null;
+ }
+
+ _onFrameAttached(frameId: string, parentFrameId?: string): void {
+ if (this._frames.has(frameId)) return;
+ assert(parentFrameId);
+ const parentFrame = this._frames.get(parentFrameId);
+ const frame = new Frame(this, parentFrame, frameId);
+ this._frames.set(frame._id, frame);
+ this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
+ }
+
+ _onFrameNavigated(framePayload: Protocol.Page.Frame): void {
+ const isMainFrame = !framePayload.parentId;
+ let frame = isMainFrame
+ ? this._mainFrame
+ : this._frames.get(framePayload.id);
+ assert(
+ isMainFrame || frame,
+ 'We either navigate top level or have old version of the navigated frame'
+ );
+
+ // Detach all child frames first.
+ if (frame) {
+ for (const child of frame.childFrames())
+ this._removeFramesRecursively(child);
+ }
+
+ // Update or create main frame.
+ if (isMainFrame) {
+ if (frame) {
+ // Update frame id to retain frame identity on cross-process navigation.
+ this._frames.delete(frame._id);
+ frame._id = framePayload.id;
+ } else {
+ // Initial main frame navigation.
+ frame = new Frame(this, null, framePayload.id);
+ }
+ this._frames.set(framePayload.id, frame);
+ this._mainFrame = frame;
+ }
+
+ // Update frame payload.
+ frame._navigated(framePayload);
+
+ this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
+ }
+
+ async _ensureIsolatedWorld(name: string): Promise<void> {
+ if (this._isolatedWorlds.has(name)) return;
+ this._isolatedWorlds.add(name);
+ await this._client.send('Page.addScriptToEvaluateOnNewDocument', {
+ source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`,
+ worldName: name,
+ }),
+ await Promise.all(
+ this.frames().map((frame) =>
+ this._client
+ .send('Page.createIsolatedWorld', {
+ frameId: frame._id,
+ grantUniveralAccess: true,
+ worldName: name,
+ })
+ .catch(debugError)
+ )
+ ); // frames might be removed before we send this
+ }
+
+ _onFrameNavigatedWithinDocument(frameId: string, url: string): void {
+ const frame = this._frames.get(frameId);
+ if (!frame) return;
+ frame._navigatedWithinDocument(url);
+ this.emit(FrameManagerEmittedEvents.FrameNavigatedWithinDocument, frame);
+ this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
+ }
+
+ _onFrameDetached(frameId: string): void {
+ const frame = this._frames.get(frameId);
+ if (frame) this._removeFramesRecursively(frame);
+ }
+
+ _onExecutionContextCreated(
+ contextPayload: Protocol.Runtime.ExecutionContextDescription
+ ): void {
+ const auxData = contextPayload.auxData as { frameId?: string };
+ const frameId = auxData ? auxData.frameId : null;
+ const frame = this._frames.get(frameId) || null;
+ let world = null;
+ if (frame) {
+ if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) {
+ world = frame._mainWorld;
+ } else if (
+ contextPayload.name === UTILITY_WORLD_NAME &&
+ !frame._secondaryWorld._hasContext()
+ ) {
+ // In case of multiple sessions to the same target, there's a race between
+ // connections so we might end up creating multiple isolated worlds.
+ // We can use either.
+ world = frame._secondaryWorld;
+ }
+ }
+ if (contextPayload.auxData && contextPayload.auxData['type'] === 'isolated')
+ this._isolatedWorlds.add(contextPayload.name);
+ const context = new ExecutionContext(this._client, contextPayload, world);
+ if (world) world._setContext(context);
+ this._contextIdToContext.set(contextPayload.id, context);
+ }
+
+ private _onExecutionContextDestroyed(executionContextId: number): void {
+ const context = this._contextIdToContext.get(executionContextId);
+ if (!context) return;
+ this._contextIdToContext.delete(executionContextId);
+ if (context._world) context._world._setContext(null);
+ }
+
+ private _onExecutionContextsCleared(): void {
+ for (const context of this._contextIdToContext.values()) {
+ if (context._world) context._world._setContext(null);
+ }
+ this._contextIdToContext.clear();
+ }
+
+ executionContextById(contextId: number): ExecutionContext {
+ const context = this._contextIdToContext.get(contextId);
+ assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
+ return context;
+ }
+
+ private _removeFramesRecursively(frame: Frame): void {
+ for (const child of frame.childFrames())
+ this._removeFramesRecursively(child);
+ frame._detach();
+ this._frames.delete(frame._id);
+ this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
+ }
+}
+
+/**
+ * @public
+ */
+export interface FrameWaitForFunctionOptions {
+ /**
+ * An interval at which the `pageFunction` is executed, defaults to `raf`. If
+ * `polling` is a number, then it is treated as an interval in milliseconds at
+ * which the function would be executed. If `polling` is a string, then it can
+ * be one of the following values:
+ *
+ * - `raf` - to constantly execute `pageFunction` in `requestAnimationFrame`
+ * callback. This is the tightest polling mode which is suitable to observe
+ * styling changes.
+ *
+ * - `mutation` - to execute `pageFunction` on every DOM mutation.
+ */
+ polling?: string | number;
+ /**
+ * Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
+ * Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
+ * using {@link Page.setDefaultTimeout}.
+ */
+ timeout?: number;
+}
+
+/**
+ * @public
+ */
+export interface FrameAddScriptTagOptions {
+ /**
+ * the URL of the script to be added.
+ */
+ url?: string;
+ /**
+ * The path to a JavaScript file to be injected into the frame.
+ * @remarks
+ * If `path` is a relative path, it is resolved relative to the current
+ * working directory (`process.cwd()` in Node.js).
+ */
+ path?: string;
+ /**
+ * Raw JavaScript content to be injected into the frame.
+ */
+ content?: string;
+ /**
+ * Set the script's `type`. Use `module` in order to load an ES2015 module.
+ */
+ type?: string;
+}
+
+/**
+ * @public
+ */
+export interface FrameAddStyleTagOptions {
+ /**
+ * the URL of the CSS file to be added.
+ */
+ url?: string;
+ /**
+ * The path to a CSS file to be injected into the frame.
+ * @remarks
+ * If `path` is a relative path, it is resolved relative to the current
+ * working directory (`process.cwd()` in Node.js).
+ */
+ path?: string;
+ /**
+ * Raw CSS content to be injected into the frame.
+ */
+ content?: string;
+}
+
+/**
+ * At every point of time, page exposes its current frame tree via the
+ * {@link Page.mainFrame | page.mainFrame} and
+ * {@link Frame.childFrames | frame.childFrames} methods.
+ *
+ * @remarks
+ *
+ * `Frame` object lifecycles are controlled by three events that are all
+ * dispatched on the page object:
+ *
+ * - {@link PageEmittedEvents.FrameAttached}
+ *
+ * - {@link PageEmittedEvents.FrameNavigated}
+ *
+ * - {@link PageEmittedEvents.FrameDetached}
+ *
+ * @Example
+ * An example of dumping frame tree:
+ *
+ * ```js
+ * const puppeteer = require('puppeteer');
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.goto('https://www.google.com/chrome/browser/canary.html');
+ * dumpFrameTree(page.mainFrame(), '');
+ * await browser.close();
+ *
+ * function dumpFrameTree(frame, indent) {
+ * console.log(indent + frame.url());
+ * for (const child of frame.childFrames()) {
+ * dumpFrameTree(child, indent + ' ');
+ * }
+ * }
+ * })();
+ * ```
+ *
+ * @Example
+ * An example of getting text from an iframe element:
+ *
+ * ```js
+ * const frame = page.frames().find(frame => frame.name() === 'myframe');
+ * const text = await frame.$eval('.selector', element => element.textContent);
+ * console.log(text);
+ * ```
+ *
+ * @public
+ */
+export class Frame {
+ /**
+ * @internal
+ */
+ _frameManager: FrameManager;
+ private _parentFrame?: Frame;
+ /**
+ * @internal
+ */
+ _id: string;
+
+ private _url = '';
+ private _detached = false;
+ /**
+ * @internal
+ */
+ _loaderId = '';
+ /**
+ * @internal
+ */
+ _name?: string;
+
+ /**
+ * @internal
+ */
+ _lifecycleEvents = new Set<string>();
+ /**
+ * @internal
+ */
+ _mainWorld: DOMWorld;
+ /**
+ * @internal
+ */
+ _secondaryWorld: DOMWorld;
+ /**
+ * @internal
+ */
+ _childFrames: Set<Frame>;
+
+ /**
+ * @internal
+ */
+ constructor(
+ frameManager: FrameManager,
+ parentFrame: Frame | null,
+ frameId: string
+ ) {
+ this._frameManager = frameManager;
+ this._parentFrame = parentFrame;
+ this._url = '';
+ this._id = frameId;
+ this._detached = false;
+
+ this._loaderId = '';
+ this._mainWorld = new DOMWorld(
+ frameManager,
+ this,
+ frameManager._timeoutSettings
+ );
+ this._secondaryWorld = new DOMWorld(
+ frameManager,
+ this,
+ frameManager._timeoutSettings
+ );
+
+ this._childFrames = new Set();
+ if (this._parentFrame) this._parentFrame._childFrames.add(this);
+ }
+
+ /**
+ * @remarks
+ *
+ * `frame.goto` will throw an error if:
+ * - there's an SSL error (e.g. in case of self-signed certificates).
+ *
+ * - target URL is invalid.
+ *
+ * - the `timeout` is exceeded during navigation.
+ *
+ * - the remote server does not respond or is unreachable.
+ *
+ * - the main resource failed to load.
+ *
+ * `frame.goto` will not throw an error when any valid HTTP status code is
+ * returned by the remote server, including 404 "Not Found" and 500 "Internal
+ * Server Error". The status code for such responses can be retrieved by
+ * calling {@link HTTPResponse.status}.
+ *
+ * NOTE: `frame.goto` either throws an error or returns a main resource
+ * response. The only exceptions are navigation to `about:blank` or
+ * navigation to the same URL with a different hash, which would succeed and
+ * return `null`.
+ *
+ * NOTE: Headless mode doesn't support navigation to a PDF document. See
+ * the {@link https://bugs.chromium.org/p/chromium/issues/detail?id=761295 | upstream
+ * issue}.
+ *
+ * @param url - the URL to navigate the frame to. This should include the
+ * scheme, e.g. `https://`.
+ * @param options - navigation options. `waitUntil` is useful to define when
+ * the navigation should be considered successful - see the docs for
+ * {@link PuppeteerLifeCycleEvent} for more details.
+ *
+ * @returns A promise which resolves to the main resource response. In case of
+ * multiple redirects, the navigation will resolve with the response of the
+ * last redirect.
+ */
+ async goto(
+ url: string,
+ options: {
+ referer?: string;
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ } = {}
+ ): Promise<HTTPResponse | null> {
+ return await this._frameManager.navigateFrame(this, url, options);
+ }
+
+ /**
+ * @remarks
+ *
+ * This resolves when the frame navigates to a new URL. It is useful for when
+ * you run code which will indirectly cause the frame to navigate. Consider
+ * this example:
+ *
+ * ```js
+ * const [response] = await Promise.all([
+ * // The navigation promise resolves after navigation has finished
+ * frame.waitForNavigation(),
+ * // Clicking the link will indirectly cause a navigation
+ * frame.click('a.my-link'),
+ * ]);
+ * ```
+ *
+ * Usage of the {@link https://developer.mozilla.org/en-US/docs/Web/API/History_API | History API} to change the URL is considered a navigation.
+ *
+ * @param options - options to configure when the navigation is consided finished.
+ * @returns a promise that resolves when the frame navigates to a new URL.
+ */
+ async waitForNavigation(
+ options: {
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ } = {}
+ ): Promise<HTTPResponse | null> {
+ return await this._frameManager.waitForFrameNavigation(this, options);
+ }
+
+ /**
+ * @returns a promise that resolves to the frame's default execution context.
+ */
+ executionContext(): Promise<ExecutionContext> {
+ return this._mainWorld.executionContext();
+ }
+
+ /**
+ * @remarks
+ *
+ * The only difference between {@link Frame.evaluate} and
+ * `frame.evaluateHandle` is that `evaluateHandle` will return the value
+ * wrapped in an in-page object.
+ *
+ * This method behaves identically to {@link Page.evaluateHandle} except it's
+ * run within the context of the `frame`, rather than the entire page.
+ *
+ * @param pageFunction - a function that is run within the frame
+ * @param args - arguments to be passed to the pageFunction
+ */
+ async evaluateHandle<HandlerType extends JSHandle = JSHandle>(
+ pageFunction: EvaluateHandleFn,
+ ...args: SerializableOrJSHandle[]
+ ): Promise<HandlerType> {
+ return this._mainWorld.evaluateHandle<HandlerType>(pageFunction, ...args);
+ }
+
+ /**
+ * @remarks
+ *
+ * This method behaves identically to {@link Page.evaluate} except it's run
+ * within the context of the `frame`, rather than the entire page.
+ *
+ * @param pageFunction - a function that is run within the frame
+ * @param args - arguments to be passed to the pageFunction
+ */
+ async evaluate<T extends EvaluateFn>(
+ pageFunction: T,
+ ...args: SerializableOrJSHandle[]
+ ): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
+ return this._mainWorld.evaluate<T>(pageFunction, ...args);
+ }
+
+ /**
+ * This method queries the frame for the given selector.
+ *
+ * @param selector - a selector to query for.
+ * @returns A promise which resolves to an `ElementHandle` pointing at the
+ * element, or `null` if it was not found.
+ */
+ async $(selector: string): Promise<ElementHandle | null> {
+ return this._mainWorld.$(selector);
+ }
+
+ /**
+ * This method evaluates the given XPath expression and returns the results.
+ *
+ * @param expression - the XPath expression to evaluate.
+ */
+ async $x(expression: string): Promise<ElementHandle[]> {
+ return this._mainWorld.$x(expression);
+ }
+
+ /**
+ * @remarks
+ *
+ * This method runs `document.querySelector` within
+ * the frame and passes it as the first argument to `pageFunction`.
+ *
+ * If `pageFunction` returns a Promise, then `frame.$eval` would wait for
+ * the promise to resolve and return its value.
+ *
+ * @example
+ *
+ * ```js
+ * const searchValue = await frame.$eval('#search', el => el.value);
+ * ```
+ *
+ * @param selector - the selector to query for
+ * @param pageFunction - the function to be evaluated in the frame's context
+ * @param args - additional arguments to pass to `pageFuncton`
+ */
+ async $eval<ReturnType>(
+ selector: string,
+ pageFunction: (
+ element: Element,
+ ...args: unknown[]
+ ) => ReturnType | Promise<ReturnType>,
+ ...args: SerializableOrJSHandle[]
+ ): Promise<WrapElementHandle<ReturnType>> {
+ return this._mainWorld.$eval<ReturnType>(selector, pageFunction, ...args);
+ }
+
+ /**
+ * @remarks
+ *
+ * This method runs `Array.from(document.querySelectorAll(selector))` within
+ * the frame and passes it as the first argument to `pageFunction`.
+ *
+ * If `pageFunction` returns a Promise, then `frame.$$eval` would wait for
+ * the promise to resolve and return its value.
+ *
+ * @example
+ *
+ * ```js
+ * const divsCounts = await frame.$$eval('div', divs => divs.length);
+ * ```
+ *
+ * @param selector - the selector to query for
+ * @param pageFunction - the function to be evaluated in the frame's context
+ * @param args - additional arguments to pass to `pageFuncton`
+ */
+ async $$eval<ReturnType>(
+ selector: string,
+ pageFunction: (
+ elements: Element[],
+ ...args: unknown[]
+ ) => ReturnType | Promise<ReturnType>,
+ ...args: SerializableOrJSHandle[]
+ ): Promise<WrapElementHandle<ReturnType>> {
+ return this._mainWorld.$$eval<ReturnType>(selector, pageFunction, ...args);
+ }
+
+ /**
+ * This runs `document.querySelectorAll` in the frame and returns the result.
+ *
+ * @param selector - a selector to search for
+ * @returns An array of element handles pointing to the found frame elements.
+ */
+ async $$(selector: string): Promise<ElementHandle[]> {
+ return this._mainWorld.$$(selector);
+ }
+
+ /**
+ * @returns the full HTML contents of the frame, including the doctype.
+ */
+ async content(): Promise<string> {
+ return this._secondaryWorld.content();
+ }
+
+ /**
+ * Set the content of the frame.
+ *
+ * @param html - HTML markup to assign to the page.
+ * @param options - options to configure how long before timing out and at
+ * what point to consider the content setting successful.
+ */
+ async setContent(
+ html: string,
+ options: {
+ timeout?: number;
+ waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
+ } = {}
+ ): Promise<void> {
+ return this._secondaryWorld.setContent(html, options);
+ }
+
+ /**
+ * @remarks
+ *
+ * If the name is empty, it returns the `id` attribute instead.
+ *
+ * Note: This value is calculated once when the frame is created, and will not
+ * update if the attribute is changed later.
+ *
+ * @returns the frame's `name` attribute as specified in the tag.
+ */
+ name(): string {
+ return this._name || '';
+ }
+
+ /**
+ * @returns the frame's URL.
+ */
+ url(): string {
+ return this._url;
+ }
+
+ /**
+ * @returns the parent `Frame`, if any. Detached and main frames return `null`.
+ */
+ parentFrame(): Frame | null {
+ return this._parentFrame;
+ }
+
+ /**
+ * @returns an array of child frames.
+ */
+ childFrames(): Frame[] {
+ return Array.from(this._childFrames);
+ }
+
+ /**
+ * @returns `true` if the frame has been detached, or `false` otherwise.
+ */
+ isDetached(): boolean {
+ return this._detached;
+ }
+
+ /**
+ * Adds a `<script>` tag into the page with the desired url or content.
+ *
+ * @param options - configure the script to add to the page.
+ *
+ * @returns a promise that resolves to the added tag when the script's
+ * `onload` event fires or when the script content was injected into the
+ * frame.
+ */
+ async addScriptTag(
+ options: FrameAddScriptTagOptions
+ ): Promise<ElementHandle> {
+ return this._mainWorld.addScriptTag(options);
+ }
+
+ /**
+ * Adds a `<link rel="stylesheet">` tag into the page with the desired url or
+ * a `<style type="text/css">` tag with the content.
+ *
+ * @param options - configure the CSS to add to the page.
+ *
+ * @returns a promise that resolves to the added tag when the stylesheets's
+ * `onload` event fires or when the CSS content was injected into the
+ * frame.
+ */
+ async addStyleTag(options: FrameAddStyleTagOptions): Promise<ElementHandle> {
+ return this._mainWorld.addStyleTag(options);
+ }
+
+ /**
+ *
+ * This method clicks the first element found that matches `selector`.
+ *
+ * @remarks
+ *
+ * This method scrolls the element into view if needed, and then uses
+ * {@link Page.mouse} to click in the center of the element. If there's no
+ * element matching `selector`, the method throws an error.
+ *
+ * Bear in mind that if `click()` triggers a navigation event and there's a
+ * separate `page.waitForNavigation()` promise to be resolved, you may end up
+ * with a race condition that yields unexpected results. The correct pattern
+ * for click and wait for navigation is the following:
+ *
+ * ```javascript
+ * const [response] = await Promise.all([
+ * page.waitForNavigation(waitOptions),
+ * frame.click(selector, clickOptions),
+ * ]);
+ * ```
+ * @param selector - the selector to search for to click. If there are
+ * multiple elements, the first will be clicked.
+ */
+ async click(
+ selector: string,
+ options: {
+ delay?: number;
+ button?: MouseButton;
+ clickCount?: number;
+ } = {}
+ ): Promise<void> {
+ return this._secondaryWorld.click(selector, options);
+ }
+
+ /**
+ * This method fetches an element with `selector` and focuses it.
+ *
+ * @remarks
+ * If there's no element matching `selector`, the method throws an error.
+ *
+ * @param selector - the selector for the element to focus. If there are
+ * multiple elements, the first will be focused.
+ */
+ async focus(selector: string): Promise<void> {
+ return this._secondaryWorld.focus(selector);
+ }
+
+ /**
+ * This method fetches an element with `selector`, scrolls it into view if
+ * needed, and then uses {@link Page.mouse} to hover over the center of the
+ * element.
+ *
+ * @remarks
+ * If there's no element matching `selector`, the method throws an
+ *
+ * @param selector - the selector for the element to hover. If there are
+ * multiple elements, the first will be hovered.
+ */
+ async hover(selector: string): Promise<void> {
+ return this._secondaryWorld.hover(selector);
+ }
+
+ /**
+ * Triggers a `change` and `input` event once all the provided options have
+ * been selected.
+ *
+ * @remarks
+ *
+ * If there's no `<select>` element matching `selector`, the
+ * method throws an error.
+ *
+ * @example
+ * ```js
+ * frame.select('select#colors', 'blue'); // single selection
+ * frame.select('select#colors', 'red', 'green', 'blue'); // multiple selections
+ * ```
+ *
+ * @param selector - a selector to query the frame for
+ * @param values - an array of values to select. If the `<select>` has the
+ * `multiple` attribute, all values are considered, otherwise only the first
+ * one is taken into account.
+ * @returns the list of values that were successfully selected.
+ */
+ select(selector: string, ...values: string[]): Promise<string[]> {
+ return this._secondaryWorld.select(selector, ...values);
+ }
+
+ /**
+ * This method fetches an element with `selector`, scrolls it into view if
+ * needed, and then uses {@link Page.touchscreen} to tap in the center of the
+ * element.
+ *
+ * @remarks
+ *
+ * If there's no element matching `selector`, the method throws an error.
+ *
+ * @param selector - the selector to tap.
+ * @returns a promise that resolves when the element has been tapped.
+ */
+ async tap(selector: string): Promise<void> {
+ return this._secondaryWorld.tap(selector);
+ }
+
+ /**
+ * Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character
+ * in the text.
+ *
+ * @remarks
+ * To press a special key, like `Control` or `ArrowDown`, use
+ * {@link Keyboard.press}.
+ *
+ * @example
+ * ```js
+ * await frame.type('#mytextarea', 'Hello'); // Types instantly
+ * await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a user
+ * ```
+ *
+ * @param selector - the selector for the element to type into. If there are
+ * multiple the first will be used.
+ * @param text - text to type into the element
+ * @param options - takes one option, `delay`, which sets the time to wait
+ * between key presses in milliseconds. Defaults to `0`.
+ *
+ * @returns a promise that resolves when the typing is complete.
+ */
+ async type(
+ selector: string,
+ text: string,
+ options?: { delay: number }
+ ): Promise<void> {
+ return this._mainWorld.type(selector, text, options);
+ }
+
+ /**
+ * @remarks
+ *
+ * This method behaves differently depending on the first parameter. If it's a
+ * `string`, it will be treated as a `selector` or `xpath` (if the string
+ * starts with `//`). This method then is a shortcut for
+ * {@link Frame.waitForSelector} or {@link Frame.waitForXPath}.
+ *
+ * If the first argument is a function this method is a shortcut for
+ * {@link Frame.waitForFunction}.
+ *
+ * If the first argument is a `number`, it's treated as a timeout in
+ * milliseconds and the method returns a promise which resolves after the
+ * timeout.
+ *
+ * @param selectorOrFunctionOrTimeout - a selector, predicate or timeout to
+ * wait for.
+ * @param options - optional waiting parameters.
+ * @param args - arguments to pass to `pageFunction`.
+ *
+ * @deprecated Don't use this method directly. Instead use the more explicit
+ * methods available: {@link Frame.waitForSelector},
+ * {@link Frame.waitForXPath}, {@link Frame.waitForFunction} or
+ * {@link Frame.waitForTimeout}.
+ */
+ waitFor(
+ selectorOrFunctionOrTimeout: string | number | Function,
+ options: Record<string, unknown> = {},
+ ...args: SerializableOrJSHandle[]
+ ): Promise<JSHandle | null> {
+ const xPathPattern = '//';
+
+ console.warn(
+ 'waitFor is deprecated and will be removed in a future release. See https://github.com/puppeteer/puppeteer/issues/6214 for details and how to migrate your code.'
+ );
+
+ if (helper.isString(selectorOrFunctionOrTimeout)) {
+ const string = selectorOrFunctionOrTimeout;
+ if (string.startsWith(xPathPattern))
+ return this.waitForXPath(string, options);
+ return this.waitForSelector(string, options);
+ }
+ if (helper.isNumber(selectorOrFunctionOrTimeout))
+ return new Promise((fulfill) =>
+ setTimeout(fulfill, selectorOrFunctionOrTimeout)
+ );
+ if (typeof selectorOrFunctionOrTimeout === 'function')
+ return this.waitForFunction(
+ selectorOrFunctionOrTimeout,
+ options,
+ ...args
+ );
+ return Promise.reject(
+ new Error(
+ 'Unsupported target type: ' + typeof selectorOrFunctionOrTimeout
+ )
+ );
+ }
+
+ /**
+ * Causes your script to wait for the given number of milliseconds.
+ *
+ * @remarks
+ * It's generally recommended to not wait for a number of seconds, but instead
+ * use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or
+ * {@link Frame.waitForFunction} to wait for exactly the conditions you want.
+ *
+ * @example
+ *
+ * Wait for 1 second:
+ *
+ * ```
+ * await frame.waitForTimeout(1000);
+ * ```
+ *
+ * @param milliseconds - the number of milliseconds to wait.
+ */
+ waitForTimeout(milliseconds: number): Promise<void> {
+ return new Promise((resolve) => {
+ setTimeout(resolve, milliseconds);
+ });
+ }
+
+ /**
+ * @remarks
+ *
+ *
+ * Wait for the `selector` to appear in page. If at the moment of calling the
+ * method the `selector` already exists, the method will return immediately.
+ * If the selector doesn't appear after the `timeout` milliseconds of waiting,
+ * the function will throw.
+ *
+ * This method works across navigations.
+ *
+ * @example
+ * ```js
+ * const puppeteer = require('puppeteer');
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * let currentURL;
+ * page.mainFrame()
+ * .waitForSelector('img')
+ * .then(() => console.log('First URL with image: ' + currentURL));
+ *
+ * for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) {
+ * await page.goto(currentURL);
+ * }
+ * await browser.close();
+ * })();
+ * ```
+ * @param selector - the selector to wait for.
+ * @param options - options to define if the element should be visible and how
+ * long to wait before timing out.
+ * @returns a promise which resolves when an element matching the selector
+ * string is added to the DOM.
+ */
+ async waitForSelector(
+ selector: string,
+ options: WaitForSelectorOptions = {}
+ ): Promise<ElementHandle | null> {
+ const handle = await this._secondaryWorld.waitForSelector(
+ selector,
+ options
+ );
+ if (!handle) return null;
+ const mainExecutionContext = await this._mainWorld.executionContext();
+ const result = await mainExecutionContext._adoptElementHandle(handle);
+ await handle.dispose();
+ return result;
+ }
+
+ /**
+ * @remarks
+ * Wait for the `xpath` to appear in page. If at the moment of calling the
+ * method the `xpath` already exists, the method will return immediately. If
+ * the xpath doesn't appear after the `timeout` milliseconds of waiting, the
+ * function will throw.
+ *
+ * For a code example, see the example for {@link Frame.waitForSelector}. That
+ * function behaves identically other than taking a CSS selector rather than
+ * an XPath.
+ *
+ * @param xpath - the XPath expression to wait for.
+ * @param options - options to configure the visiblity of the element and how
+ * long to wait before timing out.
+ */
+ async waitForXPath(
+ xpath: string,
+ options: WaitForSelectorOptions = {}
+ ): Promise<ElementHandle | null> {
+ const handle = await this._secondaryWorld.waitForXPath(xpath, options);
+ if (!handle) return null;
+ const mainExecutionContext = await this._mainWorld.executionContext();
+ const result = await mainExecutionContext._adoptElementHandle(handle);
+ await handle.dispose();
+ return result;
+ }
+
+ /**
+ * @remarks
+ *
+ * @example
+ *
+ * The `waitForFunction` can be used to observe viewport size change:
+ * ```js
+ * const puppeteer = require('puppeteer');
+ *
+ * (async () => {
+ * . const browser = await puppeteer.launch();
+ * . const page = await browser.newPage();
+ * . const watchDog = page.mainFrame().waitForFunction('window.innerWidth < 100');
+ * . page.setViewport({width: 50, height: 50});
+ * . await watchDog;
+ * . await browser.close();
+ * })();
+ * ```
+ *
+ * To pass arguments from Node.js to the predicate of `page.waitForFunction` function:
+ *
+ * ```js
+ * const selector = '.foo';
+ * await frame.waitForFunction(
+ * selector => !!document.querySelector(selector),
+ * {}, // empty options object
+ * selector
+ *);
+ * ```
+ *
+ * @param pageFunction - the function to evaluate in the frame context.
+ * @param options - options to configure the polling method and timeout.
+ * @param args - arguments to pass to the `pageFunction`.
+ * @returns the promise which resolve when the `pageFunction` returns a truthy value.
+ */
+ waitForFunction(
+ pageFunction: Function | string,
+ options: FrameWaitForFunctionOptions = {},
+ ...args: SerializableOrJSHandle[]
+ ): Promise<JSHandle> {
+ return this._mainWorld.waitForFunction(pageFunction, options, ...args);
+ }
+
+ /**
+ * @returns the frame's title.
+ */
+ async title(): Promise<string> {
+ return this._secondaryWorld.title();
+ }
+
+ /**
+ * @internal
+ */
+ _navigated(framePayload: Protocol.Page.Frame): void {
+ this._name = framePayload.name;
+ this._url = `${framePayload.url}${framePayload.urlFragment || ''}`;
+ }
+
+ /**
+ * @internal
+ */
+ _navigatedWithinDocument(url: string): void {
+ this._url = url;
+ }
+
+ /**
+ * @internal
+ */
+ _onLifecycleEvent(loaderId: string, name: string): void {
+ if (name === 'init') {
+ this._loaderId = loaderId;
+ this._lifecycleEvents.clear();
+ }
+ this._lifecycleEvents.add(name);
+ }
+
+ /**
+ * @internal
+ */
+ _onLoadingStopped(): void {
+ this._lifecycleEvents.add('DOMContentLoaded');
+ this._lifecycleEvents.add('load');
+ }
+
+ /**
+ * @internal
+ */
+ _detach(): void {
+ this._detached = true;
+ this._mainWorld._detach();
+ this._secondaryWorld._detach();
+ if (this._parentFrame) this._parentFrame._childFrames.delete(this);
+ this._parentFrame = null;
+ }
+}
+
+function assertNoLegacyNavigationOptions(options: {
+ [optionName: string]: unknown;
+}): void {
+ assert(
+ options['networkIdleTimeout'] === undefined,
+ 'ERROR: networkIdleTimeout option is no longer supported.'
+ );
+ assert(
+ options['networkIdleInflight'] === undefined,
+ 'ERROR: networkIdleInflight option is no longer supported.'
+ );
+ assert(
+ options.waitUntil !== 'networkidle',
+ 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead'
+ );
+}