puppeteer/lib/Page.js
Andrey Lushnikov 8a8076c15b Rename Page.Events.ConsoleMessageAdded -> Page.Events.ConsoleMessage
This patch:
- renames ConsoleMessageAdded into ConsoleMessage for the sake of
  clarity
- adds a test to cover basic functionality
2017-05-13 11:05:54 -07:00

427 lines
13 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.
*/
var fs = require('fs');
var EventEmitter = require('events');
var helpers = require('./helpers');
var mime = require('mime');
class Page extends EventEmitter {
/**
* @param {!Browser} browser
* @param {!CDP} client
* @return {!Promise<!Page>}
*/
static async create(browser, client) {
await Promise.all([
client.send('Network.enable', {}),
client.send('Page.enable', {}),
client.send('Runtime.enable', {}),
client.send('Security.enable', {}),
]);
var screenDPI = await helpers.evaluate(client, () => window.devicePixelRatio, []);
var page = new Page(browser, client, screenDPI.result.value);
// Initialize default page size.
await page.setSize({width: 400, height: 300});
return page;
}
/**
* @param {!Browser} browser
* @param {!CDP} client
* @param {number} screenDPI
*/
constructor(browser, client, screenDPI) {
super();
this._browser = browser;
this._client = client;
this._screenDPI = screenDPI;
this._extraHeaders = {};
/** @type {!Map<string, !InPageCallback>} */
this._sourceURLToPageCallback = new Map();
/** @type {!Map<string, !InPageCallback>} */
this._scriptIdToPageCallback = new Map();
client.on('Debugger.paused', event => this._onDebuggerPaused(event));
client.on('Debugger.scriptParsed', event => this._onScriptParsed(event));
client.on('Network.responseReceived', event => this.emit(Page.Events.ResponseReceived, event.response));
client.on('Network.loadingFailed', event => this.emit(Page.Events.ResourceLoadingFailed, event));
client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
client.on('Page.javascriptDialogOpening', dialog => this.emit(Page.Events.DialogOpened, dialog));
client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails));
}
/**
* @param {string} url
* @return {!Promise}
*/
async addScriptTag(url) {
return this.evaluateAsync(addScriptTag, url);
/**
* @param {string} url
*/
function addScriptTag(url) {
var script = document.createElement('script');
script.src = url;
var promise = new Promise(x => script.onload = x);
document.head.appendChild(script);
return promise;
}
}
/**
* @param {string} filePath
* @return {!Promise}
*/
async injectFile(filePath) {
var code = fs.readFileSync(filePath, 'utf8');
await helpers.evaluateText(this._client, code, false /* awaitPromise */);
}
/**
* @param {string} name
* @param {function(?)} callback
*/
async setInPageCallback(name, callback) {
var hasCallback = await this.evaluate(function(name) {
return !!window[name];
}, name);
if (hasCallback)
throw new Error(`Failed to set in-page callback with name ${name}: window['${name}'] already exists!`);
var sourceURL = '__in_page_callback__' + name;
this._sourceURLToPageCallback.set(sourceURL, new InPageCallback(name, callback));
var text = helpers.evaluationString(inPageCallback, [name], false /* awaitPromise */, sourceURL);
await Promise.all([
this._client.send('Debugger.enable', {}),
this._client.send('Page.addScriptToEvaluateOnLoad', { scriptSource: text }),
helpers.evaluateText(this._client, text, false /* awaitPromise */)
]);
function inPageCallback(callbackName) {
window[callbackName] = (...args) => {
window[callbackName].__args = args;
window[callbackName].__result = undefined;
debugger;
return window[callbackName].__result;
};
}
}
/**
* @param {!InPageCallback} inPageCallback
*/
async _handleInPageCallback(inPageCallback) {
var name = inPageCallback.name;
var callback = inPageCallback.callback;
var args = await this.evaluate(callbackName => window[callbackName].__args, name);
var result = callback.apply(null, args);
await this.evaluate(assignResult, name, result);
this._client.send('Debugger.resume');
/**
* @param {string} callbackName
* @param {string} callbackResult
*/
function assignResult(callbackName, callbackResult) {
window[callbackName].__result = callbackResult;
}
}
_onDebuggerPaused(event) {
var location = event.callFrames[0] ? event.callFrames[0].location : null;
var inPageCallback = location ? this._scriptIdToPageCallback.get(location.scriptId) : null;
if (inPageCallback) {
this._handleInPageCallback(inPageCallback);
return;
}
this._client.send('Debugger.resume');
}
_onScriptParsed(event) {
var inPageCallback = this._sourceURLToPageCallback.get(event.url);
if (inPageCallback)
this._scriptIdToPageCallback.set(event.scriptId, inPageCallback);
}
/**
* @param {!Object} headers
* @return {!Promise}
*/
async setExtraHTTPHeaders(headers) {
this._extraHeaders = {};
// Note: header names are case-insensitive.
for (var key of Object.keys(headers))
this._extraHeaders[key.toLowerCase()] = headers[key];
return this._client.send('Network.setExtraHTTPHeaders', { headers });
}
/**
* @return {!Object}
*/
extraHTTPHeaders() {
return Object.assign({}, this._extraHeaders);
}
/**
* @param {string} userAgent
* @return {!Promise}
*/
async setUserAgentOverride(userAgent) {
this._userAgent = userAgent;
return this._client.send('Network.setUserAgentOverride', { userAgent });
}
/**
* @return {string}
*/
userAgentOverride() {
return this._userAgent;
}
_handleException(exceptionDetails) {
var stack = [];
if (exceptionDetails.stackTrace) {
stack = exceptionDetails.stackTrace.callFrames.map(cf => cf.url);
}
var stackTrace = exceptionDetails.stackTrace;
this.emit(Page.Events.ExceptionThrown, exceptionDetails.exception.description, stack);
}
_onConsoleAPI(event) {
var values = event.args.map(arg => arg.value || arg.description || '');
this.emit(Page.Events.ConsoleMessage, values.join(' '));
}
/**
* @param {boolean} accept
* @param {string} promptText
* @return {!Promise}
*/
async handleDialog(accept, promptText) {
return this._client.send('Page.handleJavaScriptDialog', {accept, promptText});
}
/**
* @return {!Promise<string>}
*/
async url() {
return this.evaluate(function() {
return window.location.href;
});
}
/**
* @param {string} html
* @return {!Promise}
*/
async setContent(html) {
var resourceTree = await this._client.send('Page.getResourceTree', {});
await this._client.send('Page.setDocumentContent', {
frameId: resourceTree.frameTree.frame.id,
html: html
});
}
/**
* @param {string} html
* @return {!Promise<boolean>}
*/
async navigate(url) {
var loadPromise = new Promise(fulfill => this._client.once('Page.loadEventFired', fulfill)).then(() => true);
var interstitialPromise = new Promise(fulfill => this._client.once('Security.certificateError', fulfill)).then(() => false);
var referrer = this._extraHeaders.referer;
// Await for the command to throw exception in case of illegal arguments.
await this._client.send('Page.navigate', {url, referrer});
return await Promise.race([loadPromise, interstitialPromise]);
}
/**
* @param {!{width: number, height: number}} size
* @return {!Promise}
*/
async setSize(size) {
this._size = size;
var width = size.width;
var height = size.height;
var zoom = this._screenDPI;
return Promise.all([
this._client.send('Emulation.setDeviceMetricsOverride', {
width,
height,
deviceScaleFactor: 1,
scale: 1 / zoom,
mobile: false,
fitWindow: false
}),
this._client.send('Emulation.setVisibleSize', {
width: width / zoom,
height: height / zoom,
})
]);
}
/**
* @return {!{width: number, height: number}}
*/
size() {
return this._size;
}
/**
* @param {function()} fun
* @param {!Array<*>} args
* @return {!Promise<(!Object|udndefined)>}
*/
async evaluate(fun, ...args) {
var response = await helpers.evaluate(this._client, fun, args, false /* awaitPromise */);
if (response.exceptionDetails) {
this._handleException(response.exceptionDetails);
return;
}
return response.result.value;
}
/**
* @param {function()} fun
* @param {!Array<*>} args
* @return {!Promise<(!Object|udndefined)>}
*/
async evaluateAsync(fun, ...args) {
var response = await helpers.evaluate(this._client, fun, args, true /* awaitPromise */);
if (response.exceptionDetails) {
this._handleException(response.exceptionDetails);
return;
}
return response.result.value;
}
/**
* @param {function()} fun
* @param {!Array<*>} args
* @return {!Promise}
*/
async evaluateOnInitialized(fun, ...args) {
var code = helpers.evaluationString(fun, args, false);
await this._client.send('Page.addScriptToEvaluateOnLoad', {
scriptSource: code
});
}
/**
* @param {!Page.ScreenshotType} screenshotType
* @param {?{x: number, y: number, width: number, height: number}} clipRect
* @return {!Promise<!Buffer>}
*/
async screenshot(screenshotType, clipRect) {
if (clipRect) {
await Promise.all([
this._client.send('Emulation.setVisibleSize', {
width: clipRect.width / this._screenDPI,
height: clipRect.height / this._screenDPI,
}),
this._client.send('Emulation.forceViewport', {
x: clipRect.x / this._screenDPI,
y: clipRect.y / this._screenDPI,
scale: 1,
})
]);
}
var result = await this._client.send('Page.captureScreenshot', {
fromSurface: true,
format: screenshotType,
});
if (clipRect) {
await Promise.all([
this.setSize(this.size()),
this._client.send('Emulation.resetViewport')
]);
}
return new Buffer(result.data, 'base64');
}
/**
* @param {string} filePath
* @param {?{x: number, y: number, width: number, height: number}} clipRect
* @return {!Promise}
*/
async saveScreenshot(filePath, clipRect) {
var mimeType = mime.lookup(filePath);
var screenshotType = null;
if (mimeType === 'image/png')
screenshotType = Page.ScreenshotTypes.PNG;
else if (mimeType === 'image/jpeg')
screenshotType = Page.ScreenshotTypes.JPG;
if (!screenshotType)
throw new Error(`Cannot render to file ${fileName} - unsupported mimeType ${mimeType}`);
var buffer = await this.screenshot(screenshotType, clipRect);
fs.writeFileSync(filePath, buffer);
}
/**
* @return {!Promise<string>}
*/
async plainText() {
return this.evaluate(function() {
return document.body.innerText;
});
}
/**
* @return {!Promise<string>}
*/
async title() {
return this.evaluate(function() {
return document.title;
});
}
/**
* @return {!Promise}
*/
async close() {
return this._browser.closePage(this);
}
}
class InPageCallback {
/**
* @param {string} name
* @param {function(?):?} callback
*/
constructor(name, callback) {
this.name = name;
this.callback = callback;
}
}
/** @enum {string} */
Page.ScreenshotTypes = {
PNG: "png",
JPG: "jpeg",
};
Page.Events = {
ConsoleMessage: 'Page.Events.ConsoleMessage',
DialogOpened: 'Page.Events.DialogOpened',
ExceptionThrown: 'Page.Events.ExceptionThrown',
ResourceLoadingFailed: 'Page.Events.ResourceLoadingFailed',
ResponseReceived: 'Page.Events.ResponseReceived',
};
module.exports = Page;