puppeteer/lib/Page.js

1075 lines
32 KiB
JavaScript
Raw Normal View History

2017-05-11 07:06:41 +00:00
/**
* 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.
*/
2017-08-02 19:06:47 +00:00
const fs = require('fs');
const EventEmitter = require('events');
const mime = require('mime');
const {NetworkManager} = require('./NetworkManager');
2017-08-02 19:06:47 +00:00
const NavigatorWatcher = require('./NavigatorWatcher');
const Dialog = require('./Dialog');
const EmulationManager = require('./EmulationManager');
const {FrameManager} = require('./FrameManager');
const {Keyboard, Mouse, Touchscreen} = require('./Input');
2017-08-02 19:06:47 +00:00
const Tracing = require('./Tracing');
const {helper, debugError} = require('./helper');
const {Coverage} = require('./Coverage');
2017-05-11 07:06:41 +00:00
const writeFileAsync = helper.promisify(fs.writeFile);
2017-05-11 07:06:41 +00:00
class Page extends EventEmitter {
2017-06-21 20:51:06 +00:00
/**
* @param {!Puppeteer.CDPSession} client
* @param {!Puppeteer.Target} target
* @param {boolean} ignoreHTTPSErrors
* @param {boolean} setDefaultViewport
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
2017-06-21 20:58:49 +00:00
* @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);
2017-06-21 20:51:06 +00:00
await Promise.all([
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
2017-06-21 20:51:06 +00:00
client.send('Network.enable', {}),
client.send('Runtime.enable', {}),
client.send('Security.enable', {}),
client.send('Performance.enable', {}),
2017-06-21 20:51:06 +00:00
]);
if (ignoreHTTPSErrors)
await client.send('Security.setOverrideCertificateErrors', {override: true});
2017-06-21 20:51:06 +00:00
// Initialize default page size.
if (setDefaultViewport)
await page.setViewport({width: 800, height: 600});
2017-06-21 20:51:06 +00:00
return page;
}
/**
* @param {!Puppeteer.CDPSession} client
* @param {!Puppeteer.Target} target
* @param {!Protocol.Page.FrameTree} frameTree
* @param {boolean} ignoreHTTPSErrors
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
2017-06-21 20:58:49 +00:00
*/
constructor(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue) {
2017-06-21 20:51:06 +00:00
super();
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);
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;
2017-06-21 20:51:06 +00:00
this._screenshotTaskQueue = screenshotTaskQueue;
2017-06-21 20:51:06 +00:00
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));
2017-06-21 20:51:06 +00:00
client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(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));
this._target._isClosedPromise.then(() => this.emit(Page.Events.Close));
}
/**
* @return {!Puppeteer.Target}
*/
target() {
return this._target;
}
_onTargetCrashed() {
this.emit('error', new Error('Page crashed!'));
2017-06-21 20:51:06 +00:00
}
/**
* @return {!Puppeteer.Frame}
2017-06-21 20:58:49 +00:00
*/
2017-06-21 20:51:06 +00:00
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;
}
2017-06-21 20:51:06 +00:00
/**
* @return {!Array<Puppeteer.Frame>}
2017-06-21 20:58:49 +00:00
*/
2017-06-21 20:51:06 +00:00
frames() {
return this._frameManager.frames();
}
2017-06-21 20:51:06 +00:00
/**
* @param {boolean} value
2017-06-21 20:58:49 +00:00
*/
async setRequestInterception(value) {
return this._networkManager.setRequestInterception(value);
2017-06-21 20:51:06 +00:00
}
/**
* @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;
console.assert(
item.url !== 'about:blank',
`Blank page can not have cookie "${item.name}"`
);
console.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 });
}
2017-06-21 20:51:06 +00:00
/**
* @param {Object} options
* @return {!Promise<!Puppeteer.ElementHandle>}
2017-06-21 20:58:49 +00:00
*/
async addScriptTag(options) {
return this.mainFrame().addScriptTag(options);
2017-06-21 20:51:06 +00:00
}
/**
* @param {Object} options
* @return {!Promise<!Puppeteer.ElementHandle>}
*/
async addStyleTag(options) {
return this.mainFrame().addStyleTag(options);
2017-06-21 20:51:06 +00:00
}
/**
2017-06-21 20:58:49 +00:00
* @param {string} name
* @param {function(?)} puppeteerFunction
2017-06-21 20:58:49 +00:00
*/
2017-08-16 06:04:16 +00:00
async exposeFunction(name, puppeteerFunction) {
if (this._pageBindings[name])
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
this._pageBindings[name] = puppeteerFunction;
2017-06-21 20:51:06 +00:00
const expression = helper.evaluationString(addPageBinding, name);
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: expression});
await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError)));
2017-06-21 20:51:06 +00:00
function addPageBinding(bindingName) {
window[bindingName] = async(...args) => {
const me = window[bindingName];
2017-06-21 20:51:06 +00:00
let callbacks = me['callbacks'];
if (!callbacks) {
callbacks = new Map();
me['callbacks'] = callbacks;
}
2017-06-21 20:51:06 +00:00
const seq = (me['lastSeq'] || 0) + 1;
me['lastSeq'] = seq;
const promise = new Promise(fulfill => callbacks.set(seq, fulfill));
// eslint-disable-next-line no-console
console.debug('driver:page-binding', JSON.stringify({name: bindingName, seq, args}));
2017-06-21 20:51:06 +00:00
return promise;
};
2017-05-11 07:06:41 +00:00
}
2017-06-21 20:51:06 +00:00
}
/**
* @param {?{username: string, password: string}} credentials
*/
async authenticate(credentials) {
return this._networkManager.authenticate(credentials);
}
2017-06-21 20:51:06 +00:00
/**
* @param {!Object<string, string>} headers
2017-06-21 20:58:49 +00:00
*/
async setExtraHTTPHeaders(headers) {
return this._networkManager.setExtraHTTPHeaders(headers);
2017-06-21 20:51:06 +00:00
}
/**
2017-06-21 20:58:49 +00:00
* @param {string} userAgent
*/
async setUserAgent(userAgent) {
return this._networkManager.setUserAgent(userAgent);
2017-06-21 20:51:06 +00:00
}
2017-05-11 07:06:41 +00:00
/**
* @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;
}
2017-06-21 20:51:06 +00:00
/**
* @param {!Protocol.Runtime.ExceptionDetails} exceptionDetails
2017-06-21 20:58:49 +00:00
*/
_handleException(exceptionDetails) {
const message = helper.getExceptionMessage(exceptionDetails);
this.emit(Page.Events.PageError, new Error(message));
2017-06-21 20:51:06 +00:00
}
async _onConsoleAPI(event) {
if (event.type === 'debug' && event.args.length && event.args[0].value === 'driver:page-binding') {
const {name, seq, args} = JSON.parse(event.args[1].value);
const result = await this._pageBindings[name](...args);
const expression = helper.evaluationString(deliverResult, name, seq, result);
this._client.send('Runtime.evaluate', { expression, contextId: event.executionContextId }).catch(debugError);
2017-06-21 20:51:06 +00:00
function deliverResult(name, seq, result) {
window[name]['callbacks'].get(seq)(result);
window[name]['callbacks'].delete(seq);
}
return;
}
if (!this.listenerCount(Page.Events.Console)) {
event.args.map(arg => helper.releaseObject(this._client, arg));
return;
}
const values = event.args.map(arg => this._frameManager.createJSHandle(event.executionContextId, arg));
const textTokens = [];
for (let i = 0; i < event.args.length; ++i) {
const remoteObject = event.args[i];
if (remoteObject.objectId)
textTokens.push(values[i].toString());
else
textTokens.push(helper.valueFromRemoteObject(remoteObject));
}
const message = new ConsoleMessage(event.type, textTokens.join(' '), values);
this.emit(Page.Events.Console, message);
2017-06-21 20:51:06 +00:00
}
_onDialog(event) {
let dialogType = null;
2017-06-21 20:51:06 +00:00
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;
console.assert(dialogType, 'Unknown javascript dialog type: ' + event.type);
const dialog = new Dialog(this._client, dialogType, event.message, event.defaultPrompt);
2017-06-21 20:51:06 +00:00
this.emit(Page.Events.Dialog, dialog);
}
/**
* @return {!string}
2017-06-21 20:58:49 +00:00
*/
url() {
return this.mainFrame().url();
2017-06-21 20:51:06 +00:00
}
/**
* @return {!Promise<String>}
*/
async content() {
return await this._frameManager.mainFrame().content();
}
2017-06-21 20:51:06 +00:00
/**
2017-06-21 20:58:49 +00:00
* @param {string} html
*/
async setContent(html) {
await this._frameManager.mainFrame().setContent(html);
2017-06-21 20:51:06 +00:00
}
/**
* @param {string} url
2017-06-21 20:58:49 +00:00
* @param {!Object=} options
* @return {!Promise<?Puppeteer.Response>}
2017-06-21 20:58:49 +00:00
*/
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(this.mainFrame().url());
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;
2017-06-21 20:51:06 +00:00
}
/**
* @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) {
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) {
console.assert(mediaType === 'screen' || mediaType === 'print' || mediaType === null, 'Unsupported media type: ' + mediaType);
await this._client.send('Emulation.setEmulatedMedia', {media: mediaType || ''});
}
2017-06-21 20:51:06 +00:00
/**
* @param {!Page.Viewport} viewport
2017-06-21 20:58:49 +00:00
*/
async setViewport(viewport) {
const needsReload = await this._emulationManager.emulateViewport(viewport);
this._viewport = viewport;
if (needsReload)
await this.reload();
2017-06-21 20:51:06 +00:00
}
/**
* @return {!Page.Viewport}
2017-06-21 20:58:49 +00:00
*/
viewport() {
return this._viewport;
2017-06-21 20:51:06 +00:00
}
2017-06-21 20:51:06 +00:00
/**
* @param {function()|string} pageFunction
2017-06-21 20:58:49 +00:00
* @param {!Array<*>} args
* @return {!Promise<*>}
2017-06-21 20:58:49 +00:00
*/
async evaluate(pageFunction, ...args) {
return this._frameManager.mainFrame().evaluate(pageFunction, ...args);
2017-06-21 20:51:06 +00:00
}
2017-06-21 20:51:06 +00:00
/**
* @param {function()|string} pageFunction
2017-06-21 20:58:49 +00:00
* @param {!Array<*>} args
*/
async evaluateOnNewDocument(pageFunction, ...args) {
const source = helper.evaluationString(pageFunction, ...args);
await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source });
2017-06-21 20:51:06 +00:00
}
2017-06-20 01:03:01 +00:00
/**
* @param {Boolean} enabled
* @returns {!Promise}
*/
async setCacheEnabled(enabled = true) {
await this._client.send('Network.setCacheDisabled', {cacheDisabled: !enabled});
}
2017-06-21 20:51:06 +00:00
/**
2017-06-21 20:58:49 +00:00
* @param {!Object=} options
* @return {!Promise<!Buffer>}
*/
2017-08-21 23:32:39 +00:00
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) {
console.assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
screenshotType = options.type;
} else if (options.path) {
const mimeType = mime.lookup(options.path);
2017-06-21 20:51:06 +00:00
if (mimeType === 'image/png')
screenshotType = 'png';
else if (mimeType === 'image/jpeg')
screenshotType = 'jpeg';
console.assert(screenshotType, 'Unsupported screenshot mime type: ' + mimeType);
}
2017-06-21 20:51:06 +00:00
if (!screenshotType)
screenshotType = 'png';
if (options.quality) {
console.assert(screenshotType === 'jpeg', 'options.quality is unsupported for the ' + screenshotType + ' screenshots');
console.assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + (typeof options.quality));
console.assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer');
console.assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality);
}
console.assert(!options.clip || !options.fullPage, 'options.clip and options.fullPage are exclusive');
if (options.clip) {
console.assert(typeof options.clip.x === 'number', 'Expected options.clip.x to be a number but found ' + (typeof options.clip.x));
console.assert(typeof options.clip.y === 'number', 'Expected options.clip.y to be a number but found ' + (typeof options.clip.y));
console.assert(typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' + (typeof options.clip.width));
console.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));
2017-06-21 20:51:06 +00:00
}
/**
* @param {"png"|"jpeg"} format
2017-06-21 20:58:49 +00:00
* @param {!Object=} options
* @return {!Promise<!Buffer>}
*/
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 });
2017-06-21 20:51:06 +00:00
}
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 = new Buffer(result.data, 'base64');
2017-06-21 20:51:06 +00:00
if (options.path)
await writeFileAsync(options.path, buffer);
2017-06-21 20:51:06 +00:00
return buffer;
}
/**
2017-06-21 20:58:49 +00:00
* @param {!Object=} options
* @return {!Promise<!Buffer>}
2017-06-21 20:58:49 +00:00
*/
2017-08-21 23:32:39 +00:00
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 || '';
2017-06-21 20:51:06 +00:00
let paperWidth = 8.5;
let paperHeight = 11;
2017-06-21 20:51:06 +00:00
if (options.format) {
const format = Page.PaperFormats[options.format.toLowerCase()];
2017-06-21 20:51:06 +00:00
console.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;
2017-06-21 20:51:06 +00:00
const result = await this._client.send('Page.printToPDF', {
2017-06-21 20:51:06 +00:00
landscape: landscape,
displayHeaderFooter: displayHeaderFooter,
headerTemplate: headerTemplate,
footerTemplate: footerTemplate,
2017-06-21 20:51:06 +00:00
printBackground: printBackground,
scale: scale,
paperWidth: paperWidth,
paperHeight: paperHeight,
marginTop: marginTop,
marginBottom: marginBottom,
marginLeft: marginLeft,
marginRight: marginRight,
pageRanges: pageRanges
});
const buffer = new Buffer(result.data, 'base64');
if (options.path)
await writeFileAsync(options.path, buffer);
return buffer;
2017-06-21 20:51:06 +00:00
}
/**
2017-06-21 20:58:49 +00:00
* @return {!Promise<string>}
*/
2017-06-21 20:51:06 +00:00
async title() {
return this.mainFrame().title();
2017-06-21 20:51:06 +00:00
}
2017-05-11 07:06:41 +00:00
2017-06-21 20:51:06 +00:00
async close() {
console.assert(!!this._client._connection, 'Protocol error: Connection closed. Most likely the page has been closed.');
await this._client._connection.send('Target.closeTarget', { targetId: this._target._targetId });
await this._target._isClosedPromise;
2017-06-21 20:51:06 +00:00
}
/**
* @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);
}
2017-05-11 07:06:41 +00:00
}
/** @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 = {
2017-06-21 20:51:06 +00:00
'px': 1,
'in': 96,
'cm': 37.8,
'mm': 3.78
};
/**
* @param {(string|number|undefined)} parameter
* @return {(number|undefined)}
*/
function convertPrintParameterToInches(parameter) {
2017-06-21 20:51:06 +00:00
if (typeof parameter === 'undefined')
return undefined;
let pixels;
if (helper.isNumber(parameter)) {
2017-06-21 20:51:06 +00:00
// 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 = '';
2017-06-21 20:51:06 +00:00
if (unitToPixels.hasOwnProperty(unit)) {
valueText = text.substring(0, text.length - 2);
} else {
2017-06-21 20:51:06 +00:00
// 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);
2017-06-21 20:51:06 +00:00
console.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));
2017-06-21 20:51:06 +00:00
}
return pixels / 96;
}
2017-05-11 07:06:41 +00:00
Page.Events = {
Close: 'close',
Console: 'console',
2017-06-21 20:51:06 +00:00
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',
2017-06-21 20:51:06 +00:00
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
FrameNavigated: 'framenavigated',
Load: 'load',
Metrics: 'metrics',
2017-05-11 07:06:41 +00:00
};
/**
* @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<*>} 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<string>}
*/
args() {
return this._args;
}
}
2017-05-11 07:06:41 +00:00
module.exports = Page;
helper.tracePublicAPI(Page);