UNPKG

chrome-remote-interface-extra

Version:

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

1,722 lines (1,567 loc) 59.9 kB
const util = require('util') const EventEmitter = require('eventemitter3') const fs = require('fs-extra') const mime = require('mime') const ConsoleMessage = require('../ConsoleMessage') const Dialog = require('../Dialog') const EmulationManager = require('../EmulationManager') const Events = require('../Events') const LogEntry = require('./LogEntry') const Tracing = require('../Tracing') const TimeoutSettings = require('../TimeoutSettings') const Accessibility = require('../accessibility/Accessibility') const AnimationManager = require('../animations/AnimationManager') const Coverage = require('../coverage/Coverage') const DatabaseManager = require('../database/DatabaseManager') const FrameManager = require('../frames/FrameManager') const { Keyboard, Mouse, Touchscreen } = require('../input') const { createJSHandle } = require('../JSHandle') const NetworkManager = require('../network/NetworkManager') const SecurityManager = require('../SecurityManager') const TaskQueue = require('../TaskQueue') const WorkerManager = require('../workers/WorkerManager') const { helper, debugError, assert } = require('../helper') const { ensureCookie } = require('../__shared') /** * @typedef {Object} EnabledExtras * @property {?boolean} [animation = false] * @property {?boolean} [console = false] * @property {?boolean} [coverage = false] * @property {?boolean} [database = false] * @property {?boolean} [log = false] * @property {?boolean} [performance = false] * @property {?boolean} [security = false] * @property {?boolean} [serviceWorkers = false] * @property {?boolean} [workers = false] */ /** * @typedef {Object} PageInitOptions * @property {?Target} [target] * @property {?Object} [defaultViewPort] * @property {?TaskQueue} [screenshotTaskQueue] * @property {?EnabledExtras} [additionalDomains] */ /** * @typedef {Object} CreatePageOpts * @property {boolean} [ignoreHTTPSErrors] * @property {?Target} [target] * @property {?Object} [defaultViewPort] * @property {?TaskQueue} [screenshotTaskQueue] * @property {?EnabledExtras} [additionalDomains] */ /** * @type {EnabledExtras} */ const DefaultEnabledOptions = { animation: false, console: false, coverage: false, database: false, log: false, performance: false, security: false, serviceWorkers: false, workers: false } class Page extends EventEmitter { /** * @param {CDPSession|CRIConnection|Chrome|Object} client * @param {CreatePageOpts} [optionals] * @return {Promise<Page>} */ static async create (client, optionals = {}) { const { target, defaultViewport, screenshotTaskQueue = new TaskQueue(), additionalDomains, ignoreHTTPSErrors = false } = optionals /** * @type {EnabledExtras} */ const enableExtraDomains = Object.assign( {}, DefaultEnabledOptions, additionalDomains ) const page = new Page(client, { target, screenshotTaskQueue, additionalDomains: enableExtraDomains }) await Promise.all([ page.frameManager.initialize(), page.networkManager.initialize(), page.securityManager.initialize({ enable: enableExtraDomains.security, ignoreHTTPSErrors }), enableExtraDomains.database ? page.databaseManager.enable() : Promise.resolve(), page.workerManager.initialize({ workers: enableExtraDomains.workers, serviceWorkers: enableExtraDomains.serviceWorkers }), enableExtraDomains.log ? client.send('Log.enable', {}) : Promise.resolve(), enableExtraDomains.performance ? client.send('Performance.enable', {}) : Promise.resolve(), enableExtraDomains.animation ? page.animationManager.enable() : Promise.resolve() ]) // Initialize default page size. if (defaultViewport) { await page.setViewport(defaultViewport) } else { const { visualViewport } = await page.getLayoutMetrics() page._viewport = { width: visualViewport.clientWidth, height: visualViewport.clientHeight, deviceScaleFactor: visualViewport.scale } } if (page.target() == null) { // we dont have a target await client.send('Target.setDiscoverTargets', { discover: true }) } return page } /** * @param {Chrome|CRIConnection|CDPSession|Object} client * @param {PageInitOptions} initOpts */ constructor (client, initOpts) { super() /** @type {Chrome|CRIConnection|CDPSession|Object} */ this._client = client /** @type {boolean} */ this._closed = false /** @type {EnabledExtras} */ this._additionalDomains = initOpts.additionalDomains /** @type {TimeoutSettings} */ this._timeoutSettings = new TimeoutSettings() /** @type {?Target} */ this._target = initOpts.target /** @type {?string} */ this._targetId = null /** @type {Keyboard} */ this._keyboard = new Keyboard(client) /** @type {Mouse} */ this._mouse = new Mouse(client, this._keyboard) /** @type {Touchscreen} */ this._touchscreen = new Touchscreen(client, this._keyboard) /** @type {Accessibility} */ this._accessibility = new Accessibility(client) /** @type {!NetworkManager} */ this._networkManager = new NetworkManager({ client: client, timeoutSettings: this._timeoutSettings }) /** @type {!FrameManager} */ this._frameManager = new FrameManager({ client: client, timeoutSettings: this._timeoutSettings, networkManager: this._networkManager, page: this }) this._networkManager.setFrameManager(this._frameManager) /** @type {AnimationManager} */ this._animationManager = new AnimationManager(client) /** @type {DatabaseManager} */ this._databaseManager = new DatabaseManager(client) /** @type {WorkerManager} */ this._workerManager = new WorkerManager(client) /** @type {EmulationManager} */ this._emulationManager = new EmulationManager(client) /** @type {Tracing} */ this._tracing = new Tracing(client) /** @type {SecurityManager} */ this._securityManager = new SecurityManager(client) /** @type {!Map<string, Function>} */ this._pageBindings = new Map() /** @type {Coverage} */ this._coverage = new Coverage(client) /** @type {boolean} */ this._javascriptEnabled = true /** @type {?Viewport} */ this._viewport = null /** @type {TaskQueue} */ this._screenshotTaskQueue = initOpts.screenshotTaskQueue || new TaskQueue() this._frameManager.on(Events.FrameManager.FrameAttached, event => this.emit(Events.Page.FrameAttached, event) ) this._frameManager.on(Events.FrameManager.FrameDetached, event => this.emit(Events.Page.FrameDetached, event) ) this._frameManager.on(Events.FrameManager.FrameNavigated, event => this.emit(Events.Page.FrameNavigated, event) ) this._networkManager.on(Events.NetworkManager.Request, event => this.emit(Events.Page.Request, event) ) this._networkManager.on(Events.NetworkManager.Response, event => this.emit(Events.Page.Response, event) ) this._networkManager.on(Events.NetworkManager.RequestFailed, event => this.emit(Events.Page.RequestFailed, event) ) this._networkManager.on(Events.NetworkManager.RequestFinished, event => this.emit(Events.Page.RequestFinished, event) ) this._animationManager.on(Events.Animations.started, event => this.emit(Events.Page.AnimationStarted, event) ) this._animationManager.on(Events.Animations.canceled, event => this.emit(Events.Page.AnimationCanceled, event) ) this._animationManager.on(Events.Animations.created, event => this.emit(Events.Page.AnimationCreated, event) ) this._databaseManager.on(Events.DataBase.added, database => this.emit(Events.Page.DatabaseAdded, database) ) this._workerManager.on( Events.WorkerManager.ServiceWorkerAdded, serviceWorker => this.emit(Events.Page.ServiceWorkerAdded, serviceWorker) ) this._workerManager.on( Events.WorkerManager.ServiceWorkerDeleted, serviceWorker => this.emit(Events.Page.ServiceWorkerDeleted, serviceWorker) ) this._workerManager.on(Events.WorkerManager.Console, consoleMsg => this.emit(Events.Page.Console, consoleMsg) ) this._workerManager.on(Events.WorkerManager.Error, workerError => this.emit(Events.Page.Error, workerError) ) this._workerManager.on(Events.WorkerManager.WorkerCreated, worker => this.emit(Events.Page.WorkerCreated, worker) ) this._workerManager.on(Events.WorkerManager.WorkerDestroyed, worker => this.emit(Events.Page.WorkerDestroyed, worker) ) this._securityManager.on(Events.Security.StateChanged, stateChangeEvent => this.emit(Events.Page.SecurityStateChanged, stateChangeEvent) ) this._securityManager.on(Events.Security.CertificateError, certErrorEvent => this.emit(Events.Page.CertificateError, certErrorEvent) ) client.on('Page.domContentEventFired', event => this.emit(Events.Page.DOMContentLoaded, event.timestamp) ) client.on('Page.loadEventFired', event => this.emit(Events.Page.Load, event.timestamp) ) client.on('Runtime.consoleAPICalled', this._onConsoleAPI.bind(this)) client.on('Runtime.bindingCalled', this._onBindingCalled.bind(this)) client.on('Page.javascriptDialogOpening', this._onDialog.bind(this)) client.on('Runtime.exceptionThrown', this._handleException.bind(this)) client.on('Inspector.targetCrashed', this._onTargetCrashed.bind(this)) client.on('Performance.metrics', this._emitMetrics.bind(this)) client.on('Log.entryAdded', this._onLogEntryAdded.bind(this)) if (this._target) { this._target._isClosedPromise.then(this.__onClose.bind(this)) } else { this._client.on(this._client.$$disconnectEvent, this.__onClose.bind(this)) } } /** * @return {NetworkManager} */ get networkManager () { return this._networkManager } /** * @return {!FrameManager} */ get frameManager () { return this._frameManager } /** * @type {AnimationManager} * @since chrome-remote-interface-extra */ get animationManager () { return this._animationManager } /** * @return {DatabaseManager} * @since chrome-remote-interface-extra */ get databaseManager () { return this._databaseManager } /** * @return {WorkerManager} * @since chrome-remote-interface-extra */ get workerManager () { return this._workerManager } /** * @return {SecurityManager} * @since chrome-remote-interface-extra */ get securityManager () { return this._securityManager } /** * @return {boolean} */ get javascriptEnabled () { return this._javascriptEnabled } /** * @return {!Keyboard} */ get keyboard () { return this._keyboard } /** * @return {!Touchscreen} */ get touchscreen () { return this._touchscreen } /** * @return {!Coverage} */ get coverage () { return this._coverage } /** * @return {!Tracing} */ get tracing () { return this._tracing } /** * @return {!Accessibility} */ get accessibility () { return this._accessibility } /** * @return {!Mouse} */ get mouse () { return this._mouse } /** * Returns T/F indicating if the Log domain is enabled * @return {boolean} * @since chrome-remote-interface-extra */ logDomainEnabled () { return this._additionalDomains.log } /** * Returns T/F indicating if the Performance domain is enabled * @return {boolean} * @since chrome-remote-interface-extra */ performanceDomainEnabled () { return this._additionalDomains.performance } /** * Returns the {@link Target} class that represents this page if this page was initialized with one, e.g. from the {@link Browser} class. * @return {?Target} */ target () { return this._target } /** * Returns the browser this page lives in if the page was created from the {@link Browser} class * @return {?Browser} */ browser () { if (this._target) { return this._target.browser() } return null } /** * Returns the browser context this page lives in if the page was created from the {@link Browser} class * @return {?BrowserContext} */ browserContext () { if (this._target) { return this._target.browserContext() } return null } /** * Returns the top frame for the page * @return {!Frame} */ mainFrame () { return this._frameManager.mainFrame() } /** * Returns all frames contained in the page * @return {Array<Frame>} */ frames () { return this._frameManager.frames() } /** * Returns all workers, if any, that are operating in the page. * Worker monitoring must be enabled beforehand. * @return {Array<Worker>} */ workers () { return this._workerManager.workers() } /** * Returns all ServiceWorkers, if any, that are operating in the page * The ServiceWorker domain must be enabled beforehand. * @return {Array<ServiceWorker>} * @since chrome-remote-interface-extra */ serviceWorkers () { return this._workerManager.serviceWorkers() } /** * Returns the URL of the page (top frame) * @return {!string} */ url () { return this.mainFrame().url() } /** * Returns the string representation of the contents of the page (top frame) * @return {Promise<string>} */ content () { return this.mainFrame().content() } /** * Returns the title of the page (top frame) * @return {Promise<string>} */ title () { return this.mainFrame().title() } /** * @return {?Viewport} */ viewport () { return this._viewport } /** * Returns T/F indicating if the page is closed * @return {boolean} */ isClosed () { return this._closed } /** * Evaluates an arbitrary function or string in the pages (top frames) context * @param {Function|string} pageFunction * @param {...*} args * @return {Promise<*>} */ evaluate (pageFunction, ...args) { return this.mainFrame().evaluate(pageFunction, ...args) } /** * Evaluates an arbitrary function or string in the pages (top frames) context with * the console API enabled * @param {Function|string} pageFunction * @param {...*} args * @return {Promise<*>} */ evaluateWithCliAPI (pageFunction, ...args) { return this._frameManager .mainFrame() .evaluateWithCliAPI(pageFunction, ...args) } /** * Clicks the element that the supplied selector matches. * Evaluation occurs within the context of the top frame * @param {string} selector * @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}} [options] */ click (selector, options = {}) { return this.mainFrame().click(selector, options) } /** * Focuses the element that the supplied selector matches. * Evaluation occurs within the context of the top frame * @param {string} selector */ focus (selector) { return this.mainFrame().focus(selector) } /** * Hovers the element that the supplied selector matches. * Evaluation occurs within the context of the top frame * @param {string} selector */ hover (selector) { return this.mainFrame().hover(selector) } /** * Selects the "select" elements that the supplied selector matches. * Evaluation occurs within the context of the top frame * @param {string} selector * @param {...string} values * @return {Promise<Array<string>>} */ select (selector, ...values) { return this.mainFrame().select(selector, ...values) } /** * Taps the elements that the supplied selector matches. * Evaluation occurs within the context of the top frame * @param {string} selector */ tap (selector) { return this.mainFrame().tap(selector) } /** * Types the supplied text in the elements that the supplied selector matches. * Evaluation occurs within the context of the top frame * @param {string} selector * @param {string} text * @param {{delay: (number|undefined)}=} options */ type (selector, text, options) { return this.mainFrame().type(selector, text, options) } /** * Waits for a selector or xpath or function or specified amount of time. * Evaluation occurs within the context of the top frame * @param {(string|number|Function)} selectorOrFunctionOrTimeout * @param {!Object=} options * @param {...*} args * @return {Promise<JSHandle>} */ waitFor (selectorOrFunctionOrTimeout, options = {}, ...args) { return this.mainFrame().waitFor( selectorOrFunctionOrTimeout, options, ...args ) } /** * Waits for a selector. * Evaluation occurs within the context of the top frame * @param {string} selector * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}} [options] * @return {Promise<ElementHandle|undefined>} */ waitForSelector (selector, options = {}) { return this.mainFrame().waitForSelector(selector, options) } /** * Waits for xpath. * Evaluation occurs within the context of the top frame * @param {string} xpath * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options * @return {Promise<ElementHandle|undefined>} */ waitForXPath (xpath, options = {}) { return this.mainFrame().waitForXPath(xpath, options) } /** * @param {Function} pageFunction * @param {!{polling?: string|number, timeout?: number}=} options * @param {...*} args * @return {Promise<JSHandle>} */ waitForFunction (pageFunction, options = {}, ...args) { return this.mainFrame().waitForFunction(pageFunction, options, ...args) } /** * Returns metrics relating to the layout of the page, such as viewport bounds/scale * @return {Promise<{layoutViewport: Object, visualViewport: Object, contentSize: Object}>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Page#method-getLayoutMetrics * @since chrome-remote-interface-extra */ getLayoutMetrics () { return this._client.send('Page.getLayoutMetrics') } /** * @return {Promise<?CDPNavigationEntry>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Page#method-getNavigationHistory * @since chrome-remote-interface-extra */ getNavigationHistory () { return this._client.send('Page.getNavigationHistory') } /** * @param {string} frameId * @param {string} url * @return {Promise<Buffer>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Page#method-getResourceContent * @since chrome-remote-interface-extra */ getResourceContent (frameId, url) { return this._frameManager.getFrameResourceContent(frameId, url) } /** * @return {Promise<FrameResourceTree>} * @since chrome-remote-interface-extra */ getResourceTree () { return this._frameManager.getResourceTree() } /** * @return {Promise<?{url: string, errors: Array<Object>, data: ?string}>} * @since chrome-remote-interface-extra */ getAppManifest () { return this._client.send('Page.getAppManifest') } /** * Returns all browser cookies. * Depending on the backend support, will return detailed cookie information in the cookies field. * @return {Promise<Array<Cookie>>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-getAllCookies * @since chrome-remote-interface-extra */ getAllCookies () { return this._networkManager.getAllCookies() } /** * Gets the playback rate of animations. See also {@link AnimationManager#getPlaybackRate} * @return {Promise<number>} * @since chrome-remote-interface-extra */ getAnimationPlaybackRate () { return this._animationManager.getPlaybackRate() } /** * Sets the playback rate of animations on the page. * @param {number} playbackRate - Playback rate for animations on page * @return {Promise<void>} * @since chrome-remote-interface-extra */ setAnimationPlaybackRate (playbackRate) { return this._animationManager.setPlaybackRate(playbackRate) } /** * Returns a promise that resolves once this pages network has become idle. * Detection of network idle considers only the number of in-flight HTTP requests * for the Page connected to. * @param {NetIdleOptions} [options] * @return {Promise<void>} * @since chrome-remote-interface-extra */ networkIdlePromise (options) { return this._networkManager.networkIdlePromise(options) } /** * @param {boolean} enabled */ setOfflineMode (enabled) { return this._networkManager.setOfflineMode(enabled) } /** * @param {number} timeout */ setDefaultNavigationTimeout (timeout) { this._timeoutSettings.setDefaultNavigationTimeout(timeout) } /** * @param {number} timeout */ setDefaultTimeout (timeout) { this._timeoutSettings.setDefaultTimeout(timeout) } /** * @param {string} selector * @return {Promise<ElementHandle|undefined>} * @since chrome-remote-interface-extra */ querySelector (selector) { return this.$(selector) } /** * @param {string} selector * @return {Promise<Array<ElementHandle>>} * @since chrome-remote-interface-extra */ querySelectorAll (selector) { return this.$$(selector) } /** * @param {string} selector * @param {Function|String} pageFunction * @param {...*} args * @return {Promise<Object|undefined>} * @since chrome-remote-interface-extra */ querySelectorEval (selector, pageFunction, ...args) { return this.$eval(selector, pageFunction, ...args) } /** * @param {string} selector * @param {Function|String} pageFunction * @param {...*} args * @return {Promise<Object|undefined>} * @since chrome-remote-interface-extra */ querySelectorAllEval (selector, pageFunction, ...args) { return this.$$eval(selector, pageFunction, ...args) } /** * @param {string} elemId * @return {Promise<ElementHandle|undefined>} * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById * @since chrome-remote-interface-extra */ getElementById (elemId) { return this.mainFrame().getElementById(elemId) } /** * @param {string} expression * @return {Promise<Array<ElementHandle>>} * @since chrome-remote-interface-extra */ xpathQuery (expression) { return this.$x(expression) } /** * The method runs document.querySelector within the page. * If no element matches the selector, the return value resolves to null. * @param {string} selector * @return {Promise<ElementHandle|undefined>} */ $ (selector) { return this.mainFrame().$(selector) } /** * @param {string} selector * @param {Function|string} pageFunction * @param {...*} args * @return {Promise<Object|undefined>} */ $eval (selector, pageFunction, ...args) { return this.mainFrame().$eval(selector, pageFunction, ...args) } /** * @param {string} selector * @param {Function|string} pageFunction * @param {...*} args * @return {Promise<Object|undefined>} */ $$eval (selector, pageFunction, ...args) { return this.mainFrame().$$eval(selector, pageFunction, ...args) } /** * @param {string} selector * @return {Promise<Array<ElementHandle>>} */ $$ (selector) { return this.mainFrame().$$(selector) } /** * @param {string} expression * @return {Promise<Array<ElementHandle>>} */ $x (expression) { return this.mainFrame().$x(expression) } /** * Returns a ElementHandle for the main frames document object * @return {Promise<ElementHandle>} */ document () { return this.mainFrame().document() } /** * Returns a JSHandle for the main frames window object * @return {Promise<JSHandle>} */ window () { return this.mainFrame().window() } /** * Returns all browser cookies for the current URL. * Depending on the backend support, will return detailed cookie information in the cookies field. * @param {Array<string>} urls - The list of URLs for which applicable cookies will be fetched * @return {Promise<Array<Cookie>>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-getCookies * @since chrome-remote-interface-extra && puppeteer */ cookies (...urls) { return this._networkManager.getCookies(urls.length ? urls : [this.url()]) } /** * Clears browser cookies * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-clearBrowserCookies * @since chrome-remote-interface-extra */ clearBrowserCookies () { return this._networkManager.clearBrowserCookies() } /** * Blocks URLs from loading. EXPERIMENTAL * @param {...string} urls - URL patterns to block. Wildcards ('*') are allowed * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-setBlockedURLs */ setBlockedURLs (...urls) { return this._networkManager.setBlockedURLs(...urls) } /** * Returns the DER-encoded certificate. EXPERIMENTAL * @param {string} origin - Origin to get certificate for * @return {Promise<Array<string>>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-getCertificate */ getDEREncodedCertificateForOrigin (origin) { return this._networkManager.getCertificate(origin) } /** * @param {string} url * @param {!{referer?: string, timeout?: number, waitUntil?: string|Array<string>, transitionType?: string}=} options * @return {Promise<Response|undefined>} */ goto (url, options) { this._workerManager._clearKnownWorkers() return this.mainFrame().goto(url, options) } /** * @param {!{timeout?: number, waitUntil?: string|Array<string>}=} options * @return {Promise<?Response>} */ waitForNavigation (options = {}) { this._workerManager._clearKnownWorkers() return this.mainFrame().waitForNavigation(options) } /** * @param {(string|Function)} urlOrPredicate * @param {{timeout?: number}} [options] * @return {Promise<Request>} */ waitForRequest (urlOrPredicate, options = {}) { return this._networkManager.waitForRequest(urlOrPredicate, options) } /** * @param {(string|Function)} urlOrPredicate * @param {{timeout?: number}} [options] * @return {Promise<Response>} */ waitForResponse (urlOrPredicate, options = {}) { return this._networkManager.waitForResponse(urlOrPredicate, options) } /** * @param {!{timeout?: number, waitUntil?: string|Array<string>}=} options * @return {Promise<Response|undefined>} */ goBack (options) { this._workerManager._clearKnownWorkers() return this._go(-1, options) } /** * @param {!{timeout?: number, waitUntil?: string|Array<string>}=} options * @return {Promise<Response|undefined>} */ goForward (options) { this._workerManager._clearKnownWorkers() return this._go(+1, options) } /** * @param {?{username: string, password: string}} credentials */ authenticate (credentials) { return this._networkManager.authenticate(credentials) } /** * @param {!Object<string, string>} headers */ setExtraHTTPHeaders (headers) { return this._networkManager.setExtraHTTPHeaders(headers) } /** * @param {string} userAgent */ setUserAgent (userAgent) { return this._networkManager.setUserAgent(userAgent) } /** * @param {{url?: string, path?: string, content?: string, type?: string}} options * @return {Promise<ElementHandle>} */ addScriptTag (options) { return this.mainFrame().addScriptTag(options) } /** * @param {{url?: string, path?: string, content?: string}} options * @return {Promise<ElementHandle>} */ addStyleTag (options) { return this.mainFrame().addStyleTag(options) } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async enableWorkerMonitoring () { await this._workerManager.enableWorkerMonitoring() } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async disableWorkerMonitoring () { await this._workerManager.disableWorkerMonitoring() } /** * {@link WorkerManager#enable} * @return {Promise<void>} * @since chrome-remote-interface-extra */ async enableServiceWorkersDomain () { await this._workerManager.enableServiceWorkerDomain() } /** * {@link WorkerManager#disable} * @return {Promise<void>} * @since chrome-remote-interface-extra */ async disableServiceWorkersDomain () { await this._workerManager.disableServiceWorkerDomain() } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async enableLogDomain () { if (!this._additionalDomains.log) { this._additionalDomains.log = true await this._client.send('Log.enable', {}) } } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async disableLogDomain () { if (this._additionalDomains.log) { this._additionalDomains.log = false await this._client.send('Log.disable', {}) } } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async enablePerformanceDomain () { if (!this._additionalDomains.performance) { this._additionalDomains.performance = true await this._client.send('Performance.enable', {}) } } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async disablePerformanceDomain () { if (this._additionalDomains.performance) { this._additionalDomains.performance = false await this._client.send('Performance.disable', {}) } } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async enableAnimationsDomain () { await this._animationManager.enable() } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async disableAnimationsDomain () { await this._animationManager.disable() } /** * Inject object to the target's main frame that provides a communication channel with browser target. * * Injected object will be available as window[bindingName]. * * The object has the following API: * * binding.send(json) - a method to send messages over the remote debugging protocol * * binding.onmessage = json => handleMessage(json) - a callback that will be called for the protocol notifications and command responses. * * EXPERIMENTAL * @param {string} [bindingName] - Binding name, 'cdp' if not specified * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Target#method-exposeDevToolsProtocol * @since chrome-remote-interface-extra */ async exposeDevToolsProtocol (bindingName) { if (this._target) { await this._target.exposeDevToolsProtocol(bindingName) return } let pageURL let title if (this._targetId == null) { const { targetInfos } = await this._client.send('Target.getTargets', {}) pageURL = this.url() title = await this.title() for (let i = 0; i < targetInfos.length; i++) { const targetInfo = targetInfos[i] if (pageURL === targetInfo.url) { this._targetId = targetInfo.targetId break } } } if (this._targetId == null) { throw new Error( `Failed to expose devtools protocol. This page (url=${pageURL}, title=${title}) was created without passing in a target and we could not find this page's target id` ) } await this._client.send('Target.exposeDevToolsProtocol', { targetId: this._targetId, bindingName: bindingName || undefined }) } /** * @param {number} entryId * @return {Promise<void>} */ async navigateToHistoryEntry (entryId) { await this._client.send('Page.navigateToHistoryEntry', { entryId }) } async resetNavigationHistory () { await this._client.send('Page.resetNavigationHistory') } /** * Toggles ignoring of service worker for each request. EXPERIMENTAL * @param {boolean} bypass - Bypass service worker and load from network * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-setBypassServiceWorker * @since chrome-remote-interface-extra */ async httpRequestsBypassServiceWorker (bypass) { await this._networkManager.bypassServiceWorker(bypass) } /** * Force the page stop all navigations and pending resource fetches * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Page#method-stopLoading * @since chrome-remote-interface-extra */ async stopLoading () { await this._client.send('Page.stopLoading') } /** * Set the behavior when downloading a file. EXPERIMENTAL * * @param {string} behavior - Whether to allow all or deny all download requests, or use default Chrome behavior if available (otherwise deny). Allowed values: deny, allow, default * @param {string} [downloadPath] - The default path to save downloaded files to. This is requred if behavior is set to 'allow' * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Page#method-setDownloadBehavior * @since chrome-remote-interface-extra */ async setDownloadBehavior (behavior, downloadPath) { await this._client.send('Page.setDownloadBehavior', { behavior, downloadPath: downloadPath || undefined }) } /** * Evaluates given script in every frame upon creation (before loading frame's scripts) * @param {string} source - The string contents of the script * @param {string} [worldName] - If specified, creates an isolated world with the given name and evaluates given * script in it. This world name will be used as the ExecutionContextDescription::name when the corresponding * event is emitted. * @return {Promise<string>} - Identifier of the added script * @see https://chromedevtools.github.io/devtools-protocol/tot/Page#method-addScriptToEvaluateOnNewDocument * @since chrome-remote-interface-extra */ async addScriptToEvaluateOnNewDocument (source, worldName) { const { identifier } = await this._client.send( 'Page.addScriptToEvaluateOnNewDocument', { source, woldName: worldName || undefined } ) return identifier } /** * @param {string} identifier - Identifier of the added script * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Page#method-removeScriptToEvaluateOnNewDocument * @since chrome-remote-interface-extra */ async removeScriptToEvaluateOnNewDocument (identifier) { if (!identifier) return await this._client.send('Page.removeScriptToEvaluateOnNewDocument', { identifier }) } /** * @return {Promise<string>} */ async userAgent () { const version = await this._client.send('Browser.getVersion') return version.userAgent } /** * @param {string} acceptLanguage * @since chrome-remote-interface-extra */ async setAcceptLanguage (acceptLanguage) { await this._networkManager.setAcceptLanguage(acceptLanguage) } /** * @param {string} platform * @since chrome-remote-interface-extra */ async setNavigatorPlatform (platform) { await this._networkManager.setNavigatorPlatform(platform) } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async disableNetworkCache () { await this._networkManager.disableCache() } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async enableNetworkCache () { await this._networkManager.enableCache() } /** * * @return {Promise<void>} * @since chrome-remote-interface-extra */ async clearBrowserCache () { await this._networkManager.clearBrowserCache() } /** * @param {!{longitude: number, latitude: number, accuracy: (number|undefined)}} options */ async setGeolocation (options) { await this._emulationManager.setGeolocation(options) } /** * @param {boolean} value */ async setRequestInterception (value) { await this._networkManager.setRequestInterception(value) } /** * @param {Function|string} pageFunction * @param {...*} args * @return {Promise<JSHandle>} */ async evaluateHandle (pageFunction, ...args) { const context = await this.mainFrame().executionContext() return context.evaluateHandle(pageFunction, ...args) } /** * @param {Function|string} pageFunction * @param {...*} args * @return {Promise<JSHandle>} */ async evaluateHandleWithCliAPI (pageFunction, ...args) { const context = await this.mainFrame().executionContext() return context.evaluateHandleWithCliAPI(pageFunction, ...args) } /** * @param {!JSHandle} prototypeHandle * @return {Promise<JSHandle>} */ async queryObjects (prototypeHandle) { const context = await this.mainFrame().executionContext() return context.queryObjects(prototypeHandle) } /** * Deletes the specified browser cookies with matching name and url or domain/path pair. * @param {CDPCookie|CookieToBeDeleted|string|Cookie} cookie - The cookie to be deleted * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-deleteCookies * @since chrome-remote-interface-extra */ async deleteCookie (cookie) { const pageURL = this.url() const startsWithHTTP = pageURL.startsWith('http') await this._networkManager.deleteCookie( ensureCookie(cookie, pageURL, startsWithHTTP) ) } /** * Deletes the specified browser cookies with matching name and url or domain/path pair. * @param {Array<CDPCookie|CookieToBeDeleted|string|Cookie>} cookies - The cookies to be deleted * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-deleteCookies * @since chrome-remote-interface-extra */ async deleteCookies (...cookies) { const pageURL = this.url() const startsWithHTTP = pageURL.startsWith('http') for (let i = 0; i < cookies.length; i++) { await this._networkManager.deleteCookie( ensureCookie(cookies[i], pageURL, startsWithHTTP) ) } } /** * @param {CDPCookie|Cookie|string} cookie - The new cookie to be set * @return {Promise<boolean>} - T/F indicating if the cookie was set * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-setCookie * @since chrome-remote-interface-extra */ async setCookie (cookie) { const pageURL = this.url() const startsWithHTTP = pageURL.startsWith('http') const cookieToBeSet = ensureCookie(cookie, pageURL, startsWithHTTP) assert( cookieToBeSet.url !== 'about:blank', `Blank page can not have cookie "${cookieToBeSet.name}"` ) assert( !String.prototype.startsWith.call(cookieToBeSet.url || '', 'data:'), `Data URL page can not have cookie "${cookieToBeSet.name}"` ) await this._networkManager.deleteCookie(cookieToBeSet) return this._networkManager.setCookie(cookieToBeSet) } /** * Sets given cookies * @param {Array<CDPCookie|Cookie|string>} cookies * @return {Promise<void>} * @see https://chromedevtools.github.io/devtools-protocol/tot/Network#method-setCookies * @since chrome-remote-interface-extra */ async setCookies (...cookies) { if (!cookies.length) return const pageURL = this.url() const startsWithHTTP = pageURL.startsWith('http') const cookiesSet = [] for (let i = 0; i < cookies.length; i++) { let cookie = ensureCookie(cookies[i], pageURL, startsWithHTTP) assert( cookie.url !== 'about:blank', `Blank page can not have cookie "${cookie.name}"` ) assert( !String.prototype.startsWith.call(cookie.url || '', 'data:'), `Data URL page can not have cookie "${cookie.name}"` ) cookiesSet.push(cookie) } await this._networkManager.deleteCookies(...cookiesSet) await this._networkManager.setCookies(...cookiesSet) } /** * @param {string} name * @param {Function} puppeteerFunction */ async exposeFunction (name, puppeteerFunction) { if (this._pageBindings.has(name)) { throw new Error( `Failed to add page binding with name ${name}: window['${name}'] already exists!` ) } this._pageBindings.set(name, puppeteerFunction) const expression = helper.evaluationString(addPageBinding, name) await this._client.send('Runtime.addBinding', { name: name }) await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: expression }) await Promise.all( this.frames().map(frame => frame.evaluate(expression).catch(debugError)) ) function addPageBinding (bindingName) { const binding = window[bindingName] window[bindingName] = async (...args) => { const me = window[bindingName] let callbacks = me['callbacks'] if (!callbacks) { callbacks = new Map() me['callbacks'] = callbacks } const seq = (me['lastSeq'] || 0) + 1 me['lastSeq'] = seq const promise = new Promise((resolve, reject) => callbacks.set(seq, { resolve, reject }) ) binding(JSON.stringify({ name: bindingName, seq, args })) return promise } } } /** * @return {Promise<Metrics>} */ async metrics () { const response = await this._client.send('Performance.getMetrics') return this._buildMetricsObject(response.metrics) } /** * @param {string} html * @param {!{timeout?: number, waitUntil?: string|Array<string>}=} options */ async setContent (html, options) { await this.mainFrame().setContent(html, options) } /** * @param {!{timeout?: number, waitUntil?: string|Array<string>, ignoreCache?: boolean, scriptToEvaluateOnLoad?: string}=} options * @return {Promise<Response|undefined>} */ async reload (options) { const params = {} if (options) { params.ignoreCache = options.ignoreCache params.scriptToEvaluateOnLoad = options.scriptToEvaluateOnLoad } const [response] = await Promise.all([ this.waitForNavigation(options), this._client.send('Page.reload', params) ]) return response } async bringToFront () { await this._client.send('Page.bringToFront') } /** * @param {!{viewport: !Viewport, userAgent: string}} options */ async emulate (options) { await Promise.all([ this.setViewport(options.viewport), this.setUserAgent(options.userAgent) ]) } /** * @param {boolean} enabled */ async setJavaScriptEnabled (enabled) { if (this._javascriptEnabled === enabled) return this._javascriptEnabled = enabled await this._emulationManager.setScriptExecutionDisabled(!enabled) } /** * @param {boolean} enabled */ async setBypassCSP (enabled) { await this._client.send('Page.setBypassCSP', { enabled }) } /** * @param {?string} mediaType */ async emulateMedia (mediaType) { await this._emulationManager.setEmulatedMedia(mediaType || '') } /** * @param {Viewport} viewport */ async setViewport (viewport) { const needsReload = await this._emulationManager.emulateViewport(viewport) this._viewport = viewport if (needsReload) await this.reload() } /** * @param {Function|string} pageFunction * @param {...*} args */ async evaluateOnNewDocument (pageFunction, ...args) { const source = helper.evaluationString(pageFunction, ...args) await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source }) } /** * @param {boolean} enabled */ async setCacheEnabled (enabled = true) { if (enabled) { await this._networkManager.enableCache() } else { await this._networkManager.disableCache() } } /** * @param {!ScreenshotOptions=} options * @return {Promise<Buffer|!String>} */ async screenshot (options = {}) { let screenshotType = null // options.type takes precedence over inferring the type from options.path // because it may be a 0-length file with no extension created beforehand (i.e. as a temp file). if (options.type) { assert( options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type ) screenshotType = options.type } else if (options.path) { const mimeType = mime.getType(options.path) if (mimeType === 'image/png') screenshotType = 'png' else if (mimeType === 'image/jpeg') screenshotType = 'jpeg' assert(screenshotType, 'Unsupported screenshot mime type: ' + mimeType) } if (!screenshotType) screenshotType = 'png' if (options.quality) { assert( screenshotType === 'jpeg', 'options.quality is unsupported for the ' + screenshotType + ' screenshots' ) assert( typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + typeof options.quality ) assert( Number.isInteger(options.quality), 'Expected options.quality to be an integer' ) assert( options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality ) } assert( !options.clip || !options.fullPage, 'options.clip and options.fullPage are exclusive' ) if (options.clip) { assert( typeof options.clip.x === 'number', 'Expected options.clip.x to be a number but found ' + typeof options.clip.x ) assert( typeof options.clip.y === 'number', 'Expected options.clip.y to be a number but found ' + typeof options.clip.y ) assert( typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' + typeof options.clip.width ) assert( typeof options.clip.height === 'number', 'Expected options.clip.height to be a number but found ' + typeof options.clip.height ) assert( options.clip.width !== 0, 'Expected options.clip.width not to be 0.' ) assert( options.clip.height !== 0, 'Expected options.clip.width not to be 0.' ) } return this._screenshotTaskQueue.postTask( this._screenshotTask.bind(this, screenshotType, options) ) } /** * @param {!PDFOptions=} options * @return {Promise<Buffer>} */ async pdf (options = {}) { const { scale = 1, displayHeaderFooter = false, headerTemplate = '', footerTemplate = '', printBackground = false, landscape = false, pageRanges = '', preferCSSPageSize = false, margin = {}, path = null } = options let paperWidth = 8.5 let paperHeight = 11 if (options.format) { const format = Page.PaperFormats[options.format.toLowerCase()] assert(format, 'Unknown paper format: ' + options.format) paperWidth = format.width paperHeight = format.height } else { paperWidth = convertPrintParameterToInches(options.width) || paperWidth paperHeight = convertPrintParameterToInches(options.height) || paperHeight } const marginTop = convertPrintParameterToInches(margin.top) || 0 const marginLeft = convertPrintParameterToInches(margin.left) || 0 const marginBottom = convertPrintParameterToInches(margin.bottom) || 0 const marginRight = convertPrintParameterToInches(margin.right) || 0 const result = await this._client.send('Page.printToPDF', { landscape, displayHeaderFooter, headerTemplate, footerTemplate, printBackground, scale, paperWidth, paperHeight, marginTop, marginBottom, marginLeft, marginRight, pageRanges, preferCSSPageSize }) const buffer = Buffer.from(result.data, 'base64') if (path !== null) await fs.writeFile(path, buffer) return buffer } /** * @param {!{runBeforeUnload: (boolean|undefined)}=} options */ async close (options = { runBeforeUnload: undefined }) { const runBeforeUnload = !!options.runBeforeUnload if (runBeforeUnload) { await this._client.send('Page.close') } else if (this._target) { await this._target.close() await this._target._isClosedPromise } } /** * @param {"png"|"jpeg"} format * @param {!ScreenshotOptions=} options * @return {Promise<Buffer|!String>} */ async _screenshotTask (format, options) { if (this._target) { await this._client.send('Target.activateTarget', { targetId: this._target.id() }) } let clip = options.clip ? processClip(options.clip) : undefined let deviceMetricsToReset if (options.fullPage) { const metrics = await this.getLayoutMetrics() const width = Math.ceil(metrics.contentSize.width) const height = Math.ceil(metrics.contentSize.height) // Overwrite clip for full page at all times. clip = { x: 0, y: 0, width, height, scale: 1 } const { isMobile = false, deviceScaleFactor = 1, isLandscape = false } = this._viewpor