95d867aaac
When an evaluation causes a navigation, for example: ```js await page.evaluate(() => window.reload()); ``` sometimes we process the ExecutionContextDestroyed event before the ack from the evaluate. When we do get the ack from the evaluate, we try to build a JSHandle for it, and try to find the execution by id. But it is gone, and we throw an error. This patch switches createJSHandle to accept an ExecutionContext instead of just an id. This bug was making the test `should throw a nice error after a navigation` flaky.
1206 lines
36 KiB
JavaScript
1206 lines
36 KiB
JavaScript
/**
|
|
* Copyright 2017 Google Inc. All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const EventEmitter = require('events');
|
|
const mime = require('mime');
|
|
const {NetworkManager} = require('./NetworkManager');
|
|
const NavigatorWatcher = require('./NavigatorWatcher');
|
|
const Dialog = require('./Dialog');
|
|
const EmulationManager = require('./EmulationManager');
|
|
const {FrameManager} = require('./FrameManager');
|
|
const {Keyboard, Mouse, Touchscreen} = require('./Input');
|
|
const Tracing = require('./Tracing');
|
|
const {helper, debugError, assert} = require('./helper');
|
|
const {Coverage} = require('./Coverage');
|
|
const Worker = require('./Worker');
|
|
|
|
const writeFileAsync = helper.promisify(fs.writeFile);
|
|
|
|
class Page extends EventEmitter {
|
|
/**
|
|
* @param {!Puppeteer.CDPSession} client
|
|
* @param {!Puppeteer.Target} target
|
|
* @param {boolean} ignoreHTTPSErrors
|
|
* @param {boolean} setDefaultViewport
|
|
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
|
|
* @return {!Promise<!Page>}
|
|
*/
|
|
static async create(client, target, ignoreHTTPSErrors, setDefaultViewport, screenshotTaskQueue) {
|
|
|
|
await client.send('Page.enable');
|
|
const {frameTree} = await client.send('Page.getFrameTree');
|
|
const page = new Page(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);
|
|
|
|
await Promise.all([
|
|
client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false}),
|
|
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
|
client.send('Network.enable', {}),
|
|
client.send('Runtime.enable', {}),
|
|
client.send('Security.enable', {}),
|
|
client.send('Performance.enable', {}),
|
|
client.send('Log.enable', {}),
|
|
]);
|
|
if (ignoreHTTPSErrors)
|
|
await client.send('Security.setOverrideCertificateErrors', {override: true});
|
|
// Initialize default page size.
|
|
if (setDefaultViewport)
|
|
await page.setViewport({width: 800, height: 600});
|
|
|
|
return page;
|
|
}
|
|
|
|
/**
|
|
* @param {!Puppeteer.CDPSession} client
|
|
* @param {!Puppeteer.Target} target
|
|
* @param {!Protocol.Page.FrameTree} frameTree
|
|
* @param {boolean} ignoreHTTPSErrors
|
|
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
|
|
*/
|
|
constructor(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue) {
|
|
super();
|
|
this._closed = false;
|
|
this._client = client;
|
|
this._target = target;
|
|
this._keyboard = new Keyboard(client);
|
|
this._mouse = new Mouse(client, this._keyboard);
|
|
this._touchscreen = new Touchscreen(client, this._keyboard);
|
|
/** @type {!FrameManager} */
|
|
this._frameManager = new FrameManager(client, frameTree, this);
|
|
this._networkManager = new NetworkManager(client, this._frameManager);
|
|
this._emulationManager = new EmulationManager(client);
|
|
this._tracing = new Tracing(client);
|
|
/** @type {!Map<string, Function>} */
|
|
this._pageBindings = new Map();
|
|
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
|
this._coverage = new Coverage(client);
|
|
this._defaultNavigationTimeout = 30000;
|
|
this._javascriptEnabled = true;
|
|
|
|
this._screenshotTaskQueue = screenshotTaskQueue;
|
|
|
|
/** @type {!Map<string, Worker>} */
|
|
this._workers = new Map();
|
|
client.on('Target.attachedToTarget', event => {
|
|
if (event.targetInfo.type !== 'worker') {
|
|
// If we don't detach from service workers, they will never die.
|
|
client.send('Target.detachFromTarget', {
|
|
sessionId: event.sessionId
|
|
}).catch(debugError);
|
|
return;
|
|
}
|
|
const session = client._createSession(event.targetInfo.type, event.sessionId);
|
|
const worker = new Worker(session, event.targetInfo.url, this._addConsoleMessage.bind(this), this._handleException.bind(this));
|
|
this._workers.set(event.sessionId, worker);
|
|
this.emit(Page.Events.WorkerCreated, worker);
|
|
|
|
});
|
|
client.on('Target.detachedFromTarget', event => {
|
|
const worker = this._workers.get(event.sessionId);
|
|
if (!worker)
|
|
return;
|
|
this.emit(Page.Events.WorkerDestroyed, worker);
|
|
this._workers.delete(event.sessionId);
|
|
});
|
|
|
|
this._frameManager.on(FrameManager.Events.FrameAttached, event => this.emit(Page.Events.FrameAttached, event));
|
|
this._frameManager.on(FrameManager.Events.FrameDetached, event => this.emit(Page.Events.FrameDetached, event));
|
|
this._frameManager.on(FrameManager.Events.FrameNavigated, event => this.emit(Page.Events.FrameNavigated, event));
|
|
|
|
this._networkManager.on(NetworkManager.Events.Request, event => this.emit(Page.Events.Request, event));
|
|
this._networkManager.on(NetworkManager.Events.Response, event => this.emit(Page.Events.Response, event));
|
|
this._networkManager.on(NetworkManager.Events.RequestFailed, event => this.emit(Page.Events.RequestFailed, event));
|
|
this._networkManager.on(NetworkManager.Events.RequestFinished, event => this.emit(Page.Events.RequestFinished, event));
|
|
|
|
client.on('Page.domContentEventFired', event => this.emit(Page.Events.DOMContentLoaded));
|
|
client.on('Page.loadEventFired', event => this.emit(Page.Events.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('Security.certificateError', event => this._onCertificateError(event));
|
|
client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
|
|
client.on('Performance.metrics', event => this._emitMetrics(event));
|
|
client.on('Log.entryAdded', event => this._onLogEntryAdded(event));
|
|
this._target._isClosedPromise.then(() => {
|
|
this.emit(Page.Events.Close);
|
|
this._closed = true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return {!Puppeteer.Target}
|
|
*/
|
|
target() {
|
|
return this._target;
|
|
}
|
|
|
|
/**
|
|
* @return {!Puppeteer.Browser}
|
|
*/
|
|
browser() {
|
|
return this._target.browser();
|
|
}
|
|
|
|
_onTargetCrashed() {
|
|
this.emit('error', new Error('Page crashed!'));
|
|
}
|
|
|
|
/**
|
|
* @param {!Protocol.Log.entryAddedPayload} event
|
|
*/
|
|
_onLogEntryAdded(event) {
|
|
const {level, text, args, source} = event.entry;
|
|
if (args)
|
|
args.map(arg => helper.releaseObject(this._client, arg));
|
|
if (source !== 'worker')
|
|
this.emit(Page.Events.Console, new ConsoleMessage(level, text));
|
|
}
|
|
|
|
/**
|
|
* @return {!Puppeteer.Frame}
|
|
*/
|
|
mainFrame() {
|
|
return this._frameManager.mainFrame();
|
|
}
|
|
|
|
/**
|
|
* @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 {!Array<Puppeteer.Frame>}
|
|
*/
|
|
frames() {
|
|
return this._frameManager.frames();
|
|
}
|
|
|
|
/**
|
|
* @return {!Array<!Worker>}
|
|
*/
|
|
workers() {
|
|
return Array.from(this._workers.values());
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} value
|
|
*/
|
|
async setRequestInterception(value) {
|
|
return this._networkManager.setRequestInterception(value);
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} enabled
|
|
*/
|
|
setOfflineMode(enabled) {
|
|
return this._networkManager.setOfflineMode(enabled);
|
|
}
|
|
|
|
/**
|
|
* @param {number} timeout
|
|
*/
|
|
setDefaultNavigationTimeout(timeout) {
|
|
this._defaultNavigationTimeout = timeout;
|
|
}
|
|
|
|
/**
|
|
* @param {!Protocol.Security.certificateErrorPayload} event
|
|
*/
|
|
_onCertificateError(event) {
|
|
if (!this._ignoreHTTPSErrors)
|
|
return;
|
|
this._client.send('Security.handleCertificateError', {
|
|
eventId: event.eventId,
|
|
action: 'continue'
|
|
}).catch(debugError);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @return {!Promise<?Puppeteer.ElementHandle>}
|
|
*/
|
|
async $(selector) {
|
|
return this.mainFrame().$(selector);
|
|
}
|
|
|
|
/**
|
|
* @param {function()|string} pageFunction
|
|
* @param {!Array<*>} args
|
|
* @return {!Promise<!Puppeteer.JSHandle>}
|
|
*/
|
|
async evaluateHandle(pageFunction, ...args) {
|
|
const context = await this.mainFrame().executionContext();
|
|
return context.evaluateHandle(pageFunction, ...args);
|
|
}
|
|
|
|
/**
|
|
* @param {!Puppeteer.JSHandle} prototypeHandle
|
|
* @return {!Promise<!Puppeteer.JSHandle>}
|
|
*/
|
|
async queryObjects(prototypeHandle) {
|
|
const context = await this.mainFrame().executionContext();
|
|
return context.queryObjects(prototypeHandle);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {function()|string} pageFunction
|
|
* @param {!Array<*>} args
|
|
* @return {!Promise<(!Object|undefined)>}
|
|
*/
|
|
async $eval(selector, pageFunction, ...args) {
|
|
return this.mainFrame().$eval(selector, pageFunction, ...args);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {Function|string} pageFunction
|
|
* @param {!Array<*>} args
|
|
* @return {!Promise<(!Object|undefined)>}
|
|
*/
|
|
async $$eval(selector, pageFunction, ...args) {
|
|
return this.mainFrame().$$eval(selector, pageFunction, ...args);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
|
|
*/
|
|
async $$(selector) {
|
|
return this.mainFrame().$$(selector);
|
|
}
|
|
|
|
/**
|
|
* @param {string} expression
|
|
* @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
|
|
*/
|
|
async $x(expression) {
|
|
return this.mainFrame().$x(expression);
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<string>} urls
|
|
* @return {!Promise<!Array<Network.Cookie>>}
|
|
*/
|
|
async cookies(...urls) {
|
|
return (await this._client.send('Network.getCookies', {
|
|
urls: urls.length ? urls : [this.url()]
|
|
})).cookies;
|
|
}
|
|
|
|
/**
|
|
* @param {Array<Network.CookieParam>} cookies
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Array<Network.CookieParam>} cookies
|
|
*/
|
|
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;
|
|
assert(
|
|
item.url !== 'about:blank',
|
|
`Blank page can not have cookie "${item.name}"`
|
|
);
|
|
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 });
|
|
}
|
|
|
|
/**
|
|
* @param {Object} options
|
|
* @return {!Promise<!Puppeteer.ElementHandle>}
|
|
*/
|
|
async addScriptTag(options) {
|
|
return this.mainFrame().addScriptTag(options);
|
|
}
|
|
|
|
/**
|
|
* @param {Object} options
|
|
* @return {!Promise<!Puppeteer.ElementHandle>}
|
|
*/
|
|
async addStyleTag(options) {
|
|
return this.mainFrame().addStyleTag(options);
|
|
}
|
|
|
|
/**
|
|
* @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(fulfill => callbacks.set(seq, fulfill));
|
|
binding(JSON.stringify({name: bindingName, seq, args}));
|
|
return promise;
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {?{username: string, password: string}} credentials
|
|
*/
|
|
async authenticate(credentials) {
|
|
return this._networkManager.authenticate(credentials);
|
|
}
|
|
|
|
/**
|
|
* @param {!Object<string, string>} headers
|
|
*/
|
|
async setExtraHTTPHeaders(headers) {
|
|
return this._networkManager.setExtraHTTPHeaders(headers);
|
|
}
|
|
|
|
/**
|
|
* @param {string} userAgent
|
|
*/
|
|
async setUserAgent(userAgent) {
|
|
return this._networkManager.setUserAgent(userAgent);
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise<!Object>}
|
|
*/
|
|
async metrics() {
|
|
const response = await this._client.send('Performance.getMetrics');
|
|
return this._buildMetricsObject(response.metrics);
|
|
}
|
|
|
|
/**
|
|
* @param {*} event
|
|
*/
|
|
_emitMetrics(event) {
|
|
this.emit(Page.Events.Metrics, {
|
|
title: event.title,
|
|
metrics: this._buildMetricsObject(event.metrics)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {?Array<!Protocol.Performance.Metric>} metrics
|
|
* @return {!Object}
|
|
*/
|
|
_buildMetricsObject(metrics) {
|
|
const result = {};
|
|
for (const metric of metrics || []) {
|
|
if (supportedMetrics.has(metric.name))
|
|
result[metric.name] = metric.value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @param {!Protocol.Runtime.ExceptionDetails} exceptionDetails
|
|
*/
|
|
_handleException(exceptionDetails) {
|
|
const message = helper.getExceptionMessage(exceptionDetails);
|
|
const err = new Error(message);
|
|
err.stack = ''; // Don't report clientside error with a node stack attached
|
|
this.emit(Page.Events.PageError, err);
|
|
}
|
|
|
|
/**
|
|
* @param {!Protocol.Runtime.consoleAPICalledPayload} event
|
|
*/
|
|
async _onConsoleAPI(event) {
|
|
const context = this._frameManager.executionContextById(event.executionContextId);
|
|
const values = event.args.map(arg => this._frameManager.createJSHandle(context, arg));
|
|
this._addConsoleMessage(event.type, values);
|
|
}
|
|
|
|
/**
|
|
* @param {!Protocol.Runtime.bindingCalledPayload} event
|
|
*/
|
|
async _onBindingCalled(event) {
|
|
const {name, seq, args} = JSON.parse(event.payload);
|
|
const result = await this._pageBindings.get(name)(...args);
|
|
const expression = helper.evaluationString(deliverResult, name, seq, result);
|
|
this._client.send('Runtime.evaluate', { expression, contextId: event.executionContextId }).catch(debugError);
|
|
|
|
function deliverResult(name, seq, result) {
|
|
window[name]['callbacks'].get(seq)(result);
|
|
window[name]['callbacks'].delete(seq);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} type
|
|
* @param {!Array<!Puppeteer.JSHandle>} args
|
|
*/
|
|
_addConsoleMessage(type, args) {
|
|
if (!this.listenerCount(Page.Events.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.valueFromRemoteObject(remoteObject));
|
|
}
|
|
const message = new ConsoleMessage(type, textTokens.join(' '), args);
|
|
this.emit(Page.Events.Console, message);
|
|
}
|
|
|
|
_onDialog(event) {
|
|
let dialogType = null;
|
|
if (event.type === 'alert')
|
|
dialogType = Dialog.Type.Alert;
|
|
else if (event.type === 'confirm')
|
|
dialogType = Dialog.Type.Confirm;
|
|
else if (event.type === 'prompt')
|
|
dialogType = Dialog.Type.Prompt;
|
|
else if (event.type === 'beforeunload')
|
|
dialogType = Dialog.Type.BeforeUnload;
|
|
assert(dialogType, 'Unknown javascript dialog type: ' + event.type);
|
|
const dialog = new Dialog(this._client, dialogType, event.message, event.defaultPrompt);
|
|
this.emit(Page.Events.Dialog, dialog);
|
|
}
|
|
|
|
/**
|
|
* @return {!string}
|
|
*/
|
|
url() {
|
|
return this.mainFrame().url();
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise<String>}
|
|
*/
|
|
async content() {
|
|
return await this._frameManager.mainFrame().content();
|
|
}
|
|
|
|
/**
|
|
* @param {string} html
|
|
*/
|
|
async setContent(html) {
|
|
await this._frameManager.mainFrame().setContent(html);
|
|
}
|
|
|
|
/**
|
|
* @param {string} url
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Puppeteer.Response>}
|
|
*/
|
|
async goto(url, options = {}) {
|
|
const referrer = this._networkManager.extraHTTPHeaders()['referer'];
|
|
|
|
/** @type {Map<string, !Puppeteer.Request>} */
|
|
const requests = new Map();
|
|
const eventListeners = [
|
|
helper.addEventListener(this._networkManager, NetworkManager.Events.Request, request => {
|
|
if (!requests.get(request.url()))
|
|
requests.set(request.url(), request);
|
|
})
|
|
];
|
|
|
|
const mainFrame = this._frameManager.mainFrame();
|
|
const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout;
|
|
const watcher = new NavigatorWatcher(this._frameManager, mainFrame, timeout, options);
|
|
const navigationPromise = watcher.navigationPromise();
|
|
let error = await Promise.race([
|
|
navigate(this._client, url, referrer),
|
|
navigationPromise,
|
|
]);
|
|
if (!error)
|
|
error = await navigationPromise;
|
|
watcher.cancel();
|
|
helper.removeEventListeners(eventListeners);
|
|
if (error)
|
|
throw error;
|
|
const request = requests.get(mainFrame._navigationURL);
|
|
return request ? request.response() : null;
|
|
|
|
/**
|
|
* @param {!Puppeteer.CDPSession} client
|
|
* @param {string} url
|
|
* @param {string} referrer
|
|
* @return {!Promise<?Error>}
|
|
*/
|
|
async function navigate(client, url, referrer) {
|
|
try {
|
|
const response = await client.send('Page.navigate', {url, referrer});
|
|
return response.errorText ? new Error(`${response.errorText} at ${url}`) : null;
|
|
} catch (error) {
|
|
return error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Puppeteer.Response>}
|
|
*/
|
|
async reload(options) {
|
|
const [response] = await Promise.all([
|
|
this.waitForNavigation(options),
|
|
this._client.send('Page.reload')
|
|
]);
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Puppeteer.Response>}
|
|
*/
|
|
async waitForNavigation(options = {}) {
|
|
const mainFrame = this._frameManager.mainFrame();
|
|
const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout;
|
|
const watcher = new NavigatorWatcher(this._frameManager, mainFrame, timeout, options);
|
|
|
|
const responses = new Map();
|
|
const listener = helper.addEventListener(this._networkManager, NetworkManager.Events.Response, response => responses.set(response.url(), response));
|
|
const error = await watcher.navigationPromise();
|
|
helper.removeEventListeners([listener]);
|
|
if (error)
|
|
throw error;
|
|
return responses.get(this.mainFrame().url()) || null;
|
|
}
|
|
|
|
/**
|
|
* @param {(string|Function)} urlOrPredicate
|
|
* @param {!Object=} options
|
|
* @return {!Promise<!Puppeteer.Request>}
|
|
*/
|
|
async waitForRequest(urlOrPredicate, options = {}) {
|
|
const timeout = typeof options.timeout === 'number' ? options.timeout : 30000;
|
|
return helper.waitForEvent(this._networkManager, NetworkManager.Events.Request, request => {
|
|
if (helper.isString(urlOrPredicate))
|
|
return (urlOrPredicate === request.url());
|
|
if (typeof urlOrPredicate === 'function')
|
|
return !!(urlOrPredicate(request));
|
|
return false;
|
|
}, timeout);
|
|
}
|
|
|
|
/**
|
|
* @param {(string|Function)} urlOrPredicate
|
|
* @param {!Object=} options
|
|
* @return {!Promise<!Puppeteer.Response>}
|
|
*/
|
|
async waitForResponse(urlOrPredicate, options = {}) {
|
|
const timeout = typeof options.timeout === 'number' ? options.timeout : 30000;
|
|
return helper.waitForEvent(this._networkManager, NetworkManager.Events.Response, response => {
|
|
if (helper.isString(urlOrPredicate))
|
|
return (urlOrPredicate === response.url());
|
|
if (typeof urlOrPredicate === 'function')
|
|
return !!(urlOrPredicate(response));
|
|
return false;
|
|
}, timeout);
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Puppeteer.Response>}
|
|
*/
|
|
async goBack(options) {
|
|
return this._go(-1, options);
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Puppeteer.Response>}
|
|
*/
|
|
async goForward(options) {
|
|
return this._go(+1, options);
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Puppeteer.Response>}
|
|
*/
|
|
async _go(delta, options) {
|
|
const history = await this._client.send('Page.getNavigationHistory');
|
|
const entry = history.entries[history.currentIndex + delta];
|
|
if (!entry)
|
|
return null;
|
|
const [response] = await Promise.all([
|
|
this.waitForNavigation(options),
|
|
this._client.send('Page.navigateToHistoryEntry', {entryId: entry.id}),
|
|
]);
|
|
return response;
|
|
}
|
|
|
|
async bringToFront() {
|
|
await this._client.send('Page.bringToFront');
|
|
}
|
|
|
|
/**
|
|
* @param {!Object} options
|
|
*/
|
|
async emulate(options) {
|
|
return 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._client.send('Emulation.setScriptExecutionDisabled', { value: !enabled });
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} enabled
|
|
*/
|
|
async setBypassCSP(enabled) {
|
|
await this._client.send('Page.setBypassCSP', { enabled });
|
|
}
|
|
|
|
/**
|
|
* @param {?string} mediaType
|
|
*/
|
|
async emulateMedia(mediaType) {
|
|
assert(mediaType === 'screen' || mediaType === 'print' || mediaType === null, 'Unsupported media type: ' + mediaType);
|
|
await this._client.send('Emulation.setEmulatedMedia', {media: mediaType || ''});
|
|
}
|
|
|
|
/**
|
|
* @param {!Page.Viewport} viewport
|
|
*/
|
|
async setViewport(viewport) {
|
|
const needsReload = await this._emulationManager.emulateViewport(viewport);
|
|
this._viewport = viewport;
|
|
if (needsReload)
|
|
await this.reload();
|
|
}
|
|
|
|
/**
|
|
* @return {!Page.Viewport}
|
|
*/
|
|
viewport() {
|
|
return this._viewport;
|
|
}
|
|
|
|
/**
|
|
* @param {function()|string} pageFunction
|
|
* @param {!Array<*>} args
|
|
* @return {!Promise<*>}
|
|
*/
|
|
async evaluate(pageFunction, ...args) {
|
|
return this._frameManager.mainFrame().evaluate(pageFunction, ...args);
|
|
}
|
|
|
|
/**
|
|
* @param {function()|string} pageFunction
|
|
* @param {!Array<*>} args
|
|
*/
|
|
async evaluateOnNewDocument(pageFunction, ...args) {
|
|
const source = helper.evaluationString(pageFunction, ...args);
|
|
await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source });
|
|
}
|
|
|
|
/**
|
|
* @param {Boolean} enabled
|
|
* @returns {!Promise}
|
|
*/
|
|
async setCacheEnabled(enabled = true) {
|
|
await this._client.send('Network.setCacheDisabled', {cacheDisabled: !enabled});
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} 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));
|
|
}
|
|
return this._screenshotTaskQueue.postTask(this._screenshotTask.bind(this, screenshotType, options));
|
|
}
|
|
|
|
/**
|
|
* @param {"png"|"jpeg"} format
|
|
* @param {!Object=} options
|
|
* @return {!Promise<!Buffer|!String>}
|
|
*/
|
|
async _screenshotTask(format, options) {
|
|
await this._client.send('Target.activateTarget', {targetId: this._target._targetId});
|
|
let clip = options.clip ? Object.assign({}, options['clip']) : undefined;
|
|
if (clip)
|
|
clip.scale = 1;
|
|
|
|
if (options.fullPage) {
|
|
const metrics = await this._client.send('Page.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 mobile = this._viewport.isMobile || false;
|
|
const deviceScaleFactor = this._viewport.deviceScaleFactor || 1;
|
|
const landscape = this._viewport.isLandscape || false;
|
|
/** @type {!Protocol.Emulation.ScreenOrientation} */
|
|
const screenOrientation = landscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' };
|
|
await this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation });
|
|
}
|
|
|
|
if (options.omitBackground)
|
|
await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } });
|
|
const result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip });
|
|
if (options.omitBackground)
|
|
await this._client.send('Emulation.setDefaultBackgroundColorOverride');
|
|
|
|
if (options.fullPage)
|
|
await this.setViewport(this._viewport);
|
|
|
|
const buffer = options.encoding === 'base64' ? result.data : Buffer.from(result.data, 'base64');
|
|
if (options.path)
|
|
await writeFileAsync(options.path, buffer);
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<!Buffer>}
|
|
*/
|
|
async pdf(options = {}) {
|
|
const scale = options.scale || 1;
|
|
const displayHeaderFooter = !!options.displayHeaderFooter;
|
|
const headerTemplate = options.headerTemplate || '';
|
|
const footerTemplate = options.footerTemplate || '';
|
|
const printBackground = !!options.printBackground;
|
|
const landscape = !!options.landscape;
|
|
const pageRanges = options.pageRanges || '';
|
|
|
|
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 marginOptions = options.margin || {};
|
|
const marginTop = convertPrintParameterToInches(marginOptions.top) || 0;
|
|
const marginLeft = convertPrintParameterToInches(marginOptions.left) || 0;
|
|
const marginBottom = convertPrintParameterToInches(marginOptions.bottom) || 0;
|
|
const marginRight = convertPrintParameterToInches(marginOptions.right) || 0;
|
|
const preferCSSPageSize = options.preferCSSPageSize || false;
|
|
|
|
const result = await this._client.send('Page.printToPDF', {
|
|
landscape: landscape,
|
|
displayHeaderFooter: displayHeaderFooter,
|
|
headerTemplate: headerTemplate,
|
|
footerTemplate: footerTemplate,
|
|
printBackground: printBackground,
|
|
scale: scale,
|
|
paperWidth: paperWidth,
|
|
paperHeight: paperHeight,
|
|
marginTop: marginTop,
|
|
marginBottom: marginBottom,
|
|
marginLeft: marginLeft,
|
|
marginRight: marginRight,
|
|
pageRanges: pageRanges,
|
|
preferCSSPageSize: preferCSSPageSize
|
|
});
|
|
const buffer = Buffer.from(result.data, 'base64');
|
|
if (options.path)
|
|
await writeFileAsync(options.path, buffer);
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise<string>}
|
|
*/
|
|
async title() {
|
|
return this.mainFrame().title();
|
|
}
|
|
|
|
/**
|
|
* @param {!{runBeforeUnload: (boolean|undefined)}=} options
|
|
*/
|
|
async close(options = {runBeforeUnload: undefined}) {
|
|
assert(!!this._client._connection, 'Protocol error: Connection closed. Most likely the page has been closed.');
|
|
const runBeforeUnload = !!options.runBeforeUnload;
|
|
if (runBeforeUnload) {
|
|
await this._client.send('Page.close');
|
|
} else {
|
|
await this._client._connection.send('Target.closeTarget', { targetId: this._target._targetId });
|
|
await this._target._isClosedPromise;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
isClosed() {
|
|
return this._closed;
|
|
}
|
|
|
|
/**
|
|
* @return {!Mouse}
|
|
*/
|
|
get mouse() {
|
|
return this._mouse;
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {!Object=} options
|
|
*/
|
|
click(selector, options = {}) {
|
|
return this.mainFrame().click(selector, options);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
*/
|
|
focus(selector) {
|
|
return this.mainFrame().focus(selector);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
*/
|
|
hover(selector) {
|
|
return this.mainFrame().hover(selector);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {!Array<string>} values
|
|
* @return {!Promise<!Array<string>>}
|
|
*/
|
|
select(selector, ...values) {
|
|
return this.mainFrame().select(selector, ...values);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
*/
|
|
tap(selector) {
|
|
return this.mainFrame().tap(selector);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {string} text
|
|
* @param {{delay: (number|undefined)}=} options
|
|
*/
|
|
type(selector, text, options) {
|
|
return this.mainFrame().type(selector, text, options);
|
|
}
|
|
|
|
/**
|
|
* @param {(string|number|Function)} selectorOrFunctionOrTimeout
|
|
* @param {!Object=} options
|
|
* @param {!Array<*>} args
|
|
* @return {!Promise}
|
|
*/
|
|
waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
|
|
return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {!Object=} options
|
|
* @return {!Promise}
|
|
*/
|
|
waitForSelector(selector, options = {}) {
|
|
return this.mainFrame().waitForSelector(selector, options);
|
|
}
|
|
|
|
/**
|
|
* @param {string} xpath
|
|
* @param {!Object=} options
|
|
* @return {!Promise}
|
|
*/
|
|
waitForXPath(xpath, options = {}) {
|
|
return this.mainFrame().waitForXPath(xpath, options);
|
|
}
|
|
|
|
/**
|
|
* @param {function()} pageFunction
|
|
* @param {!Object=} options
|
|
* @param {!Array<*>} args
|
|
* @return {!Promise}
|
|
*/
|
|
waitForFunction(pageFunction, options = {}, ...args) {
|
|
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
|
}
|
|
}
|
|
|
|
/** @type {!Set<string>} */
|
|
const supportedMetrics = new Set([
|
|
'Timestamp',
|
|
'Documents',
|
|
'Frames',
|
|
'JSEventListeners',
|
|
'Nodes',
|
|
'LayoutCount',
|
|
'RecalcStyleCount',
|
|
'LayoutDuration',
|
|
'RecalcStyleDuration',
|
|
'ScriptDuration',
|
|
'TaskDuration',
|
|
'JSHeapUsedSize',
|
|
'JSHeapTotalSize',
|
|
]);
|
|
|
|
/** @enum {string} */
|
|
Page.PaperFormats = {
|
|
letter: {width: 8.5, height: 11},
|
|
legal: {width: 8.5, height: 14},
|
|
tabloid: {width: 11, height: 17},
|
|
ledger: {width: 17, height: 11},
|
|
a0: {width: 33.1, height: 46.8 },
|
|
a1: {width: 23.4, height: 33.1 },
|
|
a2: {width: 16.5, height: 23.4 },
|
|
a3: {width: 11.7, height: 16.5 },
|
|
a4: {width: 8.27, height: 11.7 },
|
|
a5: {width: 5.83, height: 8.27 },
|
|
a6: {width: 4.13, height: 5.83 },
|
|
};
|
|
|
|
const unitToPixels = {
|
|
'px': 1,
|
|
'in': 96,
|
|
'cm': 37.8,
|
|
'mm': 3.78
|
|
};
|
|
|
|
/**
|
|
* @param {(string|number|undefined)} parameter
|
|
* @return {(number|undefined)}
|
|
*/
|
|
function convertPrintParameterToInches(parameter) {
|
|
if (typeof parameter === 'undefined')
|
|
return undefined;
|
|
let pixels;
|
|
if (helper.isNumber(parameter)) {
|
|
// Treat numbers as pixel values to be aligned with phantom's paperSize.
|
|
pixels = /** @type {number} */ (parameter);
|
|
} else if (helper.isString(parameter)) {
|
|
const text = /** @type {string} */ (parameter);
|
|
let unit = text.substring(text.length - 2).toLowerCase();
|
|
let valueText = '';
|
|
if (unitToPixels.hasOwnProperty(unit)) {
|
|
valueText = text.substring(0, text.length - 2);
|
|
} else {
|
|
// In case of unknown unit try to parse the whole parameter as number of pixels.
|
|
// This is consistent with phantom's paperSize behavior.
|
|
unit = 'px';
|
|
valueText = text;
|
|
}
|
|
const value = Number(valueText);
|
|
assert(!isNaN(value), 'Failed to parse parameter value: ' + text);
|
|
pixels = value * unitToPixels[unit];
|
|
} else {
|
|
throw new Error('page.pdf() Cannot handle parameter type: ' + (typeof parameter));
|
|
}
|
|
return pixels / 96;
|
|
}
|
|
|
|
Page.Events = {
|
|
Close: 'close',
|
|
Console: 'console',
|
|
Dialog: 'dialog',
|
|
DOMContentLoaded: 'domcontentloaded',
|
|
Error: 'error',
|
|
// Can't use just 'error' due to node.js special treatment of error events.
|
|
// @see https://nodejs.org/api/events.html#events_error_events
|
|
PageError: 'pageerror',
|
|
Request: 'request',
|
|
Response: 'response',
|
|
RequestFailed: 'requestfailed',
|
|
RequestFinished: 'requestfinished',
|
|
FrameAttached: 'frameattached',
|
|
FrameDetached: 'framedetached',
|
|
FrameNavigated: 'framenavigated',
|
|
Load: 'load',
|
|
Metrics: 'metrics',
|
|
WorkerCreated: 'workercreated',
|
|
WorkerDestroyed: 'workerdestroyed',
|
|
};
|
|
|
|
/**
|
|
* @typedef {Object} Page.Viewport
|
|
* @property {number} width
|
|
* @property {number} height
|
|
* @property {number=} deviceScaleFactor
|
|
* @property {boolean=} isMobile
|
|
* @property {boolean=} isLandscape
|
|
* @property {boolean=} hasTouch
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} Network.Cookie
|
|
* @property {string} name
|
|
* @property {string} value
|
|
* @property {string} domain
|
|
* @property {string} path
|
|
* @property {number} expires
|
|
* @property {number} size
|
|
* @property {boolean} httpOnly
|
|
* @property {boolean} secure
|
|
* @property {boolean} session
|
|
* @property {("Strict"|"Lax")=} sameSite
|
|
*/
|
|
|
|
|
|
/**
|
|
* @typedef {Object} Network.CookieParam
|
|
* @property {string} name
|
|
* @property {string} value
|
|
* @property {string=} url
|
|
* @property {string=} domain
|
|
* @property {string=} path
|
|
* @property {number=} expires
|
|
* @property {boolean=} httpOnly
|
|
* @property {boolean=} secure
|
|
* @property {("Strict"|"Lax")=} sameSite
|
|
*/
|
|
|
|
class ConsoleMessage {
|
|
/**
|
|
* @param {string} type
|
|
* @param {string} text
|
|
* @param {!Array<!Puppeteer.JSHandle>} args
|
|
*/
|
|
constructor(type, text, args = []) {
|
|
this._type = type;
|
|
this._text = text;
|
|
this._args = args;
|
|
}
|
|
|
|
/**
|
|
* @return {string}
|
|
*/
|
|
type() {
|
|
return this._type;
|
|
}
|
|
|
|
/**
|
|
* @return {string}
|
|
*/
|
|
text() {
|
|
return this._text;
|
|
}
|
|
|
|
/**
|
|
* @return {!Array<!Puppeteer.JSHandle>}
|
|
*/
|
|
args() {
|
|
return this._args;
|
|
}
|
|
}
|
|
|
|
|
|
module.exports = Page;
|
|
helper.tracePublicAPI(Page);
|