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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
var await = require('./utilities').await;
|
|
|
|
var EventEmitter = require('events');
|
|
|
|
var fs = require('fs');
|
|
|
|
var path = require('path');
|
|
|
|
var mime = require('mime');
|
|
|
|
|
|
|
|
var PageEvents = require('../lib/Page').Events;
|
|
|
|
var ScreenshotTypes = require('../lib/Page').ScreenshotTypes;
|
|
|
|
|
|
|
|
var noop = function() { };
|
|
|
|
|
|
|
|
class WebPage {
|
|
|
|
/**
|
|
|
|
* @param {!Browser} browser
|
|
|
|
* @param {string} scriptPath
|
|
|
|
* @param {!Object=} options
|
|
|
|
*/
|
|
|
|
constructor(browser, scriptPath, options) {
|
|
|
|
this._page = await(browser.newPage());
|
|
|
|
this.settings = new WebPageSettings(this._page);
|
|
|
|
|
|
|
|
options = options || {};
|
|
|
|
options.settings = options.settings || {};
|
|
|
|
if (options.settings.userAgent)
|
|
|
|
this.settings.userAgent = options.settings.userAgent;
|
|
|
|
if (options.viewportSize)
|
|
|
|
await(this._page.setSize(options.viewportSize));
|
|
|
|
|
|
|
|
await(this._page.setInPageCallback('callPhantom', (...args) => {
|
|
|
|
return this.onCallback.apply(null, args);
|
|
|
|
}));
|
|
|
|
|
|
|
|
this.clipRect = options.clipRect || {left: 0, top: 0, width: 0, height: 0};
|
|
|
|
this.onCallback = null;
|
|
|
|
this.onConsoleMessage = null;
|
|
|
|
this.onLoadFinished = null;
|
|
|
|
this.onResourceError = null;
|
|
|
|
this.onResourceReceived = null;
|
2017-05-12 23:25:04 +00:00
|
|
|
this.onInitialized = null;
|
|
|
|
this._deferEvaluate = false;
|
|
|
|
|
2017-05-11 07:06:41 +00:00
|
|
|
this.libraryPath = path.dirname(scriptPath);
|
|
|
|
|
2017-05-13 20:44:24 +00:00
|
|
|
this._onConfirmCallback = undefined;
|
|
|
|
this._onAlertCallback = undefined;
|
2017-05-11 07:06:41 +00:00
|
|
|
this._onError = noop;
|
|
|
|
|
|
|
|
this._pageEvents = new AsyncEmitter(this._page);
|
|
|
|
this._pageEvents.on(PageEvents.ResponseReceived, response => this._onResponseReceived(response));
|
|
|
|
this._pageEvents.on(PageEvents.ResourceLoadingFailed, event => (this.onResourceError || noop).call(null, event));
|
2017-05-13 18:05:54 +00:00
|
|
|
this._pageEvents.on(PageEvents.ConsoleMessage, msg => (this.onConsoleMessage || noop).call(null, msg));
|
2017-05-13 20:44:24 +00:00
|
|
|
this._pageEvents.on(PageEvents.Confirm, message => this._onConfirm(message));
|
|
|
|
this._pageEvents.on(PageEvents.Alert, message => this._onAlert(message));
|
2017-05-13 19:04:30 +00:00
|
|
|
this._pageEvents.on(PageEvents.Exception, (exception, stack) => (this._onError || noop).call(null, exception, stack));
|
2017-05-11 07:06:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_onResponseReceived(response) {
|
|
|
|
if (!this.onResourceReceived)
|
|
|
|
return;
|
|
|
|
var headers = [];
|
|
|
|
for (var key in response.headers) {
|
|
|
|
headers.push({
|
|
|
|
name: key,
|
|
|
|
value: response.headers[key]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
response.headers = headers;
|
|
|
|
this.onResourceReceived.call(null, response);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} url
|
|
|
|
* @param {function()} callback
|
|
|
|
*/
|
|
|
|
includeJs(url, callback) {
|
2017-05-12 17:19:01 +00:00
|
|
|
this._page.addScriptTag(url).then(callback);
|
2017-05-11 07:06:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {!{width: number, height: number}}
|
|
|
|
*/
|
|
|
|
get viewportSize() {
|
|
|
|
return this._page.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {!Object}
|
|
|
|
*/
|
|
|
|
get customHeaders() {
|
|
|
|
return this._page.extraHTTPHeaders();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {!Object} value
|
|
|
|
*/
|
|
|
|
set customHeaders(value) {
|
|
|
|
await(this._page.setExtraHTTPHeaders(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} filePath
|
|
|
|
*/
|
|
|
|
injectJs(filePath) {
|
|
|
|
if (!fs.existsSync(filePath))
|
|
|
|
filePath = path.resolve(this.libraryPath, filePath);
|
|
|
|
if (!fs.existsSync(filePath))
|
|
|
|
return;
|
2017-05-12 17:19:01 +00:00
|
|
|
await(this._page.injectFile(filePath));
|
2017-05-11 07:06:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {string}
|
|
|
|
*/
|
|
|
|
get plainText() {
|
|
|
|
return await(this._page.plainText());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {string}
|
|
|
|
*/
|
|
|
|
get title() {
|
|
|
|
return await(this._page.title());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {(function()|undefined)}
|
|
|
|
*/
|
|
|
|
get onError() {
|
|
|
|
return this._onError;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {(function()|undefined)} handler
|
|
|
|
*/
|
|
|
|
set onError(handler) {
|
|
|
|
if (typeof handler !== 'function')
|
|
|
|
handler = undefined;
|
|
|
|
this._onError = handler;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {(function()|undefined)}
|
|
|
|
*/
|
|
|
|
get onConfirm() {
|
2017-05-13 20:44:24 +00:00
|
|
|
return this._onConfirmCallback;
|
2017-05-11 07:06:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {function()} handler
|
|
|
|
*/
|
|
|
|
set onConfirm(handler) {
|
|
|
|
if (typeof handler !== 'function')
|
|
|
|
handler = undefined;
|
2017-05-13 20:44:24 +00:00
|
|
|
this._onConfirmCallback = handler;
|
2017-05-11 07:06:41 +00:00
|
|
|
}
|
|
|
|
|
2017-05-13 20:44:24 +00:00
|
|
|
/**
|
|
|
|
* @param {string} message
|
|
|
|
*/
|
|
|
|
_onConfirm(message) {
|
|
|
|
if (!this._onConfirmCallback)
|
2017-05-11 07:06:41 +00:00
|
|
|
return;
|
2017-05-13 20:44:24 +00:00
|
|
|
var accept = this._onConfirmCallback.call(null, message);
|
2017-05-11 07:06:41 +00:00
|
|
|
await(this._page.handleDialog(accept));
|
|
|
|
}
|
|
|
|
|
2017-05-13 20:44:24 +00:00
|
|
|
/**
|
|
|
|
* @return {(function()|undefined)}
|
|
|
|
*/
|
|
|
|
get onAlert() {
|
|
|
|
return this._onAlertCallback;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {function()} handler
|
|
|
|
*/
|
|
|
|
set onAlert(handler) {
|
|
|
|
if (typeof handler !== 'function')
|
|
|
|
handler = undefined;
|
|
|
|
this._onAlertCallback = handler;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} message
|
|
|
|
*/
|
|
|
|
_onAlert(message) {
|
|
|
|
if (!this._onAlertCallback)
|
|
|
|
return;
|
|
|
|
this._onAlertCallback.call(null, message);
|
|
|
|
await(this._page.handleDialog(true));
|
|
|
|
}
|
|
|
|
|
2017-05-11 07:06:41 +00:00
|
|
|
/**
|
|
|
|
* @return {string}
|
|
|
|
*/
|
|
|
|
get url() {
|
|
|
|
return await(this._page.url());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} html
|
|
|
|
*/
|
|
|
|
set content(html) {
|
|
|
|
await(this._page.setContent(html));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} html
|
|
|
|
* @param {function()=} callback
|
|
|
|
*/
|
|
|
|
open(url, callback) {
|
|
|
|
console.assert(arguments.length <= 2, 'WebPage.open does not support METHOD and DATA arguments');
|
2017-05-12 23:25:04 +00:00
|
|
|
this._deferEvaluate = true;
|
|
|
|
if (typeof this.onInitialized === 'function')
|
|
|
|
this.onInitialized();
|
|
|
|
this._deferEvaluate = false;
|
2017-05-11 07:06:41 +00:00
|
|
|
this._page.navigate(url).then(result => {
|
|
|
|
var status = result ? 'success' : 'fail';
|
|
|
|
if (!result) {
|
|
|
|
this.onResourceError.call(null, {
|
|
|
|
url,
|
|
|
|
errorString: 'SSL handshake failed'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (this.onLoadFinished)
|
|
|
|
this.onLoadFinished.call(null, status);
|
|
|
|
if (callback)
|
|
|
|
callback.call(null, status);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {!{width: number, height: number}} options
|
|
|
|
*/
|
|
|
|
set viewportSize(options) {
|
|
|
|
await(this._page.setSize(options));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {function()} fun
|
|
|
|
* @param {!Array<!Object>} args
|
|
|
|
*/
|
|
|
|
evaluate(fun, ...args) {
|
2017-05-12 23:25:04 +00:00
|
|
|
if (this._deferEvaluate)
|
|
|
|
return await(this._page.evaluateOnInitialized(fun, ...args));
|
2017-05-11 07:06:41 +00:00
|
|
|
return await(this._page.evaluate(fun, ...args));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {string} fileName
|
|
|
|
*/
|
|
|
|
render(fileName) {
|
|
|
|
var clipRect = null;
|
|
|
|
if (this.clipRect && (this.clipRect.left || this.clipRect.top || this.clipRect.width || this.clipRect.height)) {
|
|
|
|
clipRect = {
|
|
|
|
x: this.clipRect.left,
|
|
|
|
y: this.clipRect.top,
|
|
|
|
width: this.clipRect.width,
|
|
|
|
height: this.clipRect.height
|
|
|
|
};
|
|
|
|
}
|
2017-05-12 17:50:06 +00:00
|
|
|
var imageBuffer = await(this._page.saveScreenshot(fileName, clipRect));
|
2017-05-11 07:06:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
release() {
|
|
|
|
this._page.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
this._page.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class WebPageSettings {
|
|
|
|
/**
|
|
|
|
* @param {!Page} page
|
|
|
|
*/
|
|
|
|
constructor(page) {
|
|
|
|
this._page = page;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} value
|
|
|
|
*/
|
|
|
|
set userAgent(value) {
|
|
|
|
await(this._page.setUserAgentOverride(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {string}
|
|
|
|
*/
|
|
|
|
get userAgent() {
|
|
|
|
return this._page.userAgentOverride();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// To prevent reenterability, eventemitters should emit events
|
|
|
|
// only being in a consistent state.
|
|
|
|
// This is not the case for 'ws' npm module: https://goo.gl/sy3dJY
|
|
|
|
//
|
|
|
|
// Since out phantomjs environment uses nested event loops, we
|
|
|
|
// exploit this condition in 'ws', which probably never happens
|
|
|
|
// in case of regular I/O.
|
|
|
|
//
|
|
|
|
// This class is a wrapper around EventEmitter which re-emits events asynchronously,
|
|
|
|
// helping to overcome the issue.
|
|
|
|
class AsyncEmitter extends EventEmitter {
|
|
|
|
/**
|
|
|
|
* @param {!Page} page
|
|
|
|
*/
|
|
|
|
constructor(page) {
|
|
|
|
super();
|
|
|
|
this._page = page;
|
|
|
|
this._symbol = Symbol('AsyncEmitter');
|
|
|
|
this.on('newListener', this._onListenerAdded);
|
|
|
|
this.on('removeListener', this._onListenerRemoved);
|
|
|
|
}
|
|
|
|
|
|
|
|
_onListenerAdded(event, listener) {
|
|
|
|
// Async listener calls original listener on next tick.
|
|
|
|
var asyncListener = (...args) => {
|
|
|
|
process.nextTick(() => listener.apply(null, args));
|
|
|
|
};
|
|
|
|
listener[this._symbol] = asyncListener;
|
|
|
|
this._page.on(event, asyncListener);
|
|
|
|
}
|
|
|
|
|
|
|
|
_onListenerRemoved(event, listener) {
|
|
|
|
this._page.removeListener(event, listener[this._symbol]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = WebPage;
|