puppeteer-core
Version:
A high-level API to control headless Chrome over the DevTools Protocol
1,206 lines • 104 kB
JavaScript
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Page = void 0;
const EventEmitter_js_1 = require("./EventEmitter.js");
const Connection_js_1 = require("./Connection.js");
const Dialog_js_1 = require("./Dialog.js");
const EmulationManager_js_1 = require("./EmulationManager.js");
const FrameManager_js_1 = require("./FrameManager.js");
const Input_js_1 = require("./Input.js");
const Tracing_js_1 = require("./Tracing.js");
const assert_js_1 = require("./assert.js");
const helper_js_1 = require("./helper.js");
const Coverage_js_1 = require("./Coverage.js");
const WebWorker_js_1 = require("./WebWorker.js");
const JSHandle_js_1 = require("./JSHandle.js");
const NetworkManager_js_1 = require("./NetworkManager.js");
const Accessibility_js_1 = require("./Accessibility.js");
const TimeoutSettings_js_1 = require("./TimeoutSettings.js");
const FileChooser_js_1 = require("./FileChooser.js");
const ConsoleMessage_js_1 = require("./ConsoleMessage.js");
const PDFOptions_js_1 = require("./PDFOptions.js");
const environment_js_1 = require("../environment.js");
/**
* Page provides methods to interact with a single tab or
* {@link https://developer.chrome.com/extensions/background_pages | extension background page} in Chromium.
*
* @remarks
*
* One Browser instance might have multiple Page instances.
*
* @example
* This example creates a page, navigates it to a URL, and then * saves a screenshot:
* ```js
* const puppeteer = require('puppeteer');
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* await page.screenshot({path: 'screenshot.png'});
* await browser.close();
* })();
* ```
*
* The Page class extends from Puppeteer's {@link EventEmitter} class and will
* emit various events which are documented in the {@link PageEmittedEvents} enum.
*
* @example
* This example logs a message for a single page `load` event:
* ```js
* page.once('load', () => console.log('Page loaded!'));
* ```
*
* To unsubscribe from events use the `off` method:
*
* ```js
* function logRequest(interceptedRequest) {
* console.log('A request was made:', interceptedRequest.url());
* }
* page.on('request', logRequest);
* // Sometime later...
* page.off('request', logRequest);
* ```
* @public
*/
class Page extends EventEmitter_js_1.EventEmitter {
/**
* @internal
*/
constructor(client, target, ignoreHTTPSErrors, screenshotTaskQueue) {
super();
this._closed = false;
this._timeoutSettings = new TimeoutSettings_js_1.TimeoutSettings();
this._pageBindings = new Map();
this._javascriptEnabled = true;
this._workers = new Map();
// TODO: improve this typedef - it's a function that takes a file chooser or
// something?
this._fileChooserInterceptors = new Set();
this._userDragInterceptionEnabled = false;
this._handlerMap = new WeakMap();
this._client = client;
this._target = target;
this._keyboard = new Input_js_1.Keyboard(client);
this._mouse = new Input_js_1.Mouse(client, this._keyboard);
this._touchscreen = new Input_js_1.Touchscreen(client, this._keyboard);
this._accessibility = new Accessibility_js_1.Accessibility(client);
this._frameManager = new FrameManager_js_1.FrameManager(client, this, ignoreHTTPSErrors, this._timeoutSettings);
this._emulationManager = new EmulationManager_js_1.EmulationManager(client);
this._tracing = new Tracing_js_1.Tracing(client);
this._coverage = new Coverage_js_1.Coverage(client);
this._screenshotTaskQueue = screenshotTaskQueue;
this._viewport = null;
client.on('Target.attachedToTarget', (event) => {
if (event.targetInfo.type !== 'worker' &&
event.targetInfo.type !== 'iframe') {
// If we don't detach from service workers, they will never die.
// We still want to attach to workers for emitting events.
// We still want to attach to iframes so sessions may interact with them.
// We detach from all other types out of an abundance of caution.
// See https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypePage%5B%5D%22
// for the complete list of available types.
client
.send('Target.detachFromTarget', {
sessionId: event.sessionId,
})
.catch(helper_js_1.debugError);
return;
}
if (event.targetInfo.type === 'worker') {
const session = Connection_js_1.Connection.fromSession(client).session(event.sessionId);
const worker = new WebWorker_js_1.WebWorker(session, event.targetInfo.url, this._addConsoleMessage.bind(this), this._handleException.bind(this));
this._workers.set(event.sessionId, worker);
this.emit("workercreated" /* WorkerCreated */, worker);
}
});
client.on('Target.detachedFromTarget', (event) => {
const worker = this._workers.get(event.sessionId);
if (!worker)
return;
this._workers.delete(event.sessionId);
this.emit("workerdestroyed" /* WorkerDestroyed */, worker);
});
this._frameManager.on(FrameManager_js_1.FrameManagerEmittedEvents.FrameAttached, (event) => this.emit("frameattached" /* FrameAttached */, event));
this._frameManager.on(FrameManager_js_1.FrameManagerEmittedEvents.FrameDetached, (event) => this.emit("framedetached" /* FrameDetached */, event));
this._frameManager.on(FrameManager_js_1.FrameManagerEmittedEvents.FrameNavigated, (event) => this.emit("framenavigated" /* FrameNavigated */, event));
const networkManager = this._frameManager.networkManager();
networkManager.on(NetworkManager_js_1.NetworkManagerEmittedEvents.Request, (event) => this.emit("request" /* Request */, event));
networkManager.on(NetworkManager_js_1.NetworkManagerEmittedEvents.RequestServedFromCache, (event) => this.emit("requestservedfromcache" /* RequestServedFromCache */, event));
networkManager.on(NetworkManager_js_1.NetworkManagerEmittedEvents.Response, (event) => this.emit("response" /* Response */, event));
networkManager.on(NetworkManager_js_1.NetworkManagerEmittedEvents.RequestFailed, (event) => this.emit("requestfailed" /* RequestFailed */, event));
networkManager.on(NetworkManager_js_1.NetworkManagerEmittedEvents.RequestFinished, (event) => this.emit("requestfinished" /* RequestFinished */, event));
this._fileChooserInterceptors = new Set();
client.on('Page.domContentEventFired', () => this.emit("domcontentloaded" /* DOMContentLoaded */));
client.on('Page.loadEventFired', () => this.emit("load" /* Load */));
client.on('Runtime.consoleAPICalled', (event) => this._onConsoleAPI(event));
client.on('Runtime.bindingCalled', (event) => this._onBindingCalled(event));
client.on('Page.javascriptDialogOpening', (event) => this._onDialog(event));
client.on('Runtime.exceptionThrown', (exception) => this._handleException(exception.exceptionDetails));
client.on('Inspector.targetCrashed', () => this._onTargetCrashed());
client.on('Performance.metrics', (event) => this._emitMetrics(event));
client.on('Log.entryAdded', (event) => this._onLogEntryAdded(event));
client.on('Page.fileChooserOpened', (event) => this._onFileChooser(event));
this._target._isClosedPromise.then(() => {
this.emit("close" /* Close */);
this._closed = true;
});
}
/**
* @internal
*/
static async create(client, target, ignoreHTTPSErrors, defaultViewport, screenshotTaskQueue) {
const page = new Page(client, target, ignoreHTTPSErrors, screenshotTaskQueue);
await page._initialize();
if (defaultViewport)
await page.setViewport(defaultViewport);
return page;
}
async _initialize() {
await Promise.all([
this._frameManager.initialize(),
this._client.send('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: false,
flatten: true,
}),
this._client.send('Performance.enable'),
this._client.send('Log.enable'),
]);
}
async _onFileChooser(event) {
if (!this._fileChooserInterceptors.size)
return;
const frame = this._frameManager.frame(event.frameId);
const context = await frame.executionContext();
const element = await context._adoptBackendNodeId(event.backendNodeId);
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const fileChooser = new FileChooser_js_1.FileChooser(element, event);
for (const interceptor of interceptors)
interceptor.call(null, fileChooser);
}
/**
* @returns `true` if drag events are being intercepted, `false` otherwise.
*/
isDragInterceptionEnabled() {
return this._userDragInterceptionEnabled;
}
/**
* @returns `true` if the page has JavaScript enabled, `false` otherwise.
*/
isJavaScriptEnabled() {
return this._javascriptEnabled;
}
/**
* Listen to page events.
*/
// Note: this method exists to define event typings and handle
// proper wireup of cooperative request interception. Actual event listening and
// dispatching is delegated to EventEmitter.
on(eventName, handler) {
if (eventName === 'request') {
const wrap = (event) => {
event.enqueueInterceptAction(() => handler(event));
};
this._handlerMap.set(handler, wrap);
return super.on(eventName, wrap);
}
return super.on(eventName, handler);
}
once(eventName, handler) {
// Note: this method only exists to define the types; we delegate the impl
// to EventEmitter.
return super.once(eventName, handler);
}
off(eventName, handler) {
if (eventName === 'request') {
handler = this._handlerMap.get(handler) || handler;
}
return super.off(eventName, handler);
}
/**
* This method is typically coupled with an action that triggers file
* choosing. The following example clicks a button that issues a file chooser
* and then responds with `/tmp/myfile.pdf` as if a user has selected this file.
*
* ```js
* const [fileChooser] = await Promise.all([
* page.waitForFileChooser(),
* page.click('#upload-file-button'),
* // some button that triggers file selection
* ]);
* await fileChooser.accept(['/tmp/myfile.pdf']);
* ```
*
* NOTE: This must be called before the file chooser is launched. It will not
* return a currently active file chooser.
* @param options - Optional waiting parameters
* @returns Resolves after a page requests a file picker.
* @remarks
* NOTE: In non-headless Chromium, this method results in the native file picker
* dialog `not showing up` for the user.
*/
async waitForFileChooser(options = {}) {
if (!this._fileChooserInterceptors.size)
await this._client.send('Page.setInterceptFileChooserDialog', {
enabled: true,
});
const { timeout = this._timeoutSettings.timeout() } = options;
let callback;
const promise = new Promise((x) => (callback = x));
this._fileChooserInterceptors.add(callback);
return helper_js_1.helper
.waitWithTimeout(promise, 'waiting for file chooser', timeout)
.catch((error) => {
this._fileChooserInterceptors.delete(callback);
throw error;
});
}
/**
* Sets the page's geolocation.
* @remarks
* NOTE: Consider using {@link BrowserContext.overridePermissions} to grant
* permissions for the page to read its geolocation.
* @example
* ```js
* await page.setGeolocation({latitude: 59.95, longitude: 30.31667});
* ```
*/
async setGeolocation(options) {
const { longitude, latitude, accuracy = 0 } = options;
if (longitude < -180 || longitude > 180)
throw new Error(`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`);
if (latitude < -90 || latitude > 90)
throw new Error(`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`);
if (accuracy < 0)
throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`);
await this._client.send('Emulation.setGeolocationOverride', {
longitude,
latitude,
accuracy,
});
}
/**
* @returns A target this page was created from.
*/
target() {
return this._target;
}
/**
* Get the CDP session client the page belongs to.
* @internal
*/
client() {
return this._client;
}
/**
* Get the browser the page belongs to.
*/
browser() {
return this._target.browser();
}
/**
* Get the browser context that the page belongs to.
*/
browserContext() {
return this._target.browserContext();
}
_onTargetCrashed() {
this.emit('error', new Error('Page crashed!'));
}
_onLogEntryAdded(event) {
const { level, text, args, source, url, lineNumber } = event.entry;
if (args)
args.map((arg) => helper_js_1.helper.releaseObject(this._client, arg));
if (source !== 'worker')
this.emit("console" /* Console */, new ConsoleMessage_js_1.ConsoleMessage(level, text, [], [{ url, lineNumber }]));
}
/**
* @returns The page's main frame.
* @remarks
* Page is guaranteed to have a main frame which persists during navigations.
*/
mainFrame() {
return this._frameManager.mainFrame();
}
get keyboard() {
return this._keyboard;
}
get touchscreen() {
return this._touchscreen;
}
get coverage() {
return this._coverage;
}
get tracing() {
return this._tracing;
}
get accessibility() {
return this._accessibility;
}
/**
* @returns An array of all frames attached to the page.
*/
frames() {
return this._frameManager.frames();
}
/**
* @returns all of the dedicated
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API |
* WebWorkers}
* associated with the page.
* @remarks
* NOTE: This does not contain ServiceWorkers
*/
workers() {
return Array.from(this._workers.values());
}
/**
* @param value - Whether to enable request interception.
*
* @remarks
* Activating request interception enables {@link HTTPRequest.abort},
* {@link HTTPRequest.continue} and {@link HTTPRequest.respond} methods. This
* provides the capability to modify network requests that are made by a page.
*
* Once request interception is enabled, every request will stall unless it's
* continued, responded or aborted; or completed using the browser cache.
*
* @example
* An example of a naïve request interceptor that aborts all image requests:
* ```js
* const puppeteer = require('puppeteer');
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.setRequestInterception(true);
* page.on('request', interceptedRequest => {
* if (interceptedRequest.url().endsWith('.png') ||
* interceptedRequest.url().endsWith('.jpg'))
* interceptedRequest.abort();
* else
* interceptedRequest.continue();
* });
* await page.goto('https://example.com');
* await browser.close();
* })();
* ```
* NOTE: Enabling request interception disables page caching.
*/
async setRequestInterception(value) {
return this._frameManager.networkManager().setRequestInterception(value);
}
/**
* @param enabled - Whether to enable drag interception.
*
* @remarks
* Activating drag interception enables the `Input.drag`,
* methods This provides the capability to capture drag events emitted
* on the page, which can then be used to simulate drag-and-drop.
*/
async setDragInterception(enabled) {
this._userDragInterceptionEnabled = enabled;
return this._client.send('Input.setInterceptDrags', { enabled });
}
/**
* @param enabled - When `true`, enables offline mode for the page.
* @remarks
* NOTE: while this method sets the network connection to offline, it does
* not change the parameters used in [page.emulateNetworkConditions(networkConditions)]
* (#pageemulatenetworkconditionsnetworkconditions)
*/
setOfflineMode(enabled) {
return this._frameManager.networkManager().setOfflineMode(enabled);
}
/**
* @param networkConditions - Passing `null` disables network condition emulation.
* @example
* ```js
* const puppeteer = require('puppeteer');
* const slow3G = puppeteer.networkConditions['Slow 3G'];
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.emulateNetworkConditions(slow3G);
* await page.goto('https://www.google.com');
* // other actions...
* await browser.close();
* })();
* ```
* @remarks
* NOTE: This does not affect WebSockets and WebRTC PeerConnections (see
* https://crbug.com/563644). To set the page offline, you can use
* [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled).
*/
emulateNetworkConditions(networkConditions) {
return this._frameManager
.networkManager()
.emulateNetworkConditions(networkConditions);
}
/**
* This setting will change the default maximum navigation time for the
* following methods and related shortcuts:
*
* - {@link Page.goBack | page.goBack(options)}
*
* - {@link Page.goForward | page.goForward(options)}
*
* - {@link Page.goto | page.goto(url,options)}
*
* - {@link Page.reload | page.reload(options)}
*
* - {@link Page.setContent | page.setContent(html,options)}
*
* - {@link Page.waitForNavigation | page.waitForNavigation(options)}
* @param timeout - Maximum navigation time in milliseconds.
*/
setDefaultNavigationTimeout(timeout) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
/**
* @param timeout - Maximum time in milliseconds.
*/
setDefaultTimeout(timeout) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
/**
* Runs `document.querySelector` within the page. If no element matches the
* selector, the return value resolves to `null`.
*
* @remarks
* Shortcut for {@link Frame.$ | Page.mainFrame().$(selector) }.
*
* @param selector - A `selector` to query page for
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
* to query page for.
*/
async $(selector) {
return this.mainFrame().$(selector);
}
/**
* @remarks
*
* The only difference between {@link Page.evaluate | page.evaluate} and
* `page.evaluateHandle` is that `evaluateHandle` will return the value
* wrapped in an in-page object.
*
* If the function passed to `page.evaluteHandle` returns a Promise, the
* function will wait for the promise to resolve and return its value.
*
* You can pass a string instead of a function (although functions are
* recommended as they are easier to debug and use with TypeScript):
*
* @example
* ```
* const aHandle = await page.evaluateHandle('document')
* ```
*
* @example
* {@link JSHandle} instances can be passed as arguments to the `pageFunction`:
* ```
* const aHandle = await page.evaluateHandle(() => document.body);
* const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle);
* console.log(await resultHandle.jsonValue());
* await resultHandle.dispose();
* ```
*
* Most of the time this function returns a {@link JSHandle},
* but if `pageFunction` returns a reference to an element,
* you instead get an {@link ElementHandle} back:
*
* @example
* ```
* const button = await page.evaluateHandle(() => document.querySelector('button'));
* // can call `click` because `button` is an `ElementHandle`
* await button.click();
* ```
*
* The TypeScript definitions assume that `evaluateHandle` returns
* a `JSHandle`, but if you know it's going to return an
* `ElementHandle`, pass it as the generic argument:
*
* ```
* const button = await page.evaluateHandle<ElementHandle>(...);
* ```
*
* @param pageFunction - a function that is run within the page
* @param args - arguments to be passed to the pageFunction
*/
async evaluateHandle(pageFunction, ...args) {
const context = await this.mainFrame().executionContext();
return context.evaluateHandle(pageFunction, ...args);
}
/**
* This method iterates the JavaScript heap and finds all objects with the
* given prototype.
*
* @remarks
* Shortcut for
* {@link ExecutionContext.queryObjects |
* page.mainFrame().executionContext().queryObjects(prototypeHandle)}.
*
* @example
*
* ```js
* // Create a Map object
* await page.evaluate(() => window.map = new Map());
* // Get a handle to the Map object prototype
* const mapPrototype = await page.evaluateHandle(() => Map.prototype);
* // Query all map instances into an array
* const mapInstances = await page.queryObjects(mapPrototype);
* // Count amount of map objects in heap
* const count = await page.evaluate(maps => maps.length, mapInstances);
* await mapInstances.dispose();
* await mapPrototype.dispose();
* ```
* @param prototypeHandle - a handle to the object prototype.
* @returns Promise which resolves to a handle to an array of objects with
* this prototype.
*/
async queryObjects(prototypeHandle) {
const context = await this.mainFrame().executionContext();
return context.queryObjects(prototypeHandle);
}
/**
* This method runs `document.querySelector` within the page and passes the
* result as the first argument to the `pageFunction`.
*
* @remarks
*
* If no element is found matching `selector`, the method will throw an error.
*
* If `pageFunction` returns a promise `$eval` will wait for the promise to
* resolve and then return its value.
*
* @example
*
* ```
* const searchValue = await page.$eval('#search', el => el.value);
* const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
* const html = await page.$eval('.main-container', el => el.outerHTML);
* ```
*
* If you are using TypeScript, you may have to provide an explicit type to the
* first argument of the `pageFunction`.
* By default it is typed as `Element`, but you may need to provide a more
* specific sub-type:
*
* @example
*
* ```
* // if you don't provide HTMLInputElement here, TS will error
* // as `value` is not on `Element`
* const searchValue = await page.$eval('#search', (el: HTMLInputElement) => el.value);
* ```
*
* The compiler should be able to infer the return type
* from the `pageFunction` you provide. If it is unable to, you can use the generic
* type to tell the compiler what return type you expect from `$eval`:
*
* @example
*
* ```
* // The compiler can infer the return type in this case, but if it can't
* // or if you want to be more explicit, provide it as the generic type.
* const searchValue = await page.$eval<string>(
* '#search', (el: HTMLInputElement) => el.value
* );
* ```
*
* @param selector - the
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
* to query for
* @param pageFunction - the function to be evaluated in the page context.
* Will be passed the result of `document.querySelector(selector)` as its
* first argument.
* @param args - any additional arguments to pass through to `pageFunction`.
*
* @returns The result of calling `pageFunction`. If it returns an element it
* is wrapped in an {@link ElementHandle}, else the raw value itself is
* returned.
*/
async $eval(selector, pageFunction, ...args) {
return this.mainFrame().$eval(selector, pageFunction, ...args);
}
/**
* This method runs `Array.from(document.querySelectorAll(selector))` within
* the page and passes the result as the first argument to the `pageFunction`.
*
* @remarks
*
* If `pageFunction` returns a promise `$$eval` will wait for the promise to
* resolve and then return its value.
*
* @example
*
* ```
* // get the amount of divs on the page
* const divCount = await page.$$eval('div', divs => divs.length);
*
* // get the text content of all the `.options` elements:
* const options = await page.$$eval('div > span.options', options => {
* return options.map(option => option.textContent)
* });
* ```
*
* If you are using TypeScript, you may have to provide an explicit type to the
* first argument of the `pageFunction`.
* By default it is typed as `Element[]`, but you may need to provide a more
* specific sub-type:
*
* @example
*
* ```
* // if you don't provide HTMLInputElement here, TS will error
* // as `value` is not on `Element`
* await page.$$eval('input', (elements: HTMLInputElement[]) => {
* return elements.map(e => e.value);
* });
* ```
*
* The compiler should be able to infer the return type
* from the `pageFunction` you provide. If it is unable to, you can use the generic
* type to tell the compiler what return type you expect from `$$eval`:
*
* @example
*
* ```
* // The compiler can infer the return type in this case, but if it can't
* // or if you want to be more explicit, provide it as the generic type.
* const allInputValues = await page.$$eval<string[]>(
* 'input', (elements: HTMLInputElement[]) => elements.map(e => e.textContent)
* );
* ```
*
* @param selector - the
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
* to query for
* @param pageFunction - the function to be evaluated in the page context. Will
* be passed the result of `Array.from(document.querySelectorAll(selector))`
* as its first argument.
* @param args - any additional arguments to pass through to `pageFunction`.
*
* @returns The result of calling `pageFunction`. If it returns an element it
* is wrapped in an {@link ElementHandle}, else the raw value itself is
* returned.
*/
async $$eval(selector, pageFunction, ...args) {
return this.mainFrame().$$eval(selector, pageFunction, ...args);
}
/**
* The method runs `document.querySelectorAll` within the page. If no elements
* match the selector, the return value resolves to `[]`.
* @remarks
* Shortcut for {@link Frame.$$ | Page.mainFrame().$$(selector) }.
* @param selector - A `selector` to query page for
*/
async $$(selector) {
return this.mainFrame().$$(selector);
}
/**
* The method evaluates the XPath expression relative to the page document as
* its context node. If there are no such elements, the method resolves to an
* empty array.
* @remarks
* Shortcut for {@link Frame.$x | Page.mainFrame().$x(expression) }.
* @param expression - Expression to evaluate
*/
async $x(expression) {
return this.mainFrame().$x(expression);
}
/**
* If no URLs are specified, this method returns cookies for the current page
* URL. If URLs are specified, only cookies for those URLs are returned.
*/
async cookies(...urls) {
const originalCookies = (await this._client.send('Network.getCookies', {
urls: urls.length ? urls : [this.url()],
})).cookies;
const unsupportedCookieAttributes = ['priority'];
const filterUnsupportedAttributes = (cookie) => {
for (const attr of unsupportedCookieAttributes)
delete cookie[attr];
return cookie;
};
return originalCookies.map(filterUnsupportedAttributes);
}
async deleteCookie(...cookies) {
const pageURL = this.url();
for (const cookie of cookies) {
const item = Object.assign({}, cookie);
if (!cookie.url && pageURL.startsWith('http'))
item.url = pageURL;
await this._client.send('Network.deleteCookies', item);
}
}
/**
* @example
* ```js
* await page.setCookie(cookieObject1, cookieObject2);
* ```
*/
async setCookie(...cookies) {
const pageURL = this.url();
const startsWithHTTP = pageURL.startsWith('http');
const items = cookies.map((cookie) => {
const item = Object.assign({}, cookie);
if (!item.url && startsWithHTTP)
item.url = pageURL;
(0, assert_js_1.assert)(item.url !== 'about:blank', `Blank page can not have cookie "${item.name}"`);
(0, assert_js_1.assert)(!String.prototype.startsWith.call(item.url || '', 'data:'), `Data URL page can not have cookie "${item.name}"`);
return item;
});
await this.deleteCookie(...items);
if (items.length)
await this._client.send('Network.setCookies', { cookies: items });
}
/**
* Adds a `<script>` tag into the page with the desired URL or content.
* @remarks
* Shortcut for {@link Frame.addScriptTag | page.mainFrame().addScriptTag(options) }.
* @returns Promise which resolves to the added tag when the script's onload fires or
* when the script content was injected into frame.
*/
async addScriptTag(options) {
return this.mainFrame().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.
* @returns Promise which resolves to the added tag when the stylesheet's
* onload fires or when the CSS content was injected into frame.
*/
async addStyleTag(options) {
return this.mainFrame().addStyleTag(options);
}
/**
* The method adds a function called `name` on the page's `window` object. When
* called, the function executes `puppeteerFunction` in node.js and returns a
* `Promise` which resolves to the return value of `puppeteerFunction`.
*
* If the puppeteerFunction returns a `Promise`, it will be awaited.
*
* NOTE: Functions installed via `page.exposeFunction` survive navigations.
* @param name - Name of the function on the window object
* @param puppeteerFunction - Callback function which will be called in
* Puppeteer's context.
* @example
* An example of adding an `md5` function into the page:
* ```js
* const puppeteer = require('puppeteer');
* const crypto = require('crypto');
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* page.on('console', (msg) => console.log(msg.text()));
* await page.exposeFunction('md5', (text) =>
* crypto.createHash('md5').update(text).digest('hex')
* );
* await page.evaluate(async () => {
* // use window.md5 to compute hashes
* const myString = 'PUPPETEER';
* const myHash = await window.md5(myString);
* console.log(`md5 of ${myString} is ${myHash}`);
* });
* await browser.close();
* })();
* ```
* An example of adding a `window.readfile` function into the page:
* ```js
* const puppeteer = require('puppeteer');
* const fs = require('fs');
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* page.on('console', (msg) => console.log(msg.text()));
* await page.exposeFunction('readfile', async (filePath) => {
* return new Promise((resolve, reject) => {
* fs.readFile(filePath, 'utf8', (err, text) => {
* if (err) reject(err);
* else resolve(text);
* });
* });
* });
* await page.evaluate(async () => {
* // use window.readfile to read contents of a file
* const content = await window.readfile('/etc/hosts');
* console.log(content);
* });
* await browser.close();
* })();
* ```
*/
async exposeFunction(name, puppeteerFunction) {
if (this._pageBindings.has(name))
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
let exposedFunction;
if (typeof puppeteerFunction === 'function') {
exposedFunction = puppeteerFunction;
}
else if (typeof puppeteerFunction.default === 'function') {
exposedFunction = puppeteerFunction.default;
}
else {
throw new Error(`Failed to add page binding with name ${name}: ${puppeteerFunction} is not a function or a module with a default export.`);
}
this._pageBindings.set(name, exposedFunction);
const expression = helper_js_1.helper.pageBindingInitString('exposedFun', 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(helper_js_1.debugError)));
}
/**
* Provide credentials for `HTTP authentication`.
* @remarks To disable authentication, pass `null`.
*/
async authenticate(credentials) {
return this._frameManager.networkManager().authenticate(credentials);
}
/**
* The extra HTTP headers will be sent with every request the page initiates.
* NOTE: All HTTP header names are lowercased. (HTTP headers are
* case-insensitive, so this shouldn’t impact your server code.)
* NOTE: page.setExtraHTTPHeaders does not guarantee the order of headers in
* the outgoing requests.
* @param headers - An object containing additional HTTP headers to be sent
* with every request. All header values must be strings.
* @returns
*/
async setExtraHTTPHeaders(headers) {
return this._frameManager.networkManager().setExtraHTTPHeaders(headers);
}
/**
* @param userAgent - Specific user agent to use in this page
* @param userAgentData - Specific user agent client hint data to use in this
* page
* @returns Promise which resolves when the user agent is set.
*/
async setUserAgent(userAgent, userAgentMetadata) {
return this._frameManager
.networkManager()
.setUserAgent(userAgent, userAgentMetadata);
}
/**
* @returns Object containing metrics as key/value pairs.
*
* - `Timestamp` : The timestamp when the metrics sample was taken.
*
* - `Documents` : Number of documents in the page.
*
* - `Frames` : Number of frames in the page.
*
* - `JSEventListeners` : Number of events in the page.
*
* - `Nodes` : Number of DOM nodes in the page.
*
* - `LayoutCount` : Total number of full or partial page layout.
*
* - `RecalcStyleCount` : Total number of page style recalculations.
*
* - `LayoutDuration` : Combined durations of all page layouts.
*
* - `RecalcStyleDuration` : Combined duration of all page style
* recalculations.
*
* - `ScriptDuration` : Combined duration of JavaScript execution.
*
* - `TaskDuration` : Combined duration of all tasks performed by the browser.
*
*
* - `JSHeapUsedSize` : Used JavaScript heap size.
*
* - `JSHeapTotalSize` : Total JavaScript heap size.
* @remarks
* NOTE: All timestamps are in monotonic time: monotonically increasing time
* in seconds since an arbitrary point in the past.
*/
async metrics() {
const response = await this._client.send('Performance.getMetrics');
return this._buildMetricsObject(response.metrics);
}
_emitMetrics(event) {
this.emit("metrics" /* Metrics */, {
title: event.title,
metrics: this._buildMetricsObject(event.metrics),
});
}
_buildMetricsObject(metrics) {
const result = {};
for (const metric of metrics || []) {
if (supportedMetrics.has(metric.name))
result[metric.name] = metric.value;
}
return result;
}
_handleException(exceptionDetails) {
const message = helper_js_1.helper.getExceptionMessage(exceptionDetails);
const err = new Error(message);
err.stack = ''; // Don't report clientside error with a node stack attached
this.emit("pageerror" /* PageError */, err);
}
async _onConsoleAPI(event) {
if (event.executionContextId === 0) {
// DevTools protocol stores the last 1000 console messages. These
// messages are always reported even for removed execution contexts. In
// this case, they are marked with executionContextId = 0 and are
// reported upon enabling Runtime agent.
//
// Ignore these messages since:
// - there's no execution context we can use to operate with message
// arguments
// - these messages are reported before Puppeteer clients can subscribe
// to the 'console'
// page event.
//
// @see https://github.com/puppeteer/puppeteer/issues/3865
return;
}
const context = this._frameManager.executionContextById(event.executionContextId, this._client);
const values = event.args.map((arg) => (0, JSHandle_js_1.createJSHandle)(context, arg));
this._addConsoleMessage(event.type, values, event.stackTrace);
}
async _onBindingCalled(event) {
let payload;
try {
payload = JSON.parse(event.payload);
}
catch {
// The binding was either called by something in the page or it was
// called before our wrapper was initialized.
return;
}
const { type, name, seq, args } = payload;
if (type !== 'exposedFun' || !this._pageBindings.has(name))
return;
let expression = null;
try {
const result = await this._pageBindings.get(name)(...args);
expression = helper_js_1.helper.pageBindingDeliverResultString(name, seq, result);
}
catch (error) {
if (error instanceof Error)
expression = helper_js_1.helper.pageBindingDeliverErrorString(name, seq, error.message, error.stack);
else
expression = helper_js_1.helper.pageBindingDeliverErrorValueString(name, seq, error);
}
this._client
.send('Runtime.evaluate', {
expression,
contextId: event.executionContextId,
})
.catch(helper_js_1.debugError);
}
_addConsoleMessage(type, args, stackTrace) {
if (!this.listenerCount("console" /* Console */)) {
args.forEach((arg) => arg.dispose());
return;
}
const textTokens = [];
for (const arg of args) {
const remoteObject = arg._remoteObject;
if (remoteObject.objectId)
textTokens.push(arg.toString());
else
textTokens.push(helper_js_1.helper.valueFromRemoteObject(remoteObject));
}
const stackTraceLocations = [];
if (stackTrace) {
for (const callFrame of stackTrace.callFrames) {
stackTraceLocations.push({
url: callFrame.url,
lineNumber: callFrame.lineNumber,
columnNumber: callFrame.columnNumber,
});
}
}
const message = new ConsoleMessage_js_1.ConsoleMessage(type, textTokens.join(' '), args, stackTraceLocations);
this.emit("console" /* Console */, message);
}
_onDialog(event) {
let dialogType = null;
const validDialogTypes = new Set([
'alert',
'confirm',
'prompt',
'beforeunload',
]);
if (validDialogTypes.has(event.type)) {
dialogType = event.type;
}
(0, assert_js_1.assert)(dialogType, 'Unknown javascript dialog type: ' + event.type);
const dialog = new Dialog_js_1.Dialog(this._client, dialogType, event.message, event.defaultPrompt);
this.emit("dialog" /* Dialog */, dialog);
}
/**
* Resets default white background
*/
async _resetDefaultBackgroundColor() {
await this._client.send('Emulation.setDefaultBackgroundColorOverride');
}
/**
* Hides default white background
*/
async _setTransparentBackgroundColor() {
await this._client.send('Emulation.setDefaultBackgroundColorOverride', {
color: { r: 0, g: 0, b: 0, a: 0 },
});
}
/**
*
* @returns
* @remarks Shortcut for
* {@link Frame.url | page.mainFrame().url()}.
*/
url() {
return this.mainFrame().url();
}
async content() {
return await this._frameManager.mainFrame().content();
}
/**
* @param html - HTML markup to assign to the page.
* @param options - Parameters that has some properties.
* @remarks
* The parameter `options` might have the following options.
*
* - `timeout` : Maximum time in milliseconds for resources to load, defaults
* to 30 seconds, pass `0` to disable timeout. The default value can be
* changed by using the
* {@link Page.setDefaultNavigationTimeout |
* page.setDefaultNavigationTimeout(timeout)}
* or {@link Page.setDefaultTimeout | page.setDefaultTimeout(timeout)}
* methods.
*
* - `waitUntil`: When to consider setting markup succeeded, defaults to `load`.
* Given an array of event strings, setting content is considered to be
* successful after all events have been fired. Events can be either:<br/>
* - `load` : consider setting content to be finished when the `load` event is
* fired.<br/>
* - `domcontentloaded` : consider setting content to be finished when the
* `DOMContentLoaded` event is fired.<br/>
* - `networkidle0` : consider setting content to be finished when there are no
* more than 0 network connections for at least `500` ms.<br/>
* - `networkidle2` : consider setting content to be finished when there are no
* more than 2 network connections for at least `500` ms.
*/
async setContent(html, options = {}) {
await this._frameManager.mainFrame().setContent(html, options);
}
/**
* @param url - URL to navigate page to. The URL should include scheme, e.g.
* `https://`
* @param options - Navigation Parameter
* @returns Promise which resolves to the main resource response. In case of
* multiple redirects, the navigation will resolve with the response of the
* last redirect.
* @remarks
* The argument `options` might have the following properties:
*
* - `timeout` : Maximum navigation time in milliseconds, defaults to 30
* seconds, pass 0 to disable timeout. The default value can be changed by
* using the
* {@link Page.setDefaultNavigationTimeout |
* page.setDefaultNavigationTimeout(timeout)}
* or {@link Page.setDefaultTimeout | page.setDefaultTimeout(timeout)}
* methods.
*
* - `waitUntil`:When to consider navigation succeeded, defaults to `load`.
* Given an array of event strings, navigation is considered to be successful
* after all events have been fired. Events can be either:<br/>
* - `load` : consider navigation to be finished when the load event is
* fired.<br/>
* - `domcontentloaded` : consider navigation to be finished when the
* DOMContentLoaded event is fired.<br/>
* - `networkidle0` : consider navigation to be finished when there are no
* more than 0 network connections for at least `500` ms.<br/>
* - `networkidle2` : consider navigation to be finished when there are no
* more than 2 network connections for at least `500` ms.
*
* - `referer` : Referer header value. If provided it will take preference
* over the referer header value set by
* {@link Page.setExtraHTTPHeaders |page.setExtraHTTPHeaders()}.
*
* `page.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.
*
* `page.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 response.status().
*
* NOTE: `page.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}.
*
* Shortcut for {@link Frame.goto | page.mainFrame().goto(url, options)}.
*/
async goto(url, options = {}) {
return await this._frameManager.mainFrame().goto(url, options);
}
/**
* @param options - Navigation parameters which might have the following
* properties:
* @returns Promise which resolves to the main resource response. In case of
* multiple redirects, the navigation will resolve with the response of the
* last redirect.
* @remarks
* The argument `options` might have the following properties:
*
* - `timeout` : Maximum navigation time in milliseconds, defaults to 30
* seconds, pass 0 to disable timeout. The default value can be changed by
* using the
* {@link Page.setDefaultNavigationTimeout |
* page.setDefaultNavigationTimeout(timeout)}
* or {@link Page.setDefaultTimeout | page.setDefaultTimeout(timeout)}
* methods.
*
* - `waitUntil`: When to consider navigation succeeded, defaults to `load`.
* Given an array of event strings, navigation is considered to be
* successful after all events have been fired. Events can be either:<br/>
* - `load` : consider navigation to be finished when the load event is fired.<br/>
* - `domcontentloaded` : consider navigation to be finished when the
* DOMContentLoaded event is fired.<br/>
* - `networkidle0` : consider navigation to be finished when there are no
* more than 0 network connections for at least `500` ms.<br/>
* - `networkidle2` : consider navigation to be finished when there are no
* more than 2 network connections for at least `500` ms.
*/
async reload(options) {
const result = await Promise.all([
this.waitForNavigation(options),
this._client.send('Page.reload'),
]);
return result[0];
}
/**
* This resolves when the page navigates to a new URL or reloads. It is useful
* when y