123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717 |
- /**
- * 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.
- */
- const EventEmitter = require('events');
- const {helper, assert, debugError} = require('./helper');
- const {Events} = require('./Events');
- const {ExecutionContext, EVALUATION_SCRIPT_URL} = require('./ExecutionContext');
- const {LifecycleWatcher} = require('./LifecycleWatcher');
- const {DOMWorld} = require('./DOMWorld');
- const {NetworkManager} = require('./NetworkManager');
- const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
- class FrameManager extends EventEmitter {
- /**
- * @param {!Puppeteer.CDPSession} client
- * @param {!Puppeteer.Page} page
- * @param {boolean} ignoreHTTPSErrors
- * @param {!Puppeteer.TimeoutSettings} timeoutSettings
- */
- constructor(client, page, ignoreHTTPSErrors, timeoutSettings) {
- super();
- this._client = client;
- this._page = page;
- this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
- this._timeoutSettings = timeoutSettings;
- /** @type {!Map<string, !Frame>} */
- this._frames = new Map();
- /** @type {!Map<number, !ExecutionContext>} */
- this._contextIdToContext = new Map();
- /** @type {!Set<string>} */
- this._isolatedWorlds = new Set();
- 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', event => this._onExecutionContextsCleared());
- this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
- }
- async initialize() {
- const [,{frameTree}] = await Promise.all([
- this._client.send('Page.enable'),
- this._client.send('Page.getFrameTree'),
- ]);
- 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(),
- ]);
- }
- /**
- * @return {!NetworkManager}
- */
- networkManager() {
- return this._networkManager;
- }
- /**
- * @param {!Puppeteer.Frame} frame
- * @param {string} url
- * @param {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
- * @return {!Promise<?Puppeteer.Response>}
- */
- async navigateFrame(frame, url, options = {}) {
- 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();
- /**
- * @param {!Puppeteer.CDPSession} client
- * @param {string} url
- * @param {string} referrer
- * @param {string} frameId
- * @return {!Promise<?Error>}
- */
- async function navigate(client, url, referrer, frameId) {
- 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;
- }
- }
- }
- /**
- * @param {!Puppeteer.Frame} frame
- * @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
- * @return {!Promise<?Puppeteer.Response>}
- */
- async waitForFrameNavigation(frame, options = {}) {
- 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();
- }
- /**
- * @param {!Protocol.Page.lifecycleEventPayload} event
- */
- _onLifecycleEvent(event) {
- const frame = this._frames.get(event.frameId);
- if (!frame)
- return;
- frame._onLifecycleEvent(event.loaderId, event.name);
- this.emit(Events.FrameManager.LifecycleEvent, frame);
- }
- /**
- * @param {string} frameId
- */
- _onFrameStoppedLoading(frameId) {
- const frame = this._frames.get(frameId);
- if (!frame)
- return;
- frame._onLoadingStopped();
- this.emit(Events.FrameManager.LifecycleEvent, frame);
- }
- /**
- * @param {!Protocol.Page.FrameTree} frameTree
- */
- _handleFrameTree(frameTree) {
- 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);
- }
- /**
- * @return {!Puppeteer.Page}
- */
- page() {
- return this._page;
- }
- /**
- * @return {!Frame}
- */
- mainFrame() {
- return this._mainFrame;
- }
- /**
- * @return {!Array<!Frame>}
- */
- frames() {
- return Array.from(this._frames.values());
- }
- /**
- * @param {!string} frameId
- * @return {?Frame}
- */
- frame(frameId) {
- return this._frames.get(frameId) || null;
- }
- /**
- * @param {string} frameId
- * @param {?string} parentFrameId
- */
- _onFrameAttached(frameId, parentFrameId) {
- if (this._frames.has(frameId))
- return;
- assert(parentFrameId);
- const parentFrame = this._frames.get(parentFrameId);
- const frame = new Frame(this, this._client, parentFrame, frameId);
- this._frames.set(frame._id, frame);
- this.emit(Events.FrameManager.FrameAttached, frame);
- }
- /**
- * @param {!Protocol.Page.Frame} framePayload
- */
- _onFrameNavigated(framePayload) {
- 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, this._client, null, framePayload.id);
- }
- this._frames.set(framePayload.id, frame);
- this._mainFrame = frame;
- }
- // Update frame payload.
- frame._navigated(framePayload);
- this.emit(Events.FrameManager.FrameNavigated, frame);
- }
- /**
- * @param {string} name
- */
- async _ensureIsolatedWorld(name) {
- 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
- }
- /**
- * @param {string} frameId
- * @param {string} url
- */
- _onFrameNavigatedWithinDocument(frameId, url) {
- const frame = this._frames.get(frameId);
- if (!frame)
- return;
- frame._navigatedWithinDocument(url);
- this.emit(Events.FrameManager.FrameNavigatedWithinDocument, frame);
- this.emit(Events.FrameManager.FrameNavigated, frame);
- }
- /**
- * @param {string} frameId
- */
- _onFrameDetached(frameId) {
- const frame = this._frames.get(frameId);
- if (frame)
- this._removeFramesRecursively(frame);
- }
- _onExecutionContextCreated(contextPayload) {
- const frameId = contextPayload.auxData ? contextPayload.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);
- /** @type {!ExecutionContext} */
- const context = new ExecutionContext(this._client, contextPayload, world);
- if (world)
- world._setContext(context);
- this._contextIdToContext.set(contextPayload.id, context);
- }
- /**
- * @param {number} executionContextId
- */
- _onExecutionContextDestroyed(executionContextId) {
- const context = this._contextIdToContext.get(executionContextId);
- if (!context)
- return;
- this._contextIdToContext.delete(executionContextId);
- if (context._world)
- context._world._setContext(null);
- }
- _onExecutionContextsCleared() {
- for (const context of this._contextIdToContext.values()) {
- if (context._world)
- context._world._setContext(null);
- }
- this._contextIdToContext.clear();
- }
- /**
- * @param {number} contextId
- * @return {!ExecutionContext}
- */
- executionContextById(contextId) {
- const context = this._contextIdToContext.get(contextId);
- assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
- return context;
- }
- /**
- * @param {!Frame} frame
- */
- _removeFramesRecursively(frame) {
- for (const child of frame.childFrames())
- this._removeFramesRecursively(child);
- frame._detach();
- this._frames.delete(frame._id);
- this.emit(Events.FrameManager.FrameDetached, frame);
- }
- }
- /**
- * @unrestricted
- */
- class Frame {
- /**
- * @param {!FrameManager} frameManager
- * @param {!Puppeteer.CDPSession} client
- * @param {?Frame} parentFrame
- * @param {string} frameId
- */
- constructor(frameManager, client, parentFrame, frameId) {
- this._frameManager = frameManager;
- this._client = client;
- this._parentFrame = parentFrame;
- this._url = '';
- this._id = frameId;
- this._detached = false;
- this._loaderId = '';
- /** @type {!Set<string>} */
- this._lifecycleEvents = new Set();
- /** @type {!DOMWorld} */
- this._mainWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings);
- /** @type {!DOMWorld} */
- this._secondaryWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings);
- /** @type {!Set<!Frame>} */
- this._childFrames = new Set();
- if (this._parentFrame)
- this._parentFrame._childFrames.add(this);
- }
- /**
- * @param {string} url
- * @param {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
- * @return {!Promise<?Puppeteer.Response>}
- */
- async goto(url, options) {
- return await this._frameManager.navigateFrame(this, url, options);
- }
- /**
- * @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
- * @return {!Promise<?Puppeteer.Response>}
- */
- async waitForNavigation(options) {
- return await this._frameManager.waitForFrameNavigation(this, options);
- }
- /**
- * @return {!Promise<!ExecutionContext>}
- */
- executionContext() {
- return this._mainWorld.executionContext();
- }
- /**
- * @param {Function|string} pageFunction
- * @param {!Array<*>} args
- * @return {!Promise<!Puppeteer.JSHandle>}
- */
- async evaluateHandle(pageFunction, ...args) {
- return this._mainWorld.evaluateHandle(pageFunction, ...args);
- }
- /**
- * @param {Function|string} pageFunction
- * @param {!Array<*>} args
- * @return {!Promise<*>}
- */
- async evaluate(pageFunction, ...args) {
- return this._mainWorld.evaluate(pageFunction, ...args);
- }
- /**
- * @param {string} selector
- * @return {!Promise<?Puppeteer.ElementHandle>}
- */
- async $(selector) {
- return this._mainWorld.$(selector);
- }
- /**
- * @param {string} expression
- * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
- */
- async $x(expression) {
- return this._mainWorld.$x(expression);
- }
- /**
- * @param {string} selector
- * @param {Function|string} pageFunction
- * @param {!Array<*>} args
- * @return {!Promise<(!Object|undefined)>}
- */
- async $eval(selector, pageFunction, ...args) {
- return this._mainWorld.$eval(selector, pageFunction, ...args);
- }
- /**
- * @param {string} selector
- * @param {Function|string} pageFunction
- * @param {!Array<*>} args
- * @return {!Promise<(!Object|undefined)>}
- */
- async $$eval(selector, pageFunction, ...args) {
- return this._mainWorld.$$eval(selector, pageFunction, ...args);
- }
- /**
- * @param {string} selector
- * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
- */
- async $$(selector) {
- return this._mainWorld.$$(selector);
- }
- /**
- * @return {!Promise<String>}
- */
- async content() {
- return this._secondaryWorld.content();
- }
- /**
- * @param {string} html
- * @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
- */
- async setContent(html, options = {}) {
- return this._secondaryWorld.setContent(html, options);
- }
- /**
- * @return {string}
- */
- name() {
- return this._name || '';
- }
- /**
- * @return {string}
- */
- url() {
- return this._url;
- }
- /**
- * @return {?Frame}
- */
- parentFrame() {
- return this._parentFrame;
- }
- /**
- * @return {!Array.<!Frame>}
- */
- childFrames() {
- return Array.from(this._childFrames);
- }
- /**
- * @return {boolean}
- */
- isDetached() {
- return this._detached;
- }
- /**
- * @param {!{url?: string, path?: string, content?: string, type?: string}} options
- * @return {!Promise<!Puppeteer.ElementHandle>}
- */
- async addScriptTag(options) {
- return this._mainWorld.addScriptTag(options);
- }
- /**
- * @param {!{url?: string, path?: string, content?: string}} options
- * @return {!Promise<!Puppeteer.ElementHandle>}
- */
- async addStyleTag(options) {
- return this._mainWorld.addStyleTag(options);
- }
- /**
- * @param {string} selector
- * @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
- */
- async click(selector, options) {
- return this._secondaryWorld.click(selector, options);
- }
- /**
- * @param {string} selector
- */
- async focus(selector) {
- return this._secondaryWorld.focus(selector);
- }
- /**
- * @param {string} selector
- */
- async hover(selector) {
- return this._secondaryWorld.hover(selector);
- }
- /**
- * @param {string} selector
- * @param {!Array<string>} values
- * @return {!Promise<!Array<string>>}
- */
- select(selector, ...values){
- return this._secondaryWorld.select(selector, ...values);
- }
- /**
- * @param {string} selector
- */
- async tap(selector) {
- return this._secondaryWorld.tap(selector);
- }
- /**
- * @param {string} selector
- * @param {string} text
- * @param {{delay: (number|undefined)}=} options
- */
- async type(selector, text, options) {
- return this._mainWorld.type(selector, text, options);
- }
- /**
- * @param {(string|number|Function)} selectorOrFunctionOrTimeout
- * @param {!Object=} options
- * @param {!Array<*>} args
- * @return {!Promise<?Puppeteer.JSHandle>}
- */
- waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
- const xPathPattern = '//';
- if (helper.isString(selectorOrFunctionOrTimeout)) {
- const string = /** @type {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, /** @type {number} */ (selectorOrFunctionOrTimeout)));
- if (typeof selectorOrFunctionOrTimeout === 'function')
- return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
- return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
- }
- /**
- * @param {string} selector
- * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
- * @return {!Promise<?Puppeteer.ElementHandle>}
- */
- async waitForSelector(selector, options) {
- 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;
- }
- /**
- * @param {string} xpath
- * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
- * @return {!Promise<?Puppeteer.ElementHandle>}
- */
- async waitForXPath(xpath, options) {
- 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;
- }
- /**
- * @param {Function|string} pageFunction
- * @param {!{polling?: string|number, timeout?: number}=} options
- * @return {!Promise<!Puppeteer.JSHandle>}
- */
- waitForFunction(pageFunction, options = {}, ...args) {
- return this._mainWorld.waitForFunction(pageFunction, options, ...args);
- }
- /**
- * @return {!Promise<string>}
- */
- async title() {
- return this._secondaryWorld.title();
- }
- /**
- * @param {!Protocol.Page.Frame} framePayload
- */
- _navigated(framePayload) {
- this._name = framePayload.name;
- // TODO(lushnikov): remove this once requestInterception has loaderId exposed.
- this._navigationURL = framePayload.url;
- this._url = framePayload.url;
- }
- /**
- * @param {string} url
- */
- _navigatedWithinDocument(url) {
- this._url = url;
- }
- /**
- * @param {string} loaderId
- * @param {string} name
- */
- _onLifecycleEvent(loaderId, name) {
- if (name === 'init') {
- this._loaderId = loaderId;
- this._lifecycleEvents.clear();
- }
- this._lifecycleEvents.add(name);
- }
- _onLoadingStopped() {
- this._lifecycleEvents.add('DOMContentLoaded');
- this._lifecycleEvents.add('load');
- }
- _detach() {
- this._detached = true;
- this._mainWorld._detach();
- this._secondaryWorld._detach();
- if (this._parentFrame)
- this._parentFrame._childFrames.delete(this);
- this._parentFrame = null;
- }
- }
- function assertNoLegacyNavigationOptions(options) {
- 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');
- }
- module.exports = {FrameManager, Frame};
|