UNPKG

chrome-remote-interface-extra

Version:

Bringing a puppeteer like API and more to the chrome-remote-interface by cyrus-and

303 lines (270 loc) 7.8 kB
const { helper, assert } = require('./helper') const Events = require('./Events') const { TimeoutError } = require('./Errors') /** * An utility class that watches the supplied frame and its children (if any) * to determine if they reach the specified lifecycle(s) * * Lifecycle mapping in the form of supplied to CDP value: * - load: load * - domcontentloaded: DOMContentLoaded * - networkIdle: networkIdle * - networkAlmostIdle: networkAlmostIdle * - networkidle0: networkIdle * - networkidle2: networkAlmostIdle */ class LifecycleWatcher { /** * @param {!FrameManager} frameManager - The frame manager for the page containing the frame being navigated * @param {!Frame} frame - The frame being navigated * @param {string|Array<string>} waitUntil - The lifecycle(s) desired to be obtained by the frame and its children * @param {number} [timeout] - An optional timeout value */ constructor (frameManager, frame, waitUntil, timeout) { let waitUntilArray if (Array.isArray(waitUntil)) { waitUntilArray = waitUntil.slice() } else if (typeof waitUntil === 'string') { waitUntilArray = [waitUntil] } /** * @type {Array<string>} * @private */ this._expectedLifecycle = waitUntilArray.map(value => { const protocolEvent = protocolLifecycle[value] assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value) return protocolEvent }) /** * @type {!FrameManager} * @private */ this._frameManager = frameManager /** * @type {?NetworkManager} * @private */ this._networkManager = frameManager._networkManager /** * @type {!Frame} * @private */ this._frame = frame /** * @type {string} * @private */ this._initialLoaderId = frame._loaderId /** * @type {number} * @private */ this._timeout = timeout /** @type {?Request} */ this._navigationRequest = null /** * @type {{emitter: !EventEmitter, eventName: (string|symbol), handler: (function(*))}[]} * @private */ this._eventListeners = [ helper.addEventListener( frameManager._client, frameManager._client.$$disconnectEvent || Events.CRIClient.Disconnected, this._onConnectionDisconnected.bind(this) ), helper.addEventListener( this._frameManager, Events.FrameManager.LifecycleEvent, this._checkLifecycleComplete.bind(this) ), helper.addEventListener( this._frameManager, Events.FrameManager.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this) ), helper.addEventListener( this._frameManager, Events.FrameManager.FrameDetached, this._onFrameDetached.bind(this) ) ] if (this._networkManager) { this._eventListeners.push( helper.addEventListener( this._networkManager, Events.NetworkManager.Request, this._onRequest.bind(this) ) ) } /** * A Promise that resolves if the frame navigated within the same document (History.pushState etc) * @type {Promise<*>} */ this._sameDocumentNavigationPromise = new Promise(resolve => { this._sameDocumentNavigationCompleteCallback = resolve }) /** * A Promise that resolves if the frame being navigated reached the expected lifecycle * @type {Promise<*>} */ this._lifecyclePromise = new Promise(resolve => { this._lifecycleCallback = resolve }) /** * A Promise that resolves if the frame being navigated navigated to a new page * @type {Promise<*>} */ this._newDocumentNavigationPromise = new Promise(resolve => { this._newDocumentNavigationCompleteCallback = resolve }) /** * A Promise that resolves if the frame being navigated did not navigate within the * supplied timeout if any * @type {Promise<*>} */ this._timeoutPromise = this._createTimeoutPromise() /** * A Promise that resolves if the the watcher is terminated * @type {Promise<*>} */ this._terminationPromise = new Promise(resolve => { this._terminationCallback = resolve }) this._checkLifecycleComplete() } _onConnectionDisconnected () { this._terminate( new Error('Navigation failed because browser has disconnected!') ) } /** * @param {!Request} request */ _onRequest (request) { if (request.frame() !== this._frame || !request.isNavigationRequest()) { return } this._navigationRequest = request } /** * @param {!Frame} frame */ _onFrameDetached (frame) { if (this._frame === frame) { this._terminationCallback.call( null, new Error('Navigating frame was detached') ) return } this._checkLifecycleComplete() } /** * @return {?Response} */ navigationResponse () { return this._navigationRequest ? this._navigationRequest.response() : null } /** * @param {!Error} error */ _terminate (error) { this._terminationCallback.call(null, error) } /** * @return {Promise<Error|undefined>} */ sameDocumentNavigationPromise () { return this._sameDocumentNavigationPromise } /** * @return {Promise<Error|undefined>} */ newDocumentNavigationPromise () { return this._newDocumentNavigationPromise } /** * @return {Promise<*>} */ lifecyclePromise () { return this._lifecyclePromise } /** * @return {Promise<Error|undefined>} */ timeoutOrTerminationPromise () { return Promise.race([this._timeoutPromise, this._terminationPromise]) } /** * @return {Promise<Error|undefined>} */ _createTimeoutPromise () { if (!this._timeout) return new Promise(() => {}) const errorMessage = 'Navigation Timeout Exceeded: ' + this._timeout + 'ms exceeded' return new Promise(resolve => { this._maximumTimer = setTimeout(resolve, this._timeout) }).then(() => new TimeoutError(errorMessage)) } /** * @param {!Frame} frame */ _navigatedWithinDocument (frame) { if (frame !== this._frame) return this._hasSameDocumentNavigation = true this._checkLifecycleComplete() } /** * Checks the frame being navigated and all its child frames for the expected lifecycle(s) * @private */ _checkLifecycleComplete () { // We expect navigation to commit. if (!checkLifecycle(this._frame, this._expectedLifecycle)) return this._lifecycleCallback() if ( this._frame._loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation ) { return } if (this._hasSameDocumentNavigation) { this._sameDocumentNavigationCompleteCallback() } if (this._frame._loaderId !== this._initialLoaderId) { this._newDocumentNavigationCompleteCallback() } } /** * Dispose of the LifecycleWatcher (i.e. clean up) */ dispose () { helper.removeEventListeners(this._eventListeners) clearTimeout(this._maximumTimer) } } /** * @param {!Frame} frame * @param {Array<string>} expectedLifecycle * @return {boolean} */ function checkLifecycle (frame, expectedLifecycle) { let i = 0 for (; i < expectedLifecycle.length; i++) { if (!frame._lifecycleEvents.has(expectedLifecycle[i])) return false } const childFrames = frame.childFrames() for (i = 0; i < childFrames.length; i++) { if (!checkLifecycle(childFrames[i], expectedLifecycle)) return false } return true } const protocolLifecycle = { load: 'load', domcontentloaded: 'DOMContentLoaded', networkidle0: 'networkIdle', networkidle2: 'networkAlmostIdle' } module.exports = LifecycleWatcher