80ee469429
This patch adds "options" parameter to the `page.setContent` method. The parameter is the same as a navigation parameter and allows to specify maximum timeout to wait for resources to be loaded, as well as to describe events that should be emitted before the setContent operation would be considered successful. Fixes #728.
985 lines
30 KiB
JavaScript
985 lines
30 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} = require('./helper');
|
|
|
|
const writeFileAsync = helper.promisify(fs.writeFile);
|
|
|
|
class Page extends EventEmitter {
|
|
/**
|
|
* @param {!Puppeteer.Session} client
|
|
* @param {boolean} ignoreHTTPSErrors
|
|
* @param {boolean} appMode
|
|
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
|
|
* @return {!Promise<!Page>}
|
|
*/
|
|
static async create(client, ignoreHTTPSErrors, appMode, screenshotTaskQueue) {
|
|
|
|
await client.send('Page.enable');
|
|
const {frameTree} = await client.send('Page.getResourceTree');
|
|
const page = new Page(client, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);
|
|
|
|
await Promise.all([
|
|
client.send('Network.enable', {}),
|
|
client.send('Runtime.enable', {}),
|
|
client.send('Security.enable', {}),
|
|
client.send('Performance.enable', {})
|
|
]);
|
|
if (ignoreHTTPSErrors)
|
|
await client.send('Security.setOverrideCertificateErrors', {override: true});
|
|
// Initialize default page size.
|
|
if (!appMode)
|
|
await page.setViewport({width: 800, height: 600});
|
|
|
|
return page;
|
|
}
|
|
|
|
/**
|
|
* @param {!Puppeteer.Session} client
|
|
* @param {{frame: Object, childFrames: ?Array}} frameTree
|
|
* @param {boolean} ignoreHTTPSErrors
|
|
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
|
|
*/
|
|
constructor(client, frameTree, ignoreHTTPSErrors, screenshotTaskQueue) {
|
|
super();
|
|
this._client = client;
|
|
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._emulationManager = new EmulationManager(client);
|
|
this._tracing = new Tracing(client);
|
|
/** @type {!Map<string, Function>} */
|
|
this._pageBindings = new Map();
|
|
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
|
|
|
this._screenshotTaskQueue = screenshotTaskQueue;
|
|
|
|
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.loadEventFired', event => this.emit(Page.Events.Load));
|
|
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));
|
|
}
|
|
|
|
_onTargetCrashed() {
|
|
this.emit('error', new Error('Page crashed!'));
|
|
}
|
|
|
|
/**
|
|
* @return {!Puppeteer.Frame}
|
|
*/
|
|
mainFrame() {
|
|
return this._frameManager.mainFrame();
|
|
}
|
|
|
|
/**
|
|
* @return {!Keyboard}
|
|
*/
|
|
get keyboard() {
|
|
return this._keyboard;
|
|
}
|
|
|
|
/**
|
|
* @return {!Touchscreen}
|
|
*/
|
|
get touchscreen() {
|
|
return this._touchscreen;
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
*/
|
|
async tap(selector) {
|
|
const handle = await this.$(selector);
|
|
console.assert(handle, 'No node found for selector: ' + selector);
|
|
await handle.tap();
|
|
await handle.dispose();
|
|
}
|
|
|
|
/**
|
|
* @return {!Tracing}
|
|
*/
|
|
get tracing() {
|
|
return this._tracing;
|
|
}
|
|
|
|
/**
|
|
* @return {!Array<Puppeteer.Frame>}
|
|
*/
|
|
frames() {
|
|
return this._frameManager.frames();
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} value
|
|
*/
|
|
async setRequestInterceptionEnabled(value) {
|
|
return this._networkManager.setRequestInterceptionEnabled(value);
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} enabled
|
|
*/
|
|
setOfflineMode(enabled) {
|
|
return this._networkManager.setOfflineMode(enabled);
|
|
}
|
|
|
|
/**
|
|
* @param {!Object} 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) {
|
|
return this.mainFrame().executionContext().evaluateHandle(pageFunction, ...args);
|
|
}
|
|
|
|
/**
|
|
* @param {!Puppeteer.JSHandle} prototypeHandle
|
|
* @return {!Promise<!Puppeteer.JSHandle>}
|
|
*/
|
|
async queryObjects(prototypeHandle) {
|
|
return this.mainFrame().executionContext().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 {!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 items = cookies.map(cookie => {
|
|
const item = Object.assign({}, cookie);
|
|
const pageURL = this.url();
|
|
if (!item.url && pageURL.startsWith('http'))
|
|
item.url = this.url();
|
|
return item;
|
|
});
|
|
await this.deleteCookie(...items);
|
|
if (items.length)
|
|
await this._client.send('Network.setCookies', { cookies: items });
|
|
}
|
|
|
|
/**
|
|
* @param {Object} options
|
|
*/
|
|
async addScriptTag(options) {
|
|
return this.mainFrame().addScriptTag(options);
|
|
}
|
|
|
|
/**
|
|
* @param {Object} options
|
|
*/
|
|
async addStyleTag(options) {
|
|
return this.mainFrame().addStyleTag(options);
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {function(?)} puppeteerFunction
|
|
*/
|
|
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;
|
|
|
|
const expression = helper.evaluationString(addPageBinding, name);
|
|
await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: expression });
|
|
await this._client.send('Runtime.evaluate', { expression, returnByValue: true });
|
|
|
|
function addPageBinding(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));
|
|
// eslint-disable-next-line no-console
|
|
console.debug('driver:page-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 getMetrics() {
|
|
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<!{name: string, value: number}>} 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 {!Object} exceptionDetails
|
|
*/
|
|
_handleException(exceptionDetails) {
|
|
const message = helper.getExceptionMessage(exceptionDetails);
|
|
this.emit(Page.Events.PageError, new Error(message));
|
|
}
|
|
|
|
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 }).catch(debugError);
|
|
|
|
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);
|
|
}
|
|
|
|
_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;
|
|
console.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.evaluate(() => {
|
|
let retVal = '';
|
|
if (document.doctype)
|
|
retVal = new XMLSerializer().serializeToString(document.doctype);
|
|
if (document.documentElement)
|
|
retVal += document.documentElement.outerHTML;
|
|
return retVal;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} html
|
|
* @param {!Object=} options
|
|
*/
|
|
async setContent(html, options) {
|
|
await Promise.all([
|
|
this.evaluate(html => {
|
|
document.open();
|
|
document.write(html);
|
|
document.close();
|
|
}, html),
|
|
this.waitForNavigation(options),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param {string} url
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Response>}
|
|
*/
|
|
async goto(url, options) {
|
|
const mainFrame = this._frameManager.mainFrame();
|
|
const watcher = new NavigatorWatcher(this._client, mainFrame._id, this._ignoreHTTPSErrors, options);
|
|
const responses = new Map();
|
|
const listener = helper.addEventListener(this._networkManager, NetworkManager.Events.Response, response => responses.set(response.url, response));
|
|
const navigationPromise = watcher.waitForNavigation();
|
|
|
|
const referrer = this._networkManager.extraHTTPHeaders()['referer'];
|
|
try {
|
|
// Await for the command to throw exception in case of illegal arguments.
|
|
await this._client.send('Page.navigate', {url, referrer});
|
|
} catch (e) {
|
|
watcher.cancel();
|
|
helper.removeEventListeners([listener]);
|
|
throw e;
|
|
}
|
|
const error = await navigationPromise;
|
|
helper.removeEventListeners([listener]);
|
|
if (error)
|
|
throw error;
|
|
if (this._frameManager.isMainFrameLoadingFailed())
|
|
throw new Error('Failed to navigate: ' + url);
|
|
return responses.get(this.mainFrame().url()) || null;
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Response>}
|
|
*/
|
|
async reload(options) {
|
|
const [response] = await Promise.all([
|
|
this.waitForNavigation(options),
|
|
this._client.send('Page.reload')
|
|
]);
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<!Response>}
|
|
*/
|
|
async waitForNavigation(options) {
|
|
const mainFrame = this._frameManager.mainFrame();
|
|
const watcher = new NavigatorWatcher(this._client, mainFrame._id, this._ignoreHTTPSErrors, options);
|
|
|
|
const responses = new Map();
|
|
const listener = helper.addEventListener(this._networkManager, NetworkManager.Events.Response, response => responses.set(response.url, response));
|
|
const error = await watcher.waitForNavigation();
|
|
helper.removeEventListeners([listener]);
|
|
if (error)
|
|
throw error;
|
|
return responses.get(this.mainFrame().url()) || null;
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Response>}
|
|
*/
|
|
async goBack(options) {
|
|
return this._go(-1, options);
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?Response>}
|
|
*/
|
|
async goForward(options) {
|
|
return this._go(+1, options);
|
|
}
|
|
|
|
/**
|
|
* @param {!Object=} options
|
|
* @return {!Promise<?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;
|
|
}
|
|
|
|
/**
|
|
* @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 {?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 || ''});
|
|
}
|
|
|
|
/**
|
|
* @param {!Page.Viewport} viewport
|
|
*/
|
|
async setViewport(viewport) {
|
|
const needsReload = await this._emulationManager.emulateViewport(this._client, viewport);
|
|
this._viewport = viewport;
|
|
if (needsReload)
|
|
await this.reload();
|
|
}
|
|
|
|
/**
|
|
* @return {!Page.Viewport}
|
|
*/
|
|
viewport() {
|
|
return this._viewport;
|
|
}
|
|
|
|
/**
|
|
* @param {function()} 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 {!Object=} options
|
|
* @return {!Promise<!Buffer>}
|
|
*/
|
|
async screenshot(options = {}) {
|
|
let screenshotType = null;
|
|
if (options.path) {
|
|
const mimeType = mime.lookup(options.path);
|
|
if (mimeType === 'image/png')
|
|
screenshotType = 'png';
|
|
else if (mimeType === 'image/jpeg')
|
|
screenshotType = 'jpeg';
|
|
console.assert(screenshotType, 'Unsupported screenshot mime type: ' + mimeType);
|
|
}
|
|
if (options.type) {
|
|
console.assert(!screenshotType || options.type === screenshotType, `Passed screenshot type '${options.type}' does not match the type inferred from the file path: '${screenshotType}'`);
|
|
console.assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
|
|
screenshotType = options.type;
|
|
}
|
|
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));
|
|
}
|
|
|
|
/**
|
|
* @param {string} format
|
|
* @param {!Object=} options
|
|
* @return {!Promise<!Buffer>}
|
|
*/
|
|
async _screenshotTask(format, options) {
|
|
await this._client.send('Target.activateTarget', {targetId: this._client.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;
|
|
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 = new Buffer(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 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()];
|
|
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;
|
|
|
|
const result = await this._client.send('Page.printToPDF', {
|
|
landscape: landscape,
|
|
displayHeaderFooter: displayHeaderFooter,
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise<string>}
|
|
*/
|
|
async title() {
|
|
return this.mainFrame().title();
|
|
}
|
|
|
|
async close() {
|
|
await this._client.dispose();
|
|
}
|
|
|
|
/**
|
|
* @return {!Mouse}
|
|
*/
|
|
get mouse() {
|
|
return this._mouse;
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {!Object} options
|
|
*/
|
|
async click(selector, options) {
|
|
const handle = await this.$(selector);
|
|
console.assert(handle, 'No node found for selector: ' + selector);
|
|
await handle.click(options);
|
|
await handle.dispose();
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
*/
|
|
async hover(selector) {
|
|
const handle = await this.$(selector);
|
|
console.assert(handle, 'No node found for selector: ' + selector);
|
|
await handle.hover();
|
|
await handle.dispose();
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
*/
|
|
async focus(selector) {
|
|
const handle = await this.$(selector);
|
|
console.assert(handle, 'No node found for selector: ' + selector);
|
|
await handle.focus();
|
|
await handle.dispose();
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {!Array<string>} values
|
|
*/
|
|
async select(selector, ...values) {
|
|
await this.$eval(selector, (element, values) => {
|
|
if (element.nodeName.toLowerCase() !== 'select')
|
|
throw new Error('Element is not a <select> element.');
|
|
|
|
const options = Array.from(element.options);
|
|
|
|
if (element.multiple) {
|
|
for (const option of options)
|
|
option.selected = values.includes(option.value);
|
|
} else {
|
|
element.value = values.shift();
|
|
}
|
|
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
|
element.dispatchEvent(new Event('change', { 'bubbles': true }));
|
|
}, values);
|
|
}
|
|
|
|
/**
|
|
* @param {string} selector
|
|
* @param {string} text
|
|
* @param {{delay: (number|undefined)}=} options
|
|
*/
|
|
async type(selector, text, options) {
|
|
const handle = await this.$(selector);
|
|
await handle.type(text, options);
|
|
await handle.dispose();
|
|
}
|
|
|
|
/**
|
|
* @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 {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);
|
|
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));
|
|
}
|
|
return pixels / 96;
|
|
}
|
|
|
|
Page.Events = {
|
|
Console: 'console',
|
|
Dialog: 'dialog',
|
|
Error: 'error',
|
|
// Can'e 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',
|
|
};
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
}
|
|
|
|
|
|
module.exports = Page;
|
|
helper.tracePublicAPI(Page);
|