Reformat codebase into 2-spaces

This patch:
- reformats codebase to use 2-spaces instead of 4. This will
  align the project with other codebases (e.g. DevTools and Lighthouse)
- enables eslint indentation checking

References #19
This commit is contained in:
Andrey Lushnikov 2017-06-21 07:45:13 -07:00 committed by Pavel Feldman
parent bbaf2f091c
commit 448ac4ce64
25 changed files with 3607 additions and 2693 deletions

View File

@ -100,12 +100,11 @@ module.exports = {
"no-trailing-spaces": 2, "no-trailing-spaces": 2,
"linebreak-style": [ 2, "unix" ], "linebreak-style": [ 2, "unix" ],
"indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }],
/** /**
* Disabled, aspirational rules * Disabled, aspirational rules
*/ */
"indent": [0, 4, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }],
// brace-style is disabled, as eslint cannot enforce 1tbs as default, but allman for functions // brace-style is disabled, as eslint cannot enforce 1tbs as default, but allman for functions
"brace-style": [0, "allman", { "allowSingleLine": true }], "brace-style": [0, "allman", { "allowSingleLine": true }],

View File

@ -15,5 +15,5 @@
*/ */
module.exports = { module.exports = {
Browser: require('./lib/Browser') Browser: require('./lib/Browser')
}; };

View File

@ -20,28 +20,26 @@ var ProgressBar = require('progress');
// Do nothing if the revision is already downloaded. // Do nothing if the revision is already downloaded.
if (Downloader.revisionInfo(Downloader.currentPlatform(), revision)) if (Downloader.revisionInfo(Downloader.currentPlatform(), revision))
return; return;
Downloader.downloadRevision(Downloader.currentPlatform(), revision, onProgress) Downloader.downloadRevision(Downloader.currentPlatform(), revision, onProgress)
.catch(error => { .catch(error => console.error('Download failed: ' + error.message));
console.error('Download failed: ' + error.message);
});
var progressBar = null; var progressBar = null;
function onProgress(bytesTotal, delta) { function onProgress(bytesTotal, delta) {
if (!progressBar) { if (!progressBar) {
progressBar = new ProgressBar(`Downloading Chromium - ${toMegabytes(bytesTotal)} [:bar] :percent :etas `, { progressBar = new ProgressBar(`Downloading Chromium - ${toMegabytes(bytesTotal)} [:bar] :percent :etas `, {
complete: '=', complete: '=',
incomplete: ' ', incomplete: ' ',
width: 20, width: 20,
total: bytesTotal, total: bytesTotal,
}); });
} }
progressBar.tick(delta); progressBar.tick(delta);
} }
function toMegabytes(bytes) { function toMegabytes(bytes) {
var mb = bytes / 1024 / 1024; var mb = bytes / 1024 / 1024;
return (Math.round(mb * 10) / 10) + ' Mb'; return (Math.round(mb * 10) / 10) + ' Mb';
} }

View File

@ -26,125 +26,125 @@ var CHROME_PROFILE_PATH = path.resolve(__dirname, '..', '.dev_profile');
var browserId = 0; var browserId = 0;
var DEFAULT_ARGS = [ var DEFAULT_ARGS = [
'--disable-background-timer-throttling', '--disable-background-timer-throttling',
'--no-first-run', '--no-first-run',
]; ];
class Browser { class Browser {
/** /**
* @param {(!Object|undefined)} options * @param {(!Object|undefined)} options
*/ */
constructor(options) { constructor(options) {
options = options || {}; options = options || {};
++browserId; ++browserId;
this._userDataDir = CHROME_PROFILE_PATH + browserId; this._userDataDir = CHROME_PROFILE_PATH + browserId;
this._remoteDebuggingPort = 9227; this._remoteDebuggingPort = 9227;
if (typeof options.remoteDebuggingPort === 'number') if (typeof options.remoteDebuggingPort === 'number')
this._remoteDebuggingPort = options.remoteDebuggingPort; this._remoteDebuggingPort = options.remoteDebuggingPort;
this._chromeArguments = DEFAULT_ARGS.concat([ this._chromeArguments = DEFAULT_ARGS.concat([
`--user-data-dir=${this._userDataDir}`, `--user-data-dir=${this._userDataDir}`,
`--remote-debugging-port=${this._remoteDebuggingPort}`, `--remote-debugging-port=${this._remoteDebuggingPort}`,
]); ]);
if (typeof options.headless !== 'boolean' || options.headless) { if (typeof options.headless !== 'boolean' || options.headless) {
this._chromeArguments.push(...[ this._chromeArguments.push(...[
`--headless`, `--headless`,
`--disable-gpu`, `--disable-gpu`,
]); ]);
}
if (typeof options.executablePath === 'string') {
this._chromeExecutable = options.executablePath;
} else {
var chromiumRevision = require('../package.json').puppeteer.chromium_revision;
var revisionInfo = Downloader.revisionInfo(Downloader.currentPlatform(), chromiumRevision);
console.assert(revisionInfo, 'Chromium revision is not downloaded. Run npm install');
this._chromeExecutable = revisionInfo.executablePath;
}
if (Array.isArray(options.args))
this._chromeArguments.push(...options.args);
this._terminated = false;
this._chromeProcess = null;
} }
if (typeof options.executablePath === 'string') {
/** this._chromeExecutable = options.executablePath;
* @return {!Promise<!Page>} } else {
*/ var chromiumRevision = require('../package.json').puppeteer.chromium_revision;
async newPage() { var revisionInfo = Downloader.revisionInfo(Downloader.currentPlatform(), chromiumRevision);
await this._ensureChromeIsRunning(); console.assert(revisionInfo, 'Chromium revision is not downloaded. Run npm install');
if (!this._chromeProcess || this._terminated) this._chromeExecutable = revisionInfo.executablePath;
throw new Error('ERROR: this chrome instance is not alive any more!');
var client = await Connection.create(this._remoteDebuggingPort);
var page = await Page.create(client);
return page;
} }
if (Array.isArray(options.args))
this._chromeArguments.push(...options.args);
this._terminated = false;
this._chromeProcess = null;
}
/** /**
* @param {!Page} page * @return {!Promise<!Page>}
*/ */
async closePage(page) { async newPage() {
if (!this._chromeProcess || this._terminated) await this._ensureChromeIsRunning();
throw new Error('ERROR: this chrome instance is not running'); if (!this._chromeProcess || this._terminated)
await page.close(); throw new Error('ERROR: this chrome instance is not alive any more!');
} var client = await Connection.create(this._remoteDebuggingPort);
var page = await Page.create(client);
return page;
}
/** /**
* @return {string} * @param {!Page} page
*/ */
async version() { async closePage(page) {
await this._ensureChromeIsRunning(); if (!this._chromeProcess || this._terminated)
var version = await Connection.version(this._remoteDebuggingPort); throw new Error('ERROR: this chrome instance is not running');
return version.Browser; await page.close();
} }
async _ensureChromeIsRunning() { /**
if (this._chromeProcess) * @return {string}
return; */
this._chromeProcess = childProcess.spawn(this._chromeExecutable, this._chromeArguments, {}); async version() {
var stderr = ''; await this._ensureChromeIsRunning();
this._chromeProcess.stderr.on('data', data => stderr += data.toString('utf8')); var version = await Connection.version(this._remoteDebuggingPort);
// Cleanup as processes exit. return version.Browser;
process.on('exit', () => this._chromeProcess.kill()); }
this._chromeProcess.on('exit', () => {
this._terminated = true;
removeRecursive(this._userDataDir);
});
await waitForChromeResponsive(this._remoteDebuggingPort, () => !this._terminated); async _ensureChromeIsRunning() {
if (this._terminated) if (this._chromeProcess)
throw new Error('Failed to launch chrome! ' + stderr); return;
} this._chromeProcess = childProcess.spawn(this._chromeExecutable, this._chromeArguments, {});
var stderr = '';
this._chromeProcess.stderr.on('data', data => stderr += data.toString('utf8'));
// Cleanup as processes exit.
process.on('exit', () => this._chromeProcess.kill());
this._chromeProcess.on('exit', () => {
this._terminated = true;
removeRecursive(this._userDataDir);
});
close() { await waitForChromeResponsive(this._remoteDebuggingPort, () => !this._terminated);
if (!this._chromeProcess) if (this._terminated)
return; throw new Error('Failed to launch chrome! ' + stderr);
this._chromeProcess.kill(); }
}
close() {
if (!this._chromeProcess)
return;
this._chromeProcess.kill();
}
} }
module.exports = Browser; module.exports = Browser;
function waitForChromeResponsive(remoteDebuggingPort, shouldWaitCallback) { function waitForChromeResponsive(remoteDebuggingPort, shouldWaitCallback) {
var fulfill; var fulfill;
var promise = new Promise(x => fulfill = x); var promise = new Promise(x => fulfill = x);
var options = { var options = {
method: 'GET', method: 'GET',
host: 'localhost', host: 'localhost',
port: remoteDebuggingPort, port: remoteDebuggingPort,
path: '/json/list' path: '/json/list'
}; };
var probeTimeout = 100; var probeTimeout = 100;
sendRequest(); sendRequest();
return promise; return promise;
function sendRequest() { function sendRequest() {
var req = http.request(options, res => { var req = http.request(options, res => {
fulfill(); fulfill();
}); });
req.on('error', e => { req.on('error', e => {
if (shouldWaitCallback()) if (shouldWaitCallback())
setTimeout(sendRequest, probeTimeout); setTimeout(sendRequest, probeTimeout);
else else
fulfill(); fulfill();
}); });
req.end(); req.end();
} }
} }

View File

@ -20,90 +20,90 @@ var http = require('http');
const COMMAND_TIMEOUT = 10000; const COMMAND_TIMEOUT = 10000;
class Connection extends EventEmitter { class Connection extends EventEmitter {
/** /**
* @param {number} port * @param {number} port
* @param {string} pageId * @param {string} pageId
* @param {!WebSocket} ws * @param {!WebSocket} ws
*/ */
constructor(port, pageId, ws) { constructor(port, pageId, ws) {
super(); super();
this._port = port; this._port = port;
this._pageId = pageId; this._pageId = pageId;
this._lastId = 0; this._lastId = 0;
/** @type {!Map<number, {resolve: function(*), reject: function(*), method: string}>}*/ /** @type {!Map<number, {resolve: function(*), reject: function(*), method: string}>}*/
this._callbacks = new Map(); this._callbacks = new Map();
this._ws = ws; this._ws = ws;
this._ws.on('message', this._onMessage.bind(this)); this._ws.on('message', this._onMessage.bind(this));
this._ws.on('close', this._onClose.bind(this)); this._ws.on('close', this._onClose.bind(this));
}
/**
* @param {string} method
* @param {(!Object|undefined)} params
* @return {!Promise<?Object>}
*/
send(method, params = {}) {
var id = ++this._lastId;
var message = JSON.stringify({id, method, params});
this._ws.send(message);
return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, method});
});
}
/**
* @param {string} message
*/
_onMessage(message) {
var object = JSON.parse(message);
if (object.id && this._callbacks.has(object.id)) {
var callback = this._callbacks.get(object.id);
this._callbacks.delete(object.id);
if (object.error)
callback.reject(new Error(`Protocol error (${callback.method}): ${object.error.message}`));
else
callback.resolve(object.result);
} else {
console.assert(!object.id);
this.emit(object.method, object.params);
} }
}
/** _onClose() {
* @param {string} method this._ws.removeAllListeners();
* @param {(!Object|undefined)} params this._ws.close();
* @return {!Promise<?Object>} }
*/
send(method, params = {}) {
var id = ++this._lastId;
var message = JSON.stringify({id, method, params});
this._ws.send(message);
return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, method});
});
}
/** /**
* @param {string} message * @return {!Promise}
*/ */
_onMessage(message) { async dispose() {
var object = JSON.parse(message); await runJsonCommand(this._port, `close/${this._pageId}`);
if (object.id && this._callbacks.has(object.id)) { }
var callback = this._callbacks.get(object.id);
this._callbacks.delete(object.id);
if (object.error)
callback.reject(new Error(`Protocol error (${callback.method}): ${object.error.message}`));
else
callback.resolve(object.result);
} else {
console.assert(!object.id);
this.emit(object.method, object.params);
}
}
_onClose() { /**
this._ws.removeAllListeners(); * @param {number} port
this._ws.close(); * @return {!Promise<!Connection>}
} */
static async create(port) {
var newTab = await runJsonCommand(port, 'new');
var url = newTab.webSocketDebuggerUrl;
/** return new Promise((resolve, reject) => {
* @return {!Promise} var ws = new WebSocket(url, { perMessageDeflate: false });
*/ ws.on('open', () => resolve(new Connection(port, newTab.id, ws)));
async dispose() { ws.on('error', reject);
await runJsonCommand(this._port, `close/${this._pageId}`); });
} }
/** /**
* @param {number} port * @param {number} port
* @return {!Promise<!Connection>} * @return {!Promise<!Object>}
*/ */
static async create(port) { static version(port) {
var newTab = await runJsonCommand(port, 'new'); return runJsonCommand(port, 'version');
var url = newTab.webSocketDebuggerUrl; }
return new Promise((resolve, reject) => {
var ws = new WebSocket(url, { perMessageDeflate: false });
ws.on('open', () => resolve(new Connection(port, newTab.id, ws)));
ws.on('error', reject);
});
}
/**
* @param {number} port
* @return {!Promise<!Object>}
*/
static version(port) {
return runJsonCommand(port, 'version');
}
} }
/** /**
@ -112,41 +112,41 @@ class Connection extends EventEmitter {
* @return {!Promise<!Object>} * @return {!Promise<!Object>}
*/ */
function runJsonCommand(port, command) { function runJsonCommand(port, command) {
var request = http.get({ var request = http.get({
hostname: 'localhost', hostname: 'localhost',
port: port, port: port,
path: '/json/' + command path: '/json/' + command
}, onResponse); }, onResponse);
request.setTimeout(COMMAND_TIMEOUT, onTimeout); request.setTimeout(COMMAND_TIMEOUT, onTimeout);
var resolve, reject; var resolve, reject;
return new Promise((res, rej) => { resolve = res; reject = rej; }); return new Promise((res, rej) => { resolve = res; reject = rej; });
function onResponse(response) { function onResponse(response) {
var data = ''; var data = '';
response.on('data', chunk => data += chunk); response.on('data', chunk => data += chunk);
response.on('end', () => { response.on('end', () => {
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
reject(new Error(`Protocol JSON API error (${command}), status: ${response.statusCode}`)); reject(new Error(`Protocol JSON API error (${command}), status: ${response.statusCode}`));
return; return;
} }
// In the case of 'close' & 'activate' Chromium returns a string rather than JSON: goo.gl/7v27xD // In the case of 'close' & 'activate' Chromium returns a string rather than JSON: goo.gl/7v27xD
if (data === 'Target is closing' || data === 'Target activated') { if (data === 'Target is closing' || data === 'Target activated') {
resolve({message: data}); resolve({message: data});
return; return;
} }
try { try {
resolve(JSON.parse(data)); resolve(JSON.parse(data));
} catch (e) { } catch (e) {
reject(e); reject(e);
} }
}); });
} }
function onTimeout() { function onTimeout() {
request.abort(); request.abort();
// Reject on error with code specifically indicating timeout in connection setup. // Reject on error with code specifically indicating timeout in connection setup.
reject(new Error('Timeout waiting for initial Debugger Protocol connection.')); reject(new Error('Timeout waiting for initial Debugger Protocol connection.'));
} }
} }
module.exports = Connection; module.exports = Connection;

View File

@ -15,55 +15,55 @@
*/ */
class Dialog { class Dialog {
/** /**
* @param {!Connection} client * @param {!Connection} client
* @param {!Dialog.Type} type * @param {!Dialog.Type} type
* @param {string} message * @param {string} message
*/ */
constructor(client, type, message) { constructor(client, type, message) {
this._client = client; this._client = client;
this.type = type; this.type = type;
this._message = message; this._message = message;
this._handled = false; this._handled = false;
} }
/** /**
* @return {string} * @return {string}
*/ */
message() { message() {
return this._message; return this._message;
} }
/** /**
* @param {string=} promptText * @param {string=} promptText
* @return {!Promise} * @return {!Promise}
*/ */
async accept(promptText) { async accept(promptText) {
console.assert(!this._handled, 'Cannot accept dialog which is already handled!'); console.assert(!this._handled, 'Cannot accept dialog which is already handled!');
this._handled = true; this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', { await this._client.send('Page.handleJavaScriptDialog', {
accept: true, accept: true,
promptText: promptText promptText: promptText
}); });
} }
/** /**
* @return {!Promise} * @return {!Promise}
*/ */
async dismiss() { async dismiss() {
console.assert(!this._handled, 'Cannot dismiss dialog which is already handled!'); console.assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
this._handled = true; this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', { await this._client.send('Page.handleJavaScriptDialog', {
accept: false accept: false
}); });
} }
} }
Dialog.Type = { Dialog.Type = {
Alert: 'alert', Alert: 'alert',
BeforeUnload: 'beforeunload', BeforeUnload: 'beforeunload',
Confirm: 'confirm', Confirm: 'confirm',
Prompt: 'prompt' Prompt: 'prompt'
}; };
module.exports = Dialog; module.exports = Dialog;

View File

@ -17,229 +17,229 @@
var EventEmitter = require('events'); var EventEmitter = require('events');
class FrameManager extends EventEmitter { class FrameManager extends EventEmitter {
/** /**
* @param {!Connection} client * @param {!Connection} client
* @return {!FrameManager} * @return {!FrameManager}
*/ */
static async create(client) { static async create(client) {
var mainFramePayload = await client.send('Page.getResourceTree'); var mainFramePayload = await client.send('Page.getResourceTree');
return new FrameManager(client, mainFramePayload.frameTree); return new FrameManager(client, mainFramePayload.frameTree);
}
/**
* @param {!Connection} client
* @param {!Object} frameTree
*/
constructor(client, frameTree) {
super();
this._client = client;
/** @type {!Map<string, !Frame>} */
this._frames = new Map();
this._mainFrame = this._addFramesRecursively(null, frameTree);
this._client.on('Page.frameAttached', event => this._frameAttached(event.frameId, event.parentFrameId));
this._client.on('Page.frameNavigated', event => this._frameNavigated(event.frame));
this._client.on('Page.frameDetached', event => this._frameDetached(event.frameId));
}
/**
* @return {!Frame}
*/
mainFrame() {
return this._mainFrame;
}
/**
* @return {!Array<!Frame>}
*/
frames() {
return Array.from(this._frames.values());
}
/**
* @param {string} frameId
* @param {?string} parentFrameId
* @return {?Frame}
*/
_frameAttached(frameId, parentFrameId) {
if (this._frames.has(frameId))
return;
if (!parentFrameId) {
// Navigation to the new backend process.
this._navigateFrame(this._mainFrame, frameId, null);
return;
} }
var parentFrame = this._frames.get(parentFrameId);
var frame = new Frame(parentFrame, frameId, null);
this._frames.set(frame._id, frame);
this.emit(FrameManager.Events.FrameAttached, frame);
}
/** /**
* @param {!Connection} client * @param {!Object} framePayload
* @param {!Object} frameTree */
*/ _frameNavigated(framePayload) {
constructor(client, frameTree) { var frame = this._frames.get(framePayload.id);
super(); if (!frame) {
this._client = client; // Simulate missed "frameAttached" for a main frame navigation to the new backend process.
/** @type {!Map<string, !Frame>} */ console.assert(!framePayload.parentId, 'Main frame shouldn\'t have parent frame id.');
this._frames = new Map(); frame = this._mainFrame;
this._mainFrame = this._addFramesRecursively(null, frameTree);
this._client.on('Page.frameAttached', event => this._frameAttached(event.frameId, event.parentFrameId));
this._client.on('Page.frameNavigated', event => this._frameNavigated(event.frame));
this._client.on('Page.frameDetached', event => this._frameDetached(event.frameId));
} }
this._navigateFrame(frame, framePayload.id, framePayload);
}
/** /**
* @return {!Frame} * @param {string} frameId
*/ */
mainFrame() { _frameDetached(frameId) {
return this._mainFrame; var frame = this._frames.get(frameId);
} if (frame)
this._removeFramesRecursively(frame);
}
/** /**
* @return {!Array<!Frame>} * @param {!Frame} frame
*/ * @param {string} newFrameId
frames() { * @param {?Object} newFramePayload
return Array.from(this._frames.values()); */
} _navigateFrame(frame, newFrameId, newFramePayload) {
// Detach all child frames first.
for (var child of frame.childFrames())
this._removeFramesRecursively(child);
this._frames.delete(frame._id, frame);
frame._id = newFrameId;
frame._adoptPayload(newFramePayload);
this._frames.set(newFrameId, frame);
this.emit(FrameManager.Events.FrameNavigated, frame);
}
/** /**
* @param {string} frameId * @param {?Frame} parentFrame
* @param {?string} parentFrameId * @param {!Object} frameTreePayload
* @return {?Frame} * @return {!Frame}
*/ */
_frameAttached(frameId, parentFrameId) { _addFramesRecursively(parentFrame, frameTreePayload) {
if (this._frames.has(frameId)) var framePayload = frameTreePayload.frame;
return; var frame = new Frame(parentFrame, framePayload.id, framePayload);
this._frames.set(frame._id, frame);
if (!parentFrameId) { for (var i = 0; frameTreePayload.childFrames && i < frameTreePayload.childFrames.length; ++i)
// Navigation to the new backend process. this._addFramesRecursively(frame, frameTreePayload.childFrames[i]);
this._navigateFrame(this._mainFrame, frameId, null); return frame;
return; }
}
var parentFrame = this._frames.get(parentFrameId);
var frame = new Frame(parentFrame, frameId, null);
this._frames.set(frame._id, frame);
this.emit(FrameManager.Events.FrameAttached, frame);
}
/** /**
* @param {!Object} framePayload * @param {!Frame} frame
*/ */
_frameNavigated(framePayload) { _removeFramesRecursively(frame) {
var frame = this._frames.get(framePayload.id); for (var child of frame.childFrames())
if (!frame) { this._removeFramesRecursively(child);
// Simulate missed "frameAttached" for a main frame navigation to the new backend process. frame._detach();
console.assert(!framePayload.parentId, 'Main frame shouldn\'t have parent frame id.'); this._frames.delete(frame._id);
frame = this._mainFrame; this.emit(FrameManager.Events.FrameDetached, frame);
} }
this._navigateFrame(frame, framePayload.id, framePayload);
}
/**
* @param {string} frameId
*/
_frameDetached(frameId) {
var frame = this._frames.get(frameId);
if (frame)
this._removeFramesRecursively(frame);
}
/**
* @param {!Frame} frame
* @param {string} newFrameId
* @param {?Object} newFramePayload
*/
_navigateFrame(frame, newFrameId, newFramePayload) {
// Detach all child frames first.
for (var child of frame.childFrames())
this._removeFramesRecursively(child);
this._frames.delete(frame._id, frame);
frame._id = newFrameId;
frame._adoptPayload(newFramePayload);
this._frames.set(newFrameId, frame);
this.emit(FrameManager.Events.FrameNavigated, frame);
}
/**
* @param {?Frame} parentFrame
* @param {!Object} frameTreePayload
* @return {!Frame}
*/
_addFramesRecursively(parentFrame, frameTreePayload) {
var framePayload = frameTreePayload.frame;
var frame = new Frame(parentFrame, framePayload.id, framePayload);
this._frames.set(frame._id, frame);
for (var i = 0; frameTreePayload.childFrames && i < frameTreePayload.childFrames.length; ++i)
this._addFramesRecursively(frame, frameTreePayload.childFrames[i]);
return frame;
}
/**
* @param {!Frame} frame
*/
_removeFramesRecursively(frame) {
for (var child of frame.childFrames())
this._removeFramesRecursively(child);
frame._detach();
this._frames.delete(frame._id);
this.emit(FrameManager.Events.FrameDetached, frame);
}
} }
/** @enum {string} */ /** @enum {string} */
FrameManager.Events = { FrameManager.Events = {
FrameAttached: 'frameattached', FrameAttached: 'frameattached',
FrameNavigated: 'framenavigated', FrameNavigated: 'framenavigated',
FrameDetached: 'framedetached' FrameDetached: 'framedetached'
}; };
/** /**
* @unrestricted * @unrestricted
*/ */
class Frame { class Frame {
/** /**
* @param {?Frame} parentFrame * @param {?Frame} parentFrame
* @param {string} frameId * @param {string} frameId
* @param {?Object} payload * @param {?Object} payload
*/ */
constructor(parentFrame, frameId, payload) { constructor(parentFrame, frameId, payload) {
this._parentFrame = parentFrame; this._parentFrame = parentFrame;
this._url = ''; this._url = '';
this._id = frameId; this._id = frameId;
this._adoptPayload(payload); this._adoptPayload(payload);
/** @type {!Set<!Frame>} */ /** @type {!Set<!Frame>} */
this._childFrames = new Set(); this._childFrames = new Set();
if (this._parentFrame) if (this._parentFrame)
this._parentFrame._childFrames.add(this); this._parentFrame._childFrames.add(this);
} }
/** /**
* @return {string} * @return {string}
*/ */
name() { name() {
return this._name || ''; return this._name || '';
} }
/** /**
* @return {string} * @return {string}
*/ */
url() { url() {
return this._url; return this._url;
} }
/** /**
* @return {string} * @return {string}
*/ */
securityOrigin() { securityOrigin() {
return this._securityOrigin; return this._securityOrigin;
} }
/** /**
* @return {?Frame} * @return {?Frame}
*/ */
parentFrame() { parentFrame() {
return this._parentFrame; return this._parentFrame;
} }
/** /**
* @return {!Array.<!Frame>} * @return {!Array.<!Frame>}
*/ */
childFrames() { childFrames() {
return Array.from(this._childFrames); return Array.from(this._childFrames);
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isMainFrame() { isMainFrame() {
return !this._detached && !this._parentFrame; return !this._detached && !this._parentFrame;
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
isDetached() { isDetached() {
return this._detached; return this._detached;
} }
/** /**
* @param {?Object} framePayload * @param {?Object} framePayload
*/ */
_adoptPayload(framePayload) { _adoptPayload(framePayload) {
framePayload = framePayload || { framePayload = framePayload || {
name: '', name: '',
url: '', url: '',
securityOrigin: '', securityOrigin: '',
mimeType: '' mimeType: ''
}; };
this._name = framePayload.name; this._name = framePayload.name;
this._url = framePayload.url; this._url = framePayload.url;
this._securityOrigin = framePayload.securityOrigin; this._securityOrigin = framePayload.securityOrigin;
this._mimeType = framePayload.mimeType; this._mimeType = framePayload.mimeType;
} }
_detach() { _detach() {
this._detached = true; this._detached = true;
if (this._parentFrame) if (this._parentFrame)
this._parentFrame._childFrames.delete(this); this._parentFrame._childFrames.delete(this);
this._parentFrame = null; this._parentFrame = null;
} }
} }
module.exports = FrameManager; module.exports = FrameManager;

File diff suppressed because it is too large Load Diff

View File

@ -15,112 +15,112 @@
*/ */
class Request { class Request {
/** /**
* @param {!Connection} client * @param {!Connection} client
* @param {string} interceptionId * @param {string} interceptionId
* @param {!Object} payload * @param {!Object} payload
*/ */
constructor(client, interceptionId, payload) { constructor(client, interceptionId, payload) {
this._client = client; this._client = client;
this._interceptionId = interceptionId; this._interceptionId = interceptionId;
this._url = payload.url; this._url = payload.url;
this._method = payload.method; this._method = payload.method;
this._headers = payload.headers; this._headers = payload.headers;
this._postData = payload.postData; this._postData = payload.postData;
this._urlOverride = undefined; this._urlOverride = undefined;
this._methodOverride = undefined; this._methodOverride = undefined;
this._postDataOverride = undefined; this._postDataOverride = undefined;
this._handled = false; this._handled = false;
} }
/** /**
* @return {string} * @return {string}
*/ */
url() { url() {
return this._urlOverride || this._url; return this._urlOverride || this._url;
} }
/** /**
* @param {string} url * @param {string} url
*/ */
setUrl(url) { setUrl(url) {
this._urlOverride = url; this._urlOverride = url;
} }
/** /**
* @return {string} * @return {string}
*/ */
method() { method() {
return this._methodOverride || this._method; return this._methodOverride || this._method;
} }
/** /**
* @param {string} method * @param {string} method
*/ */
setMethod(method) { setMethod(method) {
this._methodOverride = method; this._methodOverride = method;
} }
/** /**
* @return {!Object} * @return {!Object}
*/ */
headers() { headers() {
return Object.assign({}, this._headersOverride || this._headers); return Object.assign({}, this._headersOverride || this._headers);
} }
/** /**
* @param {string} key * @param {string} key
* @param {string} value * @param {string} value
*/ */
setHeader(key, value) { setHeader(key, value) {
if (!this._headersOverride) if (!this._headersOverride)
this._headersOverride = Object.assign({}, this._headers); this._headersOverride = Object.assign({}, this._headers);
this._headersOverride[key] = value; this._headersOverride[key] = value;
} }
/** /**
* @return {(string|undefined)} * @return {(string|undefined)}
*/ */
postData() { postData() {
return this._postDataOverride || this._postData; return this._postDataOverride || this._postData;
} }
/** /**
* @return {(string|undefined)} * @return {(string|undefined)}
*/ */
setPostData(data) { setPostData(data) {
this._postDataOverride = data; this._postDataOverride = data;
} }
abort() { abort() {
console.assert(!this._handled, 'This request is already handled!'); console.assert(!this._handled, 'This request is already handled!');
this._handled = true; this._handled = true;
this._client.send('Network.continueInterceptedRequest', { this._client.send('Network.continueInterceptedRequest', {
interceptionId: this._interceptionId, interceptionId: this._interceptionId,
errorReason: 'Aborted' errorReason: 'Aborted'
}); });
} }
continue() { continue() {
console.assert(!this._handled, 'This request is already handled!'); console.assert(!this._handled, 'This request is already handled!');
this._handled = true; this._handled = true;
this._client.send('Network.continueInterceptedRequest', { this._client.send('Network.continueInterceptedRequest', {
interceptionId: this._interceptionId, interceptionId: this._interceptionId,
url: this._urlOverride, url: this._urlOverride,
method: this._methodOverride, method: this._methodOverride,
postData: this._postDataOverride, postData: this._postDataOverride,
headers: this._headersOverride headers: this._headersOverride
}); });
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
handled() { handled() {
return this._handled; return this._handled;
} }
} }
module.exports = Request; module.exports = Request;

View File

@ -15,21 +15,20 @@
*/ */
module.exports = { module.exports = {
/**
/** * @param {function()} fun
* @param {function()} fun * @param {!Array<*>} args
* @param {!Array<*>} args * @param {boolean=} wrapInPromise
* @param {boolean=} wrapInPromise * @param {string=} sourceURL
* @param {string=} sourceURL * @return {string}
* @return {string} */
*/ evaluationString: function(fun, args, wrapInPromise, sourceURL) {
evaluationString: function(fun, args, wrapInPromise, sourceURL) { var argsString = args.map(x => JSON.stringify(x)).join(',');
var argsString = args.map(x => JSON.stringify(x)).join(','); var code = `(${fun.toString()})(${argsString})`;
var code = `(${fun.toString()})(${argsString})`; if (wrapInPromise)
if (wrapInPromise) code = `Promise.resolve(${code})`;
code = `Promise.resolve(${code})`; if (sourceURL)
if (sourceURL) code += `\n//# sourceURL=${sourceURL}`;
code += `\n//# sourceURL=${sourceURL}`; return code;
return code; }
}
}; };

914
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,355 +21,355 @@ var removeRecursive = require('rimraf').sync;
var copyRecursive = deasync(require('ncp').ncp); var copyRecursive = deasync(require('ncp').ncp);
class FileSystem { class FileSystem {
constructor() { constructor() {
this.separator = path.sep; this.separator = path.sep;
} }
/** /**
* @return {string} * @return {string}
*/ */
get workingDirectory() { get workingDirectory() {
return process.cwd(); return process.cwd();
} }
/** /**
* @param {string} directoryPath * @param {string} directoryPath
*/ */
changeWorkingDirectory(directoryPath) { changeWorkingDirectory(directoryPath) {
try { try {
process.chdir(directoryPath); process.chdir(directoryPath);
return true; return true;
} catch (e){ } catch (e){
return false; return false;
}
} }
}
/** /**
* @param {string} relativePath * @param {string} relativePath
* @return {string} * @return {string}
*/ */
absolute(relativePath) { absolute(relativePath) {
relativePath = path.normalize(relativePath); relativePath = path.normalize(relativePath);
if (path.isAbsolute(relativePath)) if (path.isAbsolute(relativePath))
return relativePath; return relativePath;
return path.resolve(path.join(process.cwd(), relativePath)); return path.resolve(path.join(process.cwd(), relativePath));
} }
/** /**
* @param {string} filePath * @param {string} filePath
* @return {boolean} * @return {boolean}
*/ */
exists(filePath) { exists(filePath) {
return fs.existsSync(filePath); return fs.existsSync(filePath);
} }
/** /**
* @param {string} fromPath * @param {string} fromPath
* @param {string} toPath * @param {string} toPath
*/ */
copy(fromPath, toPath) { copy(fromPath, toPath) {
var content = fs.readFileSync(fromPath); var content = fs.readFileSync(fromPath);
fs.writeFileSync(toPath, content); fs.writeFileSync(toPath, content);
} }
/** /**
* @param {string} fromPath * @param {string} fromPath
* @param {string} toPath * @param {string} toPath
*/ */
move(fromPath, toPath) { move(fromPath, toPath) {
var content = fs.readFileSync(fromPath); var content = fs.readFileSync(fromPath);
fs.writeFileSync(toPath, content); fs.writeFileSync(toPath, content);
fs.unlinkSync(fromPath); fs.unlinkSync(fromPath);
} }
/** /**
* @param {string} filePath * @param {string} filePath
* @return {number} * @return {number}
*/ */
size(filePath) { size(filePath) {
return fs.statSync(filePath).size; return fs.statSync(filePath).size;
} }
/** /**
* @param {string} filePath * @param {string} filePath
*/ */
touch(filePath) { touch(filePath) {
fs.closeSync(fs.openSync(filePath, 'a')); fs.closeSync(fs.openSync(filePath, 'a'));
} }
/** /**
* @param {string} filePath * @param {string} filePath
*/ */
remove(filePath) { remove(filePath) {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
} }
/** /**
* @param {string} filePath * @param {string} filePath
* @return {boolean} * @return {boolean}
*/ */
lastModified(filePath) { lastModified(filePath) {
return fs.statSync(filePath).mtime; return fs.statSync(filePath).mtime;
} }
/** /**
* @param {string} dirPath * @param {string} dirPath
* @return {boolean} * @return {boolean}
*/ */
makeDirectory(dirPath) { makeDirectory(dirPath) {
try { try {
fs.mkdirSync(dirPath); fs.mkdirSync(dirPath);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
}
} }
}
/** /**
* @param {string} dirPath * @param {string} dirPath
* @return {boolean} * @return {boolean}
*/ */
makeTree(dirPath) { makeTree(dirPath) {
return this.makeDirectory(dirPath); return this.makeDirectory(dirPath);
} }
/** /**
* @param {string} dirPath * @param {string} dirPath
*/ */
removeTree(dirPath) { removeTree(dirPath) {
removeRecursive(dirPath); removeRecursive(dirPath);
} }
/** /**
* @param {string} fromPath * @param {string} fromPath
* @param {string} toPath * @param {string} toPath
*/ */
copyTree(fromPath, toPath) { copyTree(fromPath, toPath) {
copyRecursive(fromPath, toPath); copyRecursive(fromPath, toPath);
} }
/** /**
* @param {string} dirPath * @param {string} dirPath
* @return {!Array<string>} * @return {!Array<string>}
*/ */
list(dirPath) { list(dirPath) {
return fs.readdirSync(dirPath); return fs.readdirSync(dirPath);
} }
/** /**
* @param {string} linkPath * @param {string} linkPath
* @return {string} * @return {string}
*/ */
readLink(linkPath) { readLink(linkPath) {
return fs.readlinkSync(linkPath); return fs.readlinkSync(linkPath);
} }
/** /**
* @param {string} filePath * @param {string} filePath
* @param {Object} data * @param {Object} data
* @param {string} mode * @param {string} mode
*/ */
write(filePath, data, mode) { write(filePath, data, mode) {
var fd = new FileDescriptor(filePath, mode, 'utf8'); var fd = new FileDescriptor(filePath, mode, 'utf8');
fd.write(data); fd.write(data);
fd.close(); fd.close();
} }
/** /**
* @param {string} somePath * @param {string} somePath
* @return {boolean} * @return {boolean}
*/ */
isAbsolute(somePath) { isAbsolute(somePath) {
return path.isAbsolute(somePath); return path.isAbsolute(somePath);
} }
/** /**
* @return {string} * @return {string}
*/ */
read(filePath) { read(filePath) {
return fs.readFileSync(filePath, 'utf8'); return fs.readFileSync(filePath, 'utf8');
} }
/** /**
* @param {string} filePath * @param {string} filePath
* @return {boolean} * @return {boolean}
*/ */
isFile(filePath) { isFile(filePath) {
return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile(); return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile();
} }
/** /**
* @param {string} dirPath * @param {string} dirPath
* @return {boolean} * @return {boolean}
*/ */
isDirectory(dirPath) { isDirectory(dirPath) {
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory(); return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
} }
/** /**
* @param {string} filePath * @param {string} filePath
* @return {boolean} * @return {boolean}
*/ */
isLink(filePath) { isLink(filePath) {
return fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink(); return fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink();
} }
/** /**
* @param {string} filePath * @param {string} filePath
* @return {boolean} * @return {boolean}
*/ */
isReadable(filePath) { isReadable(filePath) {
try { try {
fs.accessSync(filePath, fs.constants.R_OK); fs.accessSync(filePath, fs.constants.R_OK);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
}
} }
}
/** /**
* @param {string} filePath * @param {string} filePath
* @return {boolean} * @return {boolean}
*/ */
isWritable(filePath) { isWritable(filePath) {
try { try {
fs.accessSync(filePath, fs.constants.W_OK); fs.accessSync(filePath, fs.constants.W_OK);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
}
} }
}
/** /**
* @param {string} filePath * @param {string} filePath
* @return {boolean} * @return {boolean}
*/ */
isExecutable(filePath) { isExecutable(filePath) {
try { try {
fs.accessSync(filePath, fs.constants.X_OK); fs.accessSync(filePath, fs.constants.X_OK);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
}
} }
}
/** /**
* @param {string} somePath * @param {string} somePath
* @return {!Array<string>} * @return {!Array<string>}
*/ */
split(somePath) { split(somePath) {
somePath = path.normalize(somePath); somePath = path.normalize(somePath);
if (somePath.endsWith(path.sep)) if (somePath.endsWith(path.sep))
somePath = somePath.substring(0, somePath.length - path.sep.length); somePath = somePath.substring(0, somePath.length - path.sep.length);
return somePath.split(path.sep); return somePath.split(path.sep);
} }
/** /**
* @param {string} path1 * @param {string} path1
* @param {string} path2 * @param {string} path2
* @return {string} * @return {string}
*/ */
join(...args) { join(...args) {
if (args[0] === '' && args.length > 1) if (args[0] === '' && args.length > 1)
args[0] = path.sep; args[0] = path.sep;
args = args.filter(part => typeof part === 'string'); args = args.filter(part => typeof part === 'string');
return path.join.apply(path, args); return path.join.apply(path, args);
} }
/** /**
* @param {string} filePath * @param {string} filePath
* @param {(string|!Object)} option * @param {(string|!Object)} option
* @return {!FileDescriptor} * @return {!FileDescriptor}
*/ */
open(filePath, option) { open(filePath, option) {
if (typeof option === 'string') if (typeof option === 'string')
return new FileDescriptor(filePath, option); return new FileDescriptor(filePath, option);
return new FileDescriptor(filePath, option.mode); return new FileDescriptor(filePath, option.mode);
} }
} }
var fdwrite = deasync(fs.write); var fdwrite = deasync(fs.write);
var fdread = deasync(fs.read); var fdread = deasync(fs.read);
class FileDescriptor { class FileDescriptor {
/** /**
* @param {string} filePath * @param {string} filePath
* @param {string} mode * @param {string} mode
*/ */
constructor(filePath, mode) { constructor(filePath, mode) {
this._position = 0; this._position = 0;
this._encoding = 'utf8'; this._encoding = 'utf8';
if (mode === 'rb') { if (mode === 'rb') {
this._mode = 'r'; this._mode = 'r';
this._encoding = 'latin1'; this._encoding = 'latin1';
} else if (mode === 'wb' || mode === 'b') { } else if (mode === 'wb' || mode === 'b') {
this._mode = 'w'; this._mode = 'w';
this._encoding = 'latin1'; this._encoding = 'latin1';
} else if (mode === 'rw+') { } else if (mode === 'rw+') {
this._mode = 'a+'; this._mode = 'a+';
this._position = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0; this._position = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
} else { } else {
this._mode = mode; this._mode = mode;
}
this._fd = fs.openSync(filePath, this._mode);
} }
this._fd = fs.openSync(filePath, this._mode);
}
/** /**
* @param {string} data * @param {string} data
*/ */
write(data) { write(data) {
var buffer = Buffer.from(data, this._encoding); var buffer = Buffer.from(data, this._encoding);
var written = fdwrite(this._fd, buffer, 0, buffer.length, this._position); var written = fdwrite(this._fd, buffer, 0, buffer.length, this._position);
this._position += written; this._position += written;
} }
getEncoding() { getEncoding() {
return 'UTF-8'; return 'UTF-8';
} }
/** /**
* @param {string} data * @param {string} data
*/ */
writeLine(data) { writeLine(data) {
this.write(data + '\n'); this.write(data + '\n');
} }
/** /**
* @param {number=} size * @param {number=} size
* @return {string} * @return {string}
*/ */
read(size) { read(size) {
var position = this._position; var position = this._position;
if (!size) { if (!size) {
size = fs.fstatSync(this._fd).size; size = fs.fstatSync(this._fd).size;
position = 0; position = 0;
}
var buffer = new Buffer(size);
var bytesRead = fdread(this._fd, buffer, 0, size, position);
this._position += bytesRead;
return buffer.toString(this._encoding);
} }
var buffer = new Buffer(size);
var bytesRead = fdread(this._fd, buffer, 0, size, position);
this._position += bytesRead;
return buffer.toString(this._encoding);
}
flush() { flush() {
// noop. // noop.
} }
/** /**
* @param {number} position * @param {number} position
*/ */
seek(position) { seek(position) {
this._position = position; this._position = position;
} }
close() { close() {
fs.closeSync(this._fd); fs.closeSync(this._fd);
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
atEnd() { atEnd() {
} }
} }
module.exports = FileSystem; module.exports = FileSystem;

View File

@ -24,116 +24,116 @@ var url = require('url');
* @param {string} scriptPath * @param {string} scriptPath
*/ */
module.exports.create = function(context, scriptPath) { module.exports.create = function(context, scriptPath) {
var phantom = { var phantom = {
page: { page: {
onConsoleMessage: null, onConsoleMessage: null,
}, },
/** /**
* @param {string} relative * @param {string} relative
* @param {string} base * @param {string} base
* @return {string} * @return {string}
*/ */
resolveRelativeUrl: function(relative, base) { resolveRelativeUrl: function(relative, base) {
return url.resolve(base, relative); return url.resolve(base, relative);
}, },
/** /**
* @param {string} url * @param {string} url
* @return {string} * @return {string}
*/ */
fullyDecodeUrl: function(url) { fullyDecodeUrl: function(url) {
return decodeURI(url); return decodeURI(url);
}, },
libraryPath: path.dirname(scriptPath), libraryPath: path.dirname(scriptPath),
onError: null, onError: null,
/** /**
* @return {string} * @return {string}
*/ */
get outputEncoding() { get outputEncoding() {
return 'UTF-8'; return 'UTF-8';
}, },
/** /**
* @param {string} value * @param {string} value
*/ */
set outputEncoding(value) { set outputEncoding(value) {
throw new Error('Phantom.outputEncoding setter is not implemented'); throw new Error('Phantom.outputEncoding setter is not implemented');
}, },
/** /**
* @return {boolean} * @return {boolean}
*/ */
get cookiesEnabled() { get cookiesEnabled() {
return true; return true;
}, },
/** /**
* @param {boolean} value * @param {boolean} value
*/ */
set cookiesEnabled(value) { set cookiesEnabled(value) {
throw new Error('Phantom.cookiesEnabled setter is not implemented'); throw new Error('Phantom.cookiesEnabled setter is not implemented');
}, },
/** /**
* @return {!{major: number, minor: number, patch: number}} * @return {!{major: number, minor: number, patch: number}}
*/ */
get version() { get version() {
var versionParts = require('../package.json').version.split('.'); var versionParts = require('../package.json').version.split('.');
return { return {
major: parseInt(versionParts[0], 10), major: parseInt(versionParts[0], 10),
minor: parseInt(versionParts[1], 10), minor: parseInt(versionParts[1], 10),
patch: parseInt(versionParts[2], 10), patch: parseInt(versionParts[2], 10),
}; };
}, },
/** /**
* @param {number=} code * @param {number=} code
*/ */
exit: function(code) { exit: function(code) {
process.exit(code); process.exit(code);
}, },
/** /**
* @param {string} filePath * @param {string} filePath
* @return {boolean} * @return {boolean}
*/ */
injectJs: function(filePath) { injectJs: function(filePath) {
filePath = path.resolve(phantom.libraryPath, filePath); filePath = path.resolve(phantom.libraryPath, filePath);
if (!fs.existsSync(filePath)) if (!fs.existsSync(filePath))
return false; return false;
var code = fs.readFileSync(filePath, 'utf8'); var code = fs.readFileSync(filePath, 'utf8');
if (code.startsWith('#!')) if (code.startsWith('#!'))
code = code.substring(code.indexOf('\n')); code = code.substring(code.indexOf('\n'));
vm.runInContext(code, context, { vm.runInContext(code, context, {
filename: filePath, filename: filePath,
displayErrors: true displayErrors: true
}); });
return true; return true;
}, },
/** /**
* @param {string} moduleSource * @param {string} moduleSource
* @param {string} filename * @param {string} filename
*/ */
loadModule: function(moduleSource, filename) { loadModule: function(moduleSource, filename) {
var code = [ var code = [
'(function(require, exports, module) {\n', '(function(require, exports, module) {\n',
moduleSource, moduleSource,
'\n}.call({},', '\n}.call({},',
'require.cache[\'' + filename + '\']._getRequire(),', 'require.cache[\'' + filename + '\']._getRequire(),',
'require.cache[\'' + filename + '\'].exports,', 'require.cache[\'' + filename + '\'].exports,',
'require.cache[\'' + filename + '\']', 'require.cache[\'' + filename + '\']',
'));' '));'
].join(''); ].join('');
vm.runInContext(code, context, { vm.runInContext(code, context, {
filename: filename, filename: filename,
displayErrors: true displayErrors: true
}); });
}, },
}; };
return phantom; return phantom;
}; };

View File

@ -19,100 +19,100 @@ var await = require('./utilities').await;
var os = require('os'); var os = require('os');
class System { class System {
/** /**
* @param {!Array<string>} args * @param {!Array<string>} args
*/ */
constructor(args) { constructor(args) {
this.args = args; this.args = args;
this.env = {}; this.env = {};
Object.assign(this.env, process.env); Object.assign(this.env, process.env);
this.stdin = new StandardInput(process.stdin); this.stdin = new StandardInput(process.stdin);
this.stdout = new StandardOutput(process.stdout); this.stdout = new StandardOutput(process.stdout);
this.stderr = new StandardOutput(process.stderr); this.stderr = new StandardOutput(process.stderr);
this.platform = 'phantomjs'; this.platform = 'phantomjs';
this.pid = process.pid; this.pid = process.pid;
this.isSSLSupported = false; this.isSSLSupported = false;
this.os = { this.os = {
architecture: os.arch(), architecture: os.arch(),
name: os.type(), name: os.type(),
version: os.release() version: os.release()
}; };
} }
} }
class StandardInput { class StandardInput {
/** /**
* @param {!Readable} readableStream * @param {!Readable} readableStream
*/ */
constructor(readableStream) { constructor(readableStream) {
this._readline = readline.createInterface({ this._readline = readline.createInterface({
input: readableStream input: readableStream
}); });
this._lines = []; this._lines = [];
this._closed = false; this._closed = false;
this._readline.on('line', line => this._lines.push(line)); this._readline.on('line', line => this._lines.push(line));
this._readline.on('close', () => this._closed = true); this._readline.on('close', () => this._closed = true);
} }
/** /**
* @return {string} * @return {string}
*/ */
readLine() { readLine() {
if (this._closed && !this._lines.length) if (this._closed && !this._lines.length)
return ''; return '';
if (!this._lines.length) { if (!this._lines.length) {
var linePromise = new Promise(fulfill => this._readline.once('line', fulfill)); var linePromise = new Promise(fulfill => this._readline.once('line', fulfill));
await(linePromise); await(linePromise);
}
return this._lines.shift();
} }
return this._lines.shift();
}
/** /**
* @return {string} * @return {string}
*/ */
read() { read() {
if (!this._closed) { if (!this._closed) {
var closePromise = new Promise(fulfill => this._readline.once('close', fulfill)); var closePromise = new Promise(fulfill => this._readline.once('close', fulfill));
await(closePromise); await(closePromise);
}
var text = this._lines.join('\n');
this._lines = [];
return text;
} }
var text = this._lines.join('\n');
this._lines = [];
return text;
}
close() { close() {
this._readline.close(); this._readline.close();
} }
} }
class StandardOutput { class StandardOutput {
/** /**
* @param {!Writable} writableStream * @param {!Writable} writableStream
*/ */
constructor(writableStream) { constructor(writableStream) {
this._stream = writableStream; this._stream = writableStream;
} }
/** /**
* @param {string} data * @param {string} data
*/ */
write(data) { write(data) {
this._stream.write(data); this._stream.write(data);
} }
/** /**
* @param {string} data * @param {string} data
*/ */
writeLine(data) { writeLine(data) {
this._stream.write(data + '\n'); this._stream.write(data + '\n');
} }
flush() { flush() {
} }
close() { close() {
this._stream.end(); this._stream.end();
} }
} }
module.exports = System; module.exports = System;

View File

@ -23,319 +23,319 @@ var PageEvents = require('../lib/Page').Events;
var noop = function() { }; var noop = function() { };
class WebPage { class WebPage {
/** /**
* @param {!Browser} browser * @param {!Browser} browser
* @param {string} scriptPath * @param {string} scriptPath
* @param {!Object=} options * @param {!Object=} options
*/ */
constructor(browser, scriptPath, options) { constructor(browser, scriptPath, options) {
this._page = await(browser.newPage()); this._page = await(browser.newPage());
this.settings = new WebPageSettings(this._page); this.settings = new WebPageSettings(this._page);
options = options || {}; options = options || {};
options.settings = options.settings || {}; options.settings = options.settings || {};
if (options.settings.userAgent) if (options.settings.userAgent)
this.settings.userAgent = options.settings.userAgent; this.settings.userAgent = options.settings.userAgent;
if (options.viewportSize) if (options.viewportSize)
await(this._page.setViewportSize(options.viewportSize)); await(this._page.setViewportSize(options.viewportSize));
this.clipRect = options.clipRect || {left: 0, top: 0, width: 0, height: 0}; this.clipRect = options.clipRect || {left: 0, top: 0, width: 0, height: 0};
this.onConsoleMessage = null; this.onConsoleMessage = null;
this.onLoadFinished = null; this.onLoadFinished = null;
this.onResourceError = null; this.onResourceError = null;
this.onResourceReceived = null; this.onResourceReceived = null;
this.onInitialized = null; this.onInitialized = null;
this._deferEvaluate = false; this._deferEvaluate = false;
this.libraryPath = path.dirname(scriptPath); this.libraryPath = path.dirname(scriptPath);
this._onResourceRequestedCallback = undefined; this._onResourceRequestedCallback = undefined;
this._onConfirmCallback = undefined; this._onConfirmCallback = undefined;
this._onAlertCallback = undefined; this._onAlertCallback = undefined;
this._onError = noop; this._onError = noop;
this._pageEvents = new AsyncEmitter(this._page); this._pageEvents = new AsyncEmitter(this._page);
this._pageEvents.on(PageEvents.ResponseReceived, response => this._onResponseReceived(response)); this._pageEvents.on(PageEvents.ResponseReceived, response => this._onResponseReceived(response));
this._pageEvents.on(PageEvents.ResourceLoadingFailed, event => (this.onResourceError || noop).call(null, event)); this._pageEvents.on(PageEvents.ResourceLoadingFailed, event => (this.onResourceError || noop).call(null, event));
this._pageEvents.on(PageEvents.ConsoleMessage, msg => (this.onConsoleMessage || noop).call(null, msg)); this._pageEvents.on(PageEvents.ConsoleMessage, msg => (this.onConsoleMessage || noop).call(null, msg));
this._pageEvents.on(PageEvents.Confirm, message => this._onConfirm(message)); this._pageEvents.on(PageEvents.Confirm, message => this._onConfirm(message));
this._pageEvents.on(PageEvents.Alert, message => this._onAlert(message)); this._pageEvents.on(PageEvents.Alert, message => this._onAlert(message));
this._pageEvents.on(PageEvents.Dialog, dialog => this._onDialog(dialog)); this._pageEvents.on(PageEvents.Dialog, dialog => this._onDialog(dialog));
this._pageEvents.on(PageEvents.Error, error => (this._onError || noop).call(null, error.message, error.stack)); this._pageEvents.on(PageEvents.Error, error => (this._onError || noop).call(null, error.message, error.stack));
} }
/**
* @return {?function(!Object, !Request)}
*/
get onResourceRequested() {
return this._onResourceRequestedCallback;
}
/**
* @return {?function(!Object, !Request)} callback
*/
set onResourceRequested(callback) {
this._onResourceRequestedCallback = callback;
this._page.setRequestInterceptor(callback ? resourceInterceptor : null);
/** /**
* @return {?function(!Object, !Request)} * @param {!Request} request
*/ */
get onResourceRequested() { function resourceInterceptor(request) {
return this._onResourceRequestedCallback; var requestData = {
url: request.url(),
headers: request.headers()
};
callback(requestData, request);
if (!request.handled())
request.continue();
} }
}
/** _onResponseReceived(response) {
* @return {?function(!Object, !Request)} callback if (!this.onResourceReceived)
*/ return;
set onResourceRequested(callback) { var headers = [];
this._onResourceRequestedCallback = callback; for (var key in response.headers) {
this._page.setRequestInterceptor(callback ? resourceInterceptor : null); headers.push({
name: key,
/** value: response.headers[key]
* @param {!Request} request });
*/
function resourceInterceptor(request) {
var requestData = {
url: request.url(),
headers: request.headers()
};
callback(requestData, request);
if (!request.handled())
request.continue();
}
} }
response.headers = headers;
this.onResourceReceived.call(null, response);
}
_onResponseReceived(response) { /**
if (!this.onResourceReceived) * @param {string} url
return; * @param {function()} callback
var headers = []; */
for (var key in response.headers) { includeJs(url, callback) {
headers.push({ this._page.addScriptTag(url).then(callback);
name: key, }
value: response.headers[key]
}); /**
} * @return {!{width: number, height: number}}
response.headers = headers; */
this.onResourceReceived.call(null, response); get viewportSize() {
return this._page.viewportSize();
}
/**
* @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 false;
await(this._page.injectFile(filePath));
return true;
}
/**
* @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() {
return this._onConfirmCallback;
}
/**
* @param {function()} handler
*/
set onConfirm(handler) {
if (typeof handler !== 'function')
handler = undefined;
this._onConfirmCallback = handler;
}
/**
* @return {(function()|undefined)}
*/
get onAlert() {
return this._onAlertCallback;
}
/**
* @param {function()} handler
*/
set onAlert(handler) {
if (typeof handler !== 'function')
handler = undefined;
this._onAlertCallback = handler;
}
/**
* @param {!Dialog} dialog
*/
_onDialog(dialog) {
if (dialog.type === 'alert' && this._onAlertCallback) {
this._onAlertCallback.call(null, dialog.message());
await(dialog.accept());
} else if (dialog.type === 'confirm' && this._onConfirmCallback) {
var result = this._onConfirmCallback.call(null, dialog.message());
await(result ? dialog.accept() : dialog.dismiss());
} }
}
/** /**
* @param {string} url * @return {string}
* @param {function()} callback */
*/ get url() {
includeJs(url, callback) { return await(this._page.url());
this._page.addScriptTag(url).then(callback); }
}
/** /**
* @return {!{width: number, height: number}} * @param {string} html
*/ */
get viewportSize() { set content(html) {
return this._page.viewportSize(); await(this._page.setContent(html));
} }
/** /**
* @return {!Object} * @param {string} html
*/ * @param {function()=} callback
get customHeaders() { */
return this._page.extraHTTPHeaders(); open(url, callback) {
} console.assert(arguments.length <= 2, 'WebPage.open does not support METHOD and DATA arguments');
this._deferEvaluate = true;
/** if (typeof this.onInitialized === 'function')
* @param {!Object} value this.onInitialized();
*/ this._deferEvaluate = false;
set customHeaders(value) { this._page.navigate(url).then(result => {
await(this._page.setExtraHTTPHeaders(value)); var status = result ? 'success' : 'fail';
} if (!result) {
this.onResourceError.call(null, {
/** url,
* @param {string} filePath errorString: 'SSL handshake failed'
*/
injectJs(filePath) {
if (!fs.existsSync(filePath))
filePath = path.resolve(this.libraryPath, filePath);
if (!fs.existsSync(filePath))
return false;
await(this._page.injectFile(filePath));
return true;
}
/**
* @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() {
return this._onConfirmCallback;
}
/**
* @param {function()} handler
*/
set onConfirm(handler) {
if (typeof handler !== 'function')
handler = undefined;
this._onConfirmCallback = handler;
}
/**
* @return {(function()|undefined)}
*/
get onAlert() {
return this._onAlertCallback;
}
/**
* @param {function()} handler
*/
set onAlert(handler) {
if (typeof handler !== 'function')
handler = undefined;
this._onAlertCallback = handler;
}
/**
* @param {!Dialog} dialog
*/
_onDialog(dialog) {
if (dialog.type === 'alert' && this._onAlertCallback) {
this._onAlertCallback.call(null, dialog.message());
await(dialog.accept());
} else if (dialog.type === 'confirm' && this._onConfirmCallback) {
var result = this._onConfirmCallback.call(null, dialog.message());
await(result ? dialog.accept() : dialog.dismiss());
}
}
/**
* @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');
this._deferEvaluate = true;
if (typeof this.onInitialized === 'function')
this.onInitialized();
this._deferEvaluate = false;
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);
}); });
} }
if (this.onLoadFinished)
this.onLoadFinished.call(null, status);
if (callback)
callback.call(null, status);
});
}
/** /**
* @param {!{width: number, height: number}} options * @param {!{width: number, height: number}} options
*/ */
set viewportSize(options) { set viewportSize(options) {
await(this._page.setViewportSize(options)); await(this._page.setViewportSize(options));
} }
/** /**
* @param {function()} fun * @param {function()} fun
* @param {!Array<!Object>} args * @param {!Array<!Object>} args
*/ */
evaluate(fun, ...args) { evaluate(fun, ...args) {
if (this._deferEvaluate) if (this._deferEvaluate)
return await(this._page.evaluateOnInitialized(fun, ...args)); return await(this._page.evaluateOnInitialized(fun, ...args));
return await(this._page.evaluate(fun, ...args)); return await(this._page.evaluate(fun, ...args));
} }
/** /**
* {string} fileName * {string} fileName
*/ */
render(fileName) { render(fileName) {
if (fileName.endsWith('pdf')) { if (fileName.endsWith('pdf')) {
var options = {}; var options = {};
var paperSize = this.paperSize || {}; var paperSize = this.paperSize || {};
options.margin = paperSize.margin; options.margin = paperSize.margin;
options.format = paperSize.format; options.format = paperSize.format;
options.landscape = paperSize.orientation === 'landscape'; options.landscape = paperSize.orientation === 'landscape';
options.width = paperSize.width; options.width = paperSize.width;
options.height = paperSize.height; options.height = paperSize.height;
await(this._page.printToPDF(fileName, options)); await(this._page.printToPDF(fileName, options));
} else { } else {
var options = {}; var options = {};
if (this.clipRect && (this.clipRect.left || this.clipRect.top || this.clipRect.width || this.clipRect.height)) { if (this.clipRect && (this.clipRect.left || this.clipRect.top || this.clipRect.width || this.clipRect.height)) {
options.clip = { options.clip = {
x: this.clipRect.left, x: this.clipRect.left,
y: this.clipRect.top, y: this.clipRect.top,
width: this.clipRect.width, width: this.clipRect.width,
height: this.clipRect.height height: this.clipRect.height
}; };
} }
options.path = fileName; options.path = fileName;
await(this._page.screenshot(options)); await(this._page.screenshot(options));
}
} }
}
release() { release() {
this._page.close(); this._page.close();
} }
close() { close() {
this._page.close(); this._page.close();
} }
} }
class WebPageSettings { class WebPageSettings {
/** /**
* @param {!Page} page * @param {!Page} page
*/ */
constructor(page) { constructor(page) {
this._page = page; this._page = page;
} }
/** /**
* @param {string} value * @param {string} value
*/ */
set userAgent(value) { set userAgent(value) {
await(this._page.setUserAgentOverride(value)); await(this._page.setUserAgentOverride(value));
} }
/** /**
* @return {string} * @return {string}
*/ */
get userAgent() { get userAgent() {
return this._page.userAgentOverride(); return this._page.userAgentOverride();
} }
} }
// To prevent reenterability, eventemitters should emit events // To prevent reenterability, eventemitters should emit events
@ -349,29 +349,29 @@ class WebPageSettings {
// This class is a wrapper around EventEmitter which re-emits events asynchronously, // This class is a wrapper around EventEmitter which re-emits events asynchronously,
// helping to overcome the issue. // helping to overcome the issue.
class AsyncEmitter extends EventEmitter { class AsyncEmitter extends EventEmitter {
/** /**
* @param {!Page} page * @param {!Page} page
*/ */
constructor(page) { constructor(page) {
super(); super();
this._page = page; this._page = page;
this._symbol = Symbol('AsyncEmitter'); this._symbol = Symbol('AsyncEmitter');
this.on('newListener', this._onListenerAdded); this.on('newListener', this._onListenerAdded);
this.on('removeListener', this._onListenerRemoved); this.on('removeListener', this._onListenerRemoved);
} }
_onListenerAdded(event, listener) { _onListenerAdded(event, listener) {
// Async listener calls original listener on next tick. // Async listener calls original listener on next tick.
var asyncListener = (...args) => { var asyncListener = (...args) => {
process.nextTick(() => listener.apply(null, args)); process.nextTick(() => listener.apply(null, args));
}; };
listener[this._symbol] = asyncListener; listener[this._symbol] = asyncListener;
this._page.on(event, asyncListener); this._page.on(event, asyncListener);
} }
_onListenerRemoved(event, listener) { _onListenerRemoved(event, listener) {
this._page.removeListener(event, listener[this._symbol]); this._page.removeListener(event, listener[this._symbol]);
} }
} }
module.exports = WebPage; module.exports = WebPage;

View File

@ -18,66 +18,66 @@ var http = require('http');
var await = require('./utilities').await; var await = require('./utilities').await;
class WebServer { class WebServer {
constructor() { constructor() {
this._server = http.createServer(); this._server = http.createServer();
this.objectName = 'WebServer'; this.objectName = 'WebServer';
this.listenOnPort = this.listen; this.listenOnPort = this.listen;
this.newRequest = function(req, res) { }; this.newRequest = function(req, res) { };
Object.defineProperty(this, 'port', { Object.defineProperty(this, 'port', {
get: () => { get: () => {
if (!this._server.listening)
return '';
return this._server.address().port + '';
},
enumerable: true,
configurable: false
});
}
close() {
this._server.close();
}
/**
* @param {nubmer} port
* @return {boolean}
*/
listen(port, callback) {
if (this._server.listening)
return false;
this.newRequest = callback;
this._server.listen(port);
var errorPromise = new Promise(x => this._server.once('error', x));
var successPromise = new Promise(x => this._server.once('listening', x));
await(Promise.race([errorPromise, successPromise]));
if (!this._server.listening) if (!this._server.listening)
return false; return '';
return this._server.address().port + '';
},
enumerable: true,
configurable: false
});
}
this._server.on('request', (req, res) => { close() {
res.close = res.end.bind(res); this._server.close();
var headers = res.getHeaders(); }
res.headers = [];
for (var key in headers) { /**
res.headers.push({ * @param {nubmer} port
name: key, * @return {boolean}
value: headers[key] */
}); listen(port, callback) {
} if (this._server.listening)
res.header = res.getHeader; return false;
res.setHeaders = headers => { this.newRequest = callback;
for (var key in headers) this._server.listen(port);
res.setHeader(key, headers[key]); var errorPromise = new Promise(x => this._server.once('error', x));
}; var successPromise = new Promise(x => this._server.once('listening', x));
Object.defineProperty(res, 'statusCode', { await(Promise.race([errorPromise, successPromise]));
enumerable: true, if (!this._server.listening)
configurable: true, return false;
writable: true,
value: res.statusCode this._server.on('request', (req, res) => {
}); res.close = res.end.bind(res);
this.newRequest.call(null, req, res); var headers = res.getHeaders();
res.headers = [];
for (var key in headers) {
res.headers.push({
name: key,
value: headers[key]
}); });
return true; }
} res.header = res.getHeader;
res.setHeaders = headers => {
for (var key in headers)
res.setHeader(key, headers[key]);
};
Object.defineProperty(res, 'statusCode', {
enumerable: true,
configurable: true,
writable: true,
value: res.statusCode
});
this.newRequest.call(null, req, res);
});
return true;
}
} }
module.exports = WebServer; module.exports = WebServer;

View File

@ -27,39 +27,39 @@ var child_process = require('child_process');
var Browser = require('..').Browser; var Browser = require('..').Browser;
var version = require('../package.json').version; var version = require('../package.json').version;
var argv = require('minimist')(process.argv.slice(2), { var argv = require('minimist')(process.argv.slice(2), {
alias: { v: 'version' }, alias: { v: 'version' },
boolean: ['headless'], boolean: ['headless'],
default: {'headless': true }, default: {'headless': true },
}); });
if (argv.version) { if (argv.version) {
console.log('Puppeteer v' + version); console.log('Puppeteer v' + version);
return; return;
} }
if (argv['ssl-certificates-path']) { if (argv['ssl-certificates-path']) {
console.error('Flag --ssl-certificates-path is not supported.'); console.error('Flag --ssl-certificates-path is not supported.');
process.exit(1); process.exit(1);
return; return;
} }
var scriptArguments = argv._; var scriptArguments = argv._;
if (!scriptArguments.length) { if (!scriptArguments.length) {
console.log(__filename.split('/').pop() + ' [scriptfile]'); console.log(__filename.split('/').pop() + ' [scriptfile]');
return; return;
} }
var scriptPath = path.resolve(process.cwd(), scriptArguments[0]); var scriptPath = path.resolve(process.cwd(), scriptArguments[0]);
if (!fs.existsSync(scriptPath)) { if (!fs.existsSync(scriptPath)) {
console.error(`script not found: ${scriptPath}`); console.error(`script not found: ${scriptPath}`);
process.exit(1); process.exit(1);
return; return;
} }
var browser = new Browser({ var browser = new Browser({
remoteDebuggingPort: 9229, remoteDebuggingPort: 9229,
headless: argv.headless, headless: argv.headless,
args: ['--no-sandbox'] args: ['--no-sandbox']
}); });
var context = createPhantomContext(browser, scriptPath, argv); var context = createPhantomContext(browser, scriptPath, argv);
@ -73,38 +73,38 @@ vm.runInContext(scriptContent, context);
* @return {!Object} * @return {!Object}
*/ */
function createPhantomContext(browser, scriptPath, argv) { function createPhantomContext(browser, scriptPath, argv) {
var context = {}; var context = {};
context.setInterval = setInterval; context.setInterval = setInterval;
context.setTimeout = setTimeout; context.setTimeout = setTimeout;
context.clearInterval = clearInterval; context.clearInterval = clearInterval;
context.clearTimeout = clearTimeout; context.clearTimeout = clearTimeout;
context.phantom = Phantom.create(context, scriptPath); context.phantom = Phantom.create(context, scriptPath);
context.console = console; context.console = console;
context.window = context; context.window = context;
context.WebPage = options => new WebPage(browser, scriptPath, options); context.WebPage = options => new WebPage(browser, scriptPath, options);
vm.createContext(context); vm.createContext(context);
var nativeExports = { var nativeExports = {
fs: new FileSystem(), fs: new FileSystem(),
system: new System(argv._), system: new System(argv._),
webpage: { webpage: {
create: context.WebPage, create: context.WebPage,
}, },
webserver: { webserver: {
create: () => new WebServer(), create: () => new WebServer(),
}, },
cookiejar: { cookiejar: {
create: () => {}, create: () => {},
}, },
child_process: child_process child_process: child_process
}; };
var bootstrapPath = path.join(__dirname, '..', 'third_party', 'phantomjs', 'bootstrap.js'); var bootstrapPath = path.join(__dirname, '..', 'third_party', 'phantomjs', 'bootstrap.js');
var bootstrapCode = fs.readFileSync(bootstrapPath, 'utf8'); var bootstrapCode = fs.readFileSync(bootstrapPath, 'utf8');
vm.runInContext(bootstrapCode, context, { vm.runInContext(bootstrapCode, context, {
filename: 'bootstrap.js' filename: 'bootstrap.js'
})(nativeExports); })(nativeExports);
return context; return context;
} }

View File

@ -17,16 +17,16 @@
var loopWhile = require('deasync').loopWhile; var loopWhile = require('deasync').loopWhile;
module.exports = { module.exports = {
await: function(promise) { await: function(promise) {
var error; var error;
var result; var result;
var done = false; var done = false;
promise.then(r => result = r) promise.then(r => result = r)
.catch(err => error = err) .catch(err => error = err)
.then(() => done = true); .then(() => done = true);
loopWhile(() => !done); loopWhile(() => !done);
if (error) if (error)
throw error; throw error;
return result; return result;
} }
}; };

View File

@ -21,35 +21,35 @@ var path = require('path');
var mime = require('mime'); var mime = require('mime');
class StaticServer { class StaticServer {
/** /**
* @param {string} dirPath * @param {string} dirPath
* @param {number} port * @param {number} port
*/ */
constructor(dirPath, port) { constructor(dirPath, port) {
this._server = http.createServer(this._onRequest.bind(this)); this._server = http.createServer(this._onRequest.bind(this));
this._server.listen(port); this._server.listen(port);
this._dirPath = dirPath; this._dirPath = dirPath;
} }
stop() { stop() {
this._server.close(); this._server.close();
} }
_onRequest(request, response) { _onRequest(request, response) {
var pathName = url.parse(request.url).path; var pathName = url.parse(request.url).path;
if (pathName === '/') if (pathName === '/')
pathName = '/index.html'; pathName = '/index.html';
pathName = path.join(this._dirPath, pathName.substring(1)); pathName = path.join(this._dirPath, pathName.substring(1));
fs.readFile(pathName, function(err, data) { fs.readFile(pathName, function(err, data) {
if (err) { if (err) {
response.statusCode = 404; response.statusCode = 404;
response.end(`File not found: ${pathName}`); response.end(`File not found: ${pathName}`);
return; return;
} }
response.setHeader('Content-Type', mime.lookup(pathName)); response.setHeader('Content-Type', mime.lookup(pathName));
response.end(data); response.end(data);
}); });
} }
} }
module.exports = StaticServer; module.exports = StaticServer;

View File

@ -1,62 +1,62 @@
var utils = module.exports = { var utils = module.exports = {
/** /**
* @param {!Page} page * @param {!Page} page
* @param {string} frameId * @param {string} frameId
* @param {string} url * @param {string} url
* @return {!Promise} * @return {!Promise}
*/ */
attachFrame: async function(page, frameId, url) { attachFrame: async function(page, frameId, url) {
await page.evaluate(attachFrame, frameId, url); await page.evaluate(attachFrame, frameId, url);
function attachFrame(frameId, url) { function attachFrame(frameId, url) {
var frame = document.createElement('iframe'); var frame = document.createElement('iframe');
frame.src = url; frame.src = url;
frame.id = frameId; frame.id = frameId;
document.body.appendChild(frame); document.body.appendChild(frame);
return new Promise(x => frame.onload = x); return new Promise(x => frame.onload = x);
} }
}, },
/** /**
* @param {!Page} page * @param {!Page} page
* @param {string} frameId * @param {string} frameId
* @return {!Promise} * @return {!Promise}
*/ */
detachFrame: async function(page, frameId) { detachFrame: async function(page, frameId) {
await page.evaluate(detachFrame, frameId); await page.evaluate(detachFrame, frameId);
function detachFrame(frameId) { function detachFrame(frameId) {
var frame = document.getElementById(frameId); var frame = document.getElementById(frameId);
frame.remove(); frame.remove();
} }
}, },
/** /**
* @param {!Page} page * @param {!Page} page
* @param {string} frameId * @param {string} frameId
* @param {string} url * @param {string} url
* @return {!Promise} * @return {!Promise}
*/ */
navigateFrame: async function(page, frameId, url) { navigateFrame: async function(page, frameId, url) {
await page.evaluate(navigateFrame, frameId, url); await page.evaluate(navigateFrame, frameId, url);
function navigateFrame(frameId, url) { function navigateFrame(frameId, url) {
var frame = document.getElementById(frameId); var frame = document.getElementById(frameId);
frame.src = url; frame.src = url;
return new Promise(x => frame.onload = x); return new Promise(x => frame.onload = x);
} }
}, },
/** /**
* @param {!Frame} frame * @param {!Frame} frame
* @param {string=} indentation * @param {string=} indentation
* @return {string} * @return {string}
*/ */
dumpFrames: function(frame, indentation) { dumpFrames: function(frame, indentation) {
indentation = indentation || ''; indentation = indentation || '';
var result = indentation + frame.url(); var result = indentation + frame.url();
for (var child of frame.childFrames()) for (var child of frame.childFrames())
result += '\n' + utils.dumpFrames(child, ' ' + indentation); result += '\n' + utils.dumpFrames(child, ' ' + indentation);
return result; return result;
}, },
}; };

View File

@ -25,19 +25,19 @@ var GOLDEN_DIR = path.join(__dirname, 'golden');
var OUTPUT_DIR = path.join(__dirname, 'output'); var OUTPUT_DIR = path.join(__dirname, 'output');
module.exports = { module.exports = {
addMatchers: function(jasmine) { addMatchers: function(jasmine) {
jasmine.addMatchers(customMatchers); jasmine.addMatchers(customMatchers);
}, },
removeOutputDir: function() { removeOutputDir: function() {
if (fs.existsSync(OUTPUT_DIR)) if (fs.existsSync(OUTPUT_DIR))
rm(OUTPUT_DIR); rm(OUTPUT_DIR);
}, },
}; };
var GoldenComparators = { var GoldenComparators = {
'image/png': compareImages, 'image/png': compareImages,
'text/plain': compareText 'text/plain': compareText
}; };
/** /**
@ -46,19 +46,19 @@ var GoldenComparators = {
* @return {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} * @return {?{diff: (!Object:undefined), errorMessage: (string|undefined)}}
*/ */
function compareImages(actualBuffer, expectedBuffer) { function compareImages(actualBuffer, expectedBuffer) {
if (!actualBuffer || !(actualBuffer instanceof Buffer)) if (!actualBuffer || !(actualBuffer instanceof Buffer))
return { errorMessage: 'Actual result should be Buffer.' }; return { errorMessage: 'Actual result should be Buffer.' };
var actual = PNG.sync.read(actualBuffer); var actual = PNG.sync.read(actualBuffer);
var expected = PNG.sync.read(expectedBuffer); var expected = PNG.sync.read(expectedBuffer);
if (expected.width !== actual.width || expected.height !== actual.height) { if (expected.width !== actual.width || expected.height !== actual.height) {
return { return {
errorMessage: `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. ` errorMessage: `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. `
}; };
} }
var diff = new PNG({width: expected.width, height: expected.height}); var diff = new PNG({width: expected.width, height: expected.height});
var count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, {threshold: 0.1}); var count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, {threshold: 0.1});
return count > 0 ? { diff: PNG.sync.write(diff) } : null; return count > 0 ? { diff: PNG.sync.write(diff) } : null;
} }
/** /**
@ -67,80 +67,80 @@ function compareImages(actualBuffer, expectedBuffer) {
* @return {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} * @return {?{diff: (!Object:undefined), errorMessage: (string|undefined)}}
*/ */
function compareText(actual, expectedBuffer) { function compareText(actual, expectedBuffer) {
if (typeof actual !== 'string') if (typeof actual !== 'string')
return { errorMessage: 'Actual result should be string' }; return { errorMessage: 'Actual result should be string' };
var expected = expectedBuffer.toString('utf-8'); var expected = expectedBuffer.toString('utf-8');
if (expected === actual) if (expected === actual)
return null; return null;
var diff = new Diff(); var diff = new Diff();
var result = diff.main(expected, actual); var result = diff.main(expected, actual);
diff.cleanupSemantic(result); diff.cleanupSemantic(result);
var html = diff.prettyHtml(result); var html = diff.prettyHtml(result);
var diffStylePath = path.join(__dirname, 'diffstyle.css'); var diffStylePath = path.join(__dirname, 'diffstyle.css');
html = `<link rel="stylesheet" href="file://${diffStylePath}">` + html; html = `<link rel="stylesheet" href="file://${diffStylePath}">` + html;
return { return {
diff: html, diff: html,
diffExtension: '.html' diffExtension: '.html'
}; };
} }
var customMatchers = { var customMatchers = {
toBeGolden: function(util, customEqualityTesters) { toBeGolden: function(util, customEqualityTesters) {
return {
/**
* @param {?Object} actual
* @param {string} goldenName
* @return {!{pass: boolean, message: (undefined|string)}}
*/
compare: function(actual, goldenName) {
var expectedPath = path.join(GOLDEN_DIR, goldenName);
var actualPath = path.join(OUTPUT_DIR, goldenName);
var messageSuffix = 'Output is saved in "' + path.basename(OUTPUT_DIR + '" directory');
if (!fs.existsSync(expectedPath)) {
ensureOutputDir();
fs.writeFileSync(actualPath, actual);
return {
pass: false,
message: goldenName + ' is missing in golden results. ' + messageSuffix
};
}
var expected = fs.readFileSync(expectedPath);
var comparator = GoldenComparators[mime.lookup(goldenName)];
if (!comparator) {
return {
pass: false,
message: 'Failed to find comparator with type ' + mime.lookup(goldenName) + ': ' + goldenName
};
}
var result = comparator(actual, expected);
if (!result)
return { pass: true };
ensureOutputDir();
fs.writeFileSync(actualPath, actual);
// Copy expected to the output/ folder for convenience.
fs.writeFileSync(addSuffix(actualPath, '-expected'), expected);
if (result.diff) {
var diffPath = addSuffix(actualPath, '-diff', result.diffExtension);
fs.writeFileSync(diffPath, result.diff);
}
var message = goldenName + ' mismatch!';
if (result.errorMessage)
message += ' ' + result.errorMessage;
return { return {
/** pass: false,
* @param {?Object} actual message: message + ' ' + messageSuffix
* @param {string} goldenName
* @return {!{pass: boolean, message: (undefined|string)}}
*/
compare: function(actual, goldenName) {
var expectedPath = path.join(GOLDEN_DIR, goldenName);
var actualPath = path.join(OUTPUT_DIR, goldenName);
var messageSuffix = 'Output is saved in "' + path.basename(OUTPUT_DIR + '" directory');
if (!fs.existsSync(expectedPath)) {
ensureOutputDir();
fs.writeFileSync(actualPath, actual);
return {
pass: false,
message: goldenName + ' is missing in golden results. ' + messageSuffix
};
}
var expected = fs.readFileSync(expectedPath);
var comparator = GoldenComparators[mime.lookup(goldenName)];
if (!comparator) {
return {
pass: false,
message: 'Failed to find comparator with type ' + mime.lookup(goldenName) + ': ' + goldenName
};
}
var result = comparator(actual, expected);
if (!result)
return { pass: true };
ensureOutputDir();
fs.writeFileSync(actualPath, actual);
// Copy expected to the output/ folder for convenience.
fs.writeFileSync(addSuffix(actualPath, '-expected'), expected);
if (result.diff) {
var diffPath = addSuffix(actualPath, '-diff', result.diffExtension);
fs.writeFileSync(diffPath, result.diff);
}
var message = goldenName + ' mismatch!';
if (result.errorMessage)
message += ' ' + result.errorMessage;
return {
pass: false,
message: message + ' ' + messageSuffix
};
function ensureOutputDir() {
if (!fs.existsSync(OUTPUT_DIR))
fs.mkdirSync(OUTPUT_DIR);
}
}
}; };
},
function ensureOutputDir() {
if (!fs.existsSync(OUTPUT_DIR))
fs.mkdirSync(OUTPUT_DIR);
}
}
};
},
}; };
/** /**
@ -150,8 +150,8 @@ var customMatchers = {
* @return {string} * @return {string}
*/ */
function addSuffix(filePath, suffix, customExtension) { function addSuffix(filePath, suffix, customExtension) {
var dirname = path.dirname(filePath); var dirname = path.dirname(filePath);
var ext = path.extname(filePath); var ext = path.extname(filePath);
var name = path.basename(filePath, ext); var name = path.basename(filePath, ext);
return path.join(dirname, name + suffix + (customExtension || ext)); return path.join(dirname, name + suffix + (customExtension || ext));
} }

View File

@ -26,308 +26,308 @@ var EMPTY_PAGE = STATIC_PREFIX + '/empty.html';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 1000;
describe('Puppeteer', function() { describe('Puppeteer', function() {
var browser; var browser;
var staticServer; var staticServer;
var page; var page;
beforeAll(function() { beforeAll(function() {
browser = new Browser({args: ['--no-sandbox']}); browser = new Browser({args: ['--no-sandbox']});
staticServer = new StaticServer(path.join(__dirname, 'assets'), PORT); staticServer = new StaticServer(path.join(__dirname, 'assets'), PORT);
GoldenUtils.removeOutputDir(); GoldenUtils.removeOutputDir();
}); });
afterAll(function() { afterAll(function() {
browser.close(); browser.close();
staticServer.stop(); staticServer.stop();
}); });
beforeEach(SX(async function() { beforeEach(SX(async function() {
page = await browser.newPage(); page = await browser.newPage();
GoldenUtils.addMatchers(jasmine); GoldenUtils.addMatchers(jasmine);
}));
afterEach(function() {
page.close();
});
describe('Page.evaluate', function() {
it('should work', SX(async function() {
var result = await page.evaluate(() => 7 * 3);
expect(result).toBe(21);
})); }));
it('should await promise', SX(async function() {
afterEach(function() { var result = await page.evaluate(() => Promise.resolve(8 * 7));
page.close(); expect(result).toBe(56);
});
describe('Page.evaluate', function() {
it('should work', SX(async function() {
var result = await page.evaluate(() => 7 * 3);
expect(result).toBe(21);
}));
it('should await promise', SX(async function() {
var result = await page.evaluate(() => Promise.resolve(8 * 7));
expect(result).toBe(56);
}));
it('should work from-inside inPageCallback', SX(async function() {
// Setup inpage callback, which calls Page.evaluate
await page.setInPageCallback('callController', async function(a, b) {
return await page.evaluate((a, b) => a * b, a, b);
});
var result = await page.evaluate(async function() {
return await callController(9, 3);
});
expect(result).toBe(27);
}));
it('should reject promise with exception', SX(async function() {
var error = null;
try {
await page.evaluate(() => not.existing.object.property);
} catch (e) {
error = e;
}
expect(error).toBeTruthy();
expect(error.message).toContain('not is not defined');
}));
});
it('Page Events: ConsoleMessage', SX(async function() {
var msgs = [];
page.on('consolemessage', msg => msgs.push(msg));
await page.evaluate(() => console.log('Message!'));
expect(msgs).toEqual(['Message!']);
})); }));
it('should work from-inside inPageCallback', SX(async function() {
// Setup inpage callback, which calls Page.evaluate
await page.setInPageCallback('callController', async function(a, b) {
return await page.evaluate((a, b) => a * b, a, b);
});
var result = await page.evaluate(async function() {
return await callController(9, 3);
});
expect(result).toBe(27);
}));
it('should reject promise with exception', SX(async function() {
var error = null;
try {
await page.evaluate(() => not.existing.object.property);
} catch (e) {
error = e;
}
expect(error).toBeTruthy();
expect(error.message).toContain('not is not defined');
}));
});
describe('Page.navigate', function() { it('Page Events: ConsoleMessage', SX(async function() {
it('should fail when navigating to bad url', SX(async function() { var msgs = [];
var success = await page.navigate('asdfasdf'); page.on('consolemessage', msg => msgs.push(msg));
expect(success).toBe(false); await page.evaluate(() => console.log('Message!'));
})); expect(msgs).toEqual(['Message!']);
it('should succeed when navigating to good url', SX(async function() { }));
var success = await page.navigate(EMPTY_PAGE);
expect(success).toBe(true); describe('Page.navigate', function() {
})); it('should fail when navigating to bad url', SX(async function() {
var success = await page.navigate('asdfasdf');
expect(success).toBe(false);
}));
it('should succeed when navigating to good url', SX(async function() {
var success = await page.navigate(EMPTY_PAGE);
expect(success).toBe(true);
}));
});
describe('Page.setInPageCallback', function() {
it('should work', SX(async function() {
await page.setInPageCallback('callController', function(a, b) {
return a * b;
});
var result = await page.evaluate(async function() {
return await callController(9, 4);
});
expect(result).toBe(36);
}));
it('should survive navigation', SX(async function() {
await page.setInPageCallback('callController', function(a, b) {
return a * b;
});
await page.navigate(EMPTY_PAGE);
var result = await page.evaluate(async function() {
return await callController(9, 4);
});
expect(result).toBe(36);
}));
it('should await returned promise', SX(async function() {
await page.setInPageCallback('callController', function(a, b) {
return Promise.resolve(a * b);
});
var result = await page.evaluate(async function() {
return await callController(3, 5);
});
expect(result).toBe(15);
}));
});
describe('Page.setRequestInterceptor', function() {
it('should intercept', SX(async function() {
page.setRequestInterceptor(request => {
expect(request.url()).toContain('empty.html');
expect(request.headers()['User-Agent']).toBeTruthy();
expect(request.method()).toBe('GET');
expect(request.postData()).toBe(undefined);
request.continue();
});
var success = await page.navigate(EMPTY_PAGE);
expect(success).toBe(true);
}));
it('should show extraHTTPHeaders', SX(async function() {
await page.setExtraHTTPHeaders({
foo: 'bar'
});
page.setRequestInterceptor(request => {
expect(request.headers()['foo']).toBe('bar');
request.continue();
});
var success = await page.navigate(EMPTY_PAGE);
expect(success).toBe(true);
}));
it('should be abortable', SX(async function() {
page.setRequestInterceptor(request => {
if (request.url().endsWith('.css'))
request.abort();
else
request.continue();
});
var failedResources = 0;
page.on('resourceloadingfailed', event => ++failedResources);
var success = await page.navigate(STATIC_PREFIX + '/one-style.html');
expect(success).toBe(true);
expect(failedResources).toBe(1);
}));
});
describe('Page.Events.Dialog', function() {
it('should fire', function(done) {
page.on('dialog', dialog => {
expect(dialog.type).toBe('alert');
expect(dialog.message()).toBe('yo');
done();
});
page.evaluate(() => alert('yo'));
}); });
// TODO Enable this when crbug.com/718235 is fixed.
xit('should allow accepting prompts', SX(async function(done) {
page.on('dialog', dialog => {
expect(dialog.type).toBe('prompt');
expect(dialog.message()).toBe('question?');
dialog.accept('answer!');
});
var result = await page.evaluate(() => prompt('question?'));
expect(result).toBe('answer!');
}));
});
describe('Page.setInPageCallback', function() { describe('Page.Events.Error', function() {
it('should work', SX(async function() { it('should fire', function(done) {
await page.setInPageCallback('callController', function(a, b) { page.on('error', error => {
return a * b; expect(error.message).toContain('Fancy');
}); done();
var result = await page.evaluate(async function() { });
return await callController(9, 4); page.navigate(STATIC_PREFIX + '/error.html');
});
expect(result).toBe(36);
}));
it('should survive navigation', SX(async function() {
await page.setInPageCallback('callController', function(a, b) {
return a * b;
});
await page.navigate(EMPTY_PAGE);
var result = await page.evaluate(async function() {
return await callController(9, 4);
});
expect(result).toBe(36);
}));
it('should await returned promise', SX(async function() {
await page.setInPageCallback('callController', function(a, b) {
return Promise.resolve(a * b);
});
var result = await page.evaluate(async function() {
return await callController(3, 5);
});
expect(result).toBe(15);
}));
}); });
});
describe('Page.setRequestInterceptor', function() { describe('Page.screenshot', function() {
it('should intercept', SX(async function() { it('should work', SX(async function() {
page.setRequestInterceptor(request => { await page.setViewportSize({width: 500, height: 500});
expect(request.url()).toContain('empty.html'); await page.navigate(STATIC_PREFIX + '/grid.html');
expect(request.headers()['User-Agent']).toBeTruthy(); var screenshot = await page.screenshot();
expect(request.method()).toBe('GET'); expect(screenshot).toBeGolden('screenshot-sanity.png');
expect(request.postData()).toBe(undefined); }));
request.continue(); it('should clip rect', SX(async function() {
}); await page.setViewportSize({width: 500, height: 500});
var success = await page.navigate(EMPTY_PAGE); await page.navigate(STATIC_PREFIX + '/grid.html');
expect(success).toBe(true); var screenshot = await page.screenshot({
clip: {
x: 50,
y: 100,
width: 150,
height: 100
}
});
expect(screenshot).toBeGolden('screenshot-clip-rect.png');
}));
it('should work for offscreen clip', SX(async function() {
await page.setViewportSize({width: 500, height: 500});
await page.navigate(STATIC_PREFIX + '/grid.html');
var screenshot = await page.screenshot({
clip: {
x: 50,
y: 600,
width: 100,
height: 100
}
});
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
}));
it('should run in parallel', SX(async function() {
await page.setViewportSize({width: 500, height: 500});
await page.navigate(STATIC_PREFIX + '/grid.html');
var promises = [];
for (var i = 0; i < 3; ++i) {
promises.push(page.screenshot({
clip: {
x: 50 * i,
y: 0,
width: 50,
height: 50
}
})); }));
it('should show extraHTTPHeaders', SX(async function() { }
await page.setExtraHTTPHeaders({ var screenshot = await promises[1];
foo: 'bar' expect(screenshot).toBeGolden('screenshot-parallel-calls.png');
}); }));
page.setRequestInterceptor(request => { it('should take fullPage screenshots', SX(async function() {
expect(request.headers()['foo']).toBe('bar'); await page.setViewportSize({width: 500, height: 500});
request.continue(); await page.navigate(STATIC_PREFIX + '/grid.html');
}); var screenshot = await page.screenshot({
var success = await page.navigate(EMPTY_PAGE); fullPage: true
expect(success).toBe(true); });
})); expect(screenshot).toBeGolden('screenshot-grid-fullpage.png');
it('should be abortable', SX(async function() { }));
page.setRequestInterceptor(request => { });
if (request.url().endsWith('.css'))
request.abort();
else
request.continue();
});
var failedResources = 0;
page.on('resourceloadingfailed', event => ++failedResources);
var success = await page.navigate(STATIC_PREFIX + '/one-style.html');
expect(success).toBe(true);
expect(failedResources).toBe(1);
}));
});
describe('Page.Events.Dialog', function() { describe('Frame Management', function() {
it('should fire', function(done) { var FrameUtils = require('./frame-utils');
page.on('dialog', dialog => { it('should handle nested frames', SX(async function() {
expect(dialog.type).toBe('alert'); await page.navigate(STATIC_PREFIX + '/frames/nested-frames.html');
expect(dialog.message()).toBe('yo'); expect(FrameUtils.dumpFrames(page.mainFrame())).toBeGolden('nested-frames.txt');
done(); }));
}); it('should send events when frames are manipulated dynamically', SX(async function() {
page.evaluate(() => alert('yo')); await page.navigate(EMPTY_PAGE);
}); // validate frameattached events
// TODO Enable this when crbug.com/718235 is fixed. var attachedFrames = [];
xit('should allow accepting prompts', SX(async function(done) { page.on('frameattached', frame => attachedFrames.push(frame));
page.on('dialog', dialog => { await FrameUtils.attachFrame(page, 'frame1', './assets/frame.html');
expect(dialog.type).toBe('prompt'); expect(attachedFrames.length).toBe(1);
expect(dialog.message()).toBe('question?'); expect(attachedFrames[0].url()).toContain('/assets/frame.html');
dialog.accept('answer!');
});
var result = await page.evaluate(() => prompt('question?'));
expect(result).toBe('answer!');
}));
});
describe('Page.Events.Error', function() { // validate framenavigated events
it('should fire', function(done) { var navigatedFrames = [];
page.on('error', error => { page.on('framenavigated', frame => navigatedFrames.push(frame));
expect(error.message).toContain('Fancy'); await FrameUtils.navigateFrame(page, 'frame1', './empty.html');
done(); expect(navigatedFrames.length).toBe(1);
}); expect(navigatedFrames[0].url()).toContain('/empty.html');
page.navigate(STATIC_PREFIX + '/error.html');
});
});
describe('Page.screenshot', function() { // validate framedetached events
it('should work', SX(async function() { var detachedFrames = [];
await page.setViewportSize({width: 500, height: 500}); page.on('framedetached', frame => detachedFrames.push(frame));
await page.navigate(STATIC_PREFIX + '/grid.html'); await FrameUtils.detachFrame(page, 'frame1');
var screenshot = await page.screenshot(); expect(detachedFrames.length).toBe(1);
expect(screenshot).toBeGolden('screenshot-sanity.png'); expect(detachedFrames[0].isDetached()).toBe(true);
})); }));
it('should clip rect', SX(async function() { it('should persist mainFrame on cross-process navigation', SX(async function() {
await page.setViewportSize({width: 500, height: 500}); await page.navigate(EMPTY_PAGE);
await page.navigate(STATIC_PREFIX + '/grid.html'); var mainFrame = page.mainFrame();
var screenshot = await page.screenshot({ await page.navigate('http://127.0.0.1:' + PORT + '/empty.html');
clip: { expect(page.mainFrame() === mainFrame).toBeTruthy();
x: 50, }));
y: 100, it('should not send attach/detach events for main frame', SX(async function() {
width: 150, var hasEvents = false;
height: 100 page.on('frameattached', frame => hasEvents = true);
} page.on('framedetached', frame => hasEvents = true);
}); await page.navigate(EMPTY_PAGE);
expect(screenshot).toBeGolden('screenshot-clip-rect.png'); expect(hasEvents).toBe(false);
})); }));
it('should work for offscreen clip', SX(async function() { it('should detach child frames on navigation', SX(async function() {
await page.setViewportSize({width: 500, height: 500}); var attachedFrames = [];
await page.navigate(STATIC_PREFIX + '/grid.html'); var detachedFrames = [];
var screenshot = await page.screenshot({ var navigatedFrames = [];
clip: { page.on('frameattached', frame => attachedFrames.push(frame));
x: 50, page.on('framedetached', frame => detachedFrames.push(frame));
y: 600, page.on('framenavigated', frame => navigatedFrames.push(frame));
width: 100, await page.navigate(STATIC_PREFIX + '/frames/nested-frames.html');
height: 100 expect(attachedFrames.length).toBe(4);
} expect(detachedFrames.length).toBe(0);
}); expect(navigatedFrames.length).toBe(5);
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
}));
it('should run in parallel', SX(async function() {
await page.setViewportSize({width: 500, height: 500});
await page.navigate(STATIC_PREFIX + '/grid.html');
var promises = [];
for (var i = 0; i < 3; ++i) {
promises.push(page.screenshot({
clip: {
x: 50 * i,
y: 0,
width: 50,
height: 50
}
}));
}
var screenshot = await promises[1];
expect(screenshot).toBeGolden('screenshot-parallel-calls.png');
}));
it('should take fullPage screenshots', SX(async function() {
await page.setViewportSize({width: 500, height: 500});
await page.navigate(STATIC_PREFIX + '/grid.html');
var screenshot = await page.screenshot({
fullPage: true
});
expect(screenshot).toBeGolden('screenshot-grid-fullpage.png');
}));
});
describe('Frame Management', function() { var attachedFrames = [];
var FrameUtils = require('./frame-utils'); var detachedFrames = [];
it('should handle nested frames', SX(async function() { var navigatedFrames = [];
await page.navigate(STATIC_PREFIX + '/frames/nested-frames.html'); await page.navigate(EMPTY_PAGE);
expect(FrameUtils.dumpFrames(page.mainFrame())).toBeGolden('nested-frames.txt'); expect(attachedFrames.length).toBe(0);
})); expect(detachedFrames.length).toBe(4);
it('should send events when frames are manipulated dynamically', SX(async function() { expect(navigatedFrames.length).toBe(1);
await page.navigate(EMPTY_PAGE); }));
// validate frameattached events });
var attachedFrames = [];
page.on('frameattached', frame => attachedFrames.push(frame));
await FrameUtils.attachFrame(page, 'frame1', './assets/frame.html');
expect(attachedFrames.length).toBe(1);
expect(attachedFrames[0].url()).toContain('/assets/frame.html');
// validate framenavigated events
var navigatedFrames = [];
page.on('framenavigated', frame => navigatedFrames.push(frame));
await FrameUtils.navigateFrame(page, 'frame1', './empty.html');
expect(navigatedFrames.length).toBe(1);
expect(navigatedFrames[0].url()).toContain('/empty.html');
// validate framedetached events
var detachedFrames = [];
page.on('framedetached', frame => detachedFrames.push(frame));
await FrameUtils.detachFrame(page, 'frame1');
expect(detachedFrames.length).toBe(1);
expect(detachedFrames[0].isDetached()).toBe(true);
}));
it('should persist mainFrame on cross-process navigation', SX(async function() {
await page.navigate(EMPTY_PAGE);
var mainFrame = page.mainFrame();
await page.navigate('http://127.0.0.1:' + PORT + '/empty.html');
expect(page.mainFrame() === mainFrame).toBeTruthy();
}));
it('should not send attach/detach events for main frame', SX(async function() {
var hasEvents = false;
page.on('frameattached', frame => hasEvents = true);
page.on('framedetached', frame => hasEvents = true);
await page.navigate(EMPTY_PAGE);
expect(hasEvents).toBe(false);
}));
it('should detach child frames on navigation', SX(async function() {
var attachedFrames = [];
var detachedFrames = [];
var navigatedFrames = [];
page.on('frameattached', frame => attachedFrames.push(frame));
page.on('framedetached', frame => detachedFrames.push(frame));
page.on('framenavigated', frame => navigatedFrames.push(frame));
await page.navigate(STATIC_PREFIX + '/frames/nested-frames.html');
expect(attachedFrames.length).toBe(4);
expect(detachedFrames.length).toBe(0);
expect(navigatedFrames.length).toBe(5);
var attachedFrames = [];
var detachedFrames = [];
var navigatedFrames = [];
await page.navigate(EMPTY_PAGE);
expect(attachedFrames.length).toBe(0);
expect(detachedFrames.length).toBe(4);
expect(navigatedFrames.length).toBe(1);
}));
});
}); });
// Since Jasmine doesn't like async functions, they should be wrapped // Since Jasmine doesn't like async functions, they should be wrapped
// in a SX function. // in a SX function.
function SX(fun) { function SX(fun) {
return done => Promise.resolve(fun()).then(done).catch(done.fail); return done => Promise.resolve(fun()).then(done).catch(done.fail);
} }

View File

@ -25,107 +25,107 @@ var URL = require('url');
var CHROMIUM_PATH = path.join(__dirname, '..', '.local-chromium'); var CHROMIUM_PATH = path.join(__dirname, '..', '.local-chromium');
var downloadURLs = { var downloadURLs = {
linux: 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip', linux: 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip',
mac: 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/%d/chrome-mac.zip', mac: 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/%d/chrome-mac.zip',
win32: 'https://storage.googleapis.com/chromium-browser-snapshots/Win/%d/chrome-win32.zip', win32: 'https://storage.googleapis.com/chromium-browser-snapshots/Win/%d/chrome-win32.zip',
win64: 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip', win64: 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip',
}; };
module.exports = { module.exports = {
/** /**
* @return {!Array<string>} * @return {!Array<string>}
*/ */
supportedPlatforms: function() { supportedPlatforms: function() {
return Object.keys(downloadURLs); return Object.keys(downloadURLs);
}, },
/** /**
* @return {string} * @return {string}
*/ */
currentPlatform: function() { currentPlatform: function() {
var platform = os.platform(); var platform = os.platform();
if (platform === 'darwin') if (platform === 'darwin')
return 'mac'; return 'mac';
if (platform === 'linux') if (platform === 'linux')
return 'linux'; return 'linux';
if (platform === 'win32') if (platform === 'win32')
return os.arch() === 'x64' ? 'win64' : 'win32'; return os.arch() === 'x64' ? 'win64' : 'win32';
return ''; return '';
}, },
/** /**
* @param {string} platform * @param {string} platform
* @param {string} revision * @param {string} revision
* @return {!Promise<boolean>} * @return {!Promise<boolean>}
*/ */
canDownloadRevision: function(platform, revision) { canDownloadRevision: function(platform, revision) {
console.assert(downloadURLs[platform], 'Unknown platform: ' + platform); console.assert(downloadURLs[platform], 'Unknown platform: ' + platform);
var url = URL.parse(util.format(downloadURLs[platform], revision)); var url = URL.parse(util.format(downloadURLs[platform], revision));
var options = { var options = {
method: 'HEAD', method: 'HEAD',
host: url.host, host: url.host,
path: url.pathname, path: url.pathname,
}; };
var resolve; var resolve;
var promise = new Promise(x => resolve = x); var promise = new Promise(x => resolve = x);
var request = https.request(options, response => { var request = https.request(options, response => {
resolve(response.statusCode === 200); resolve(response.statusCode === 200);
}); });
request.on('error', error => { request.on('error', error => {
console.error(error); console.error(error);
resolve(false); resolve(false);
}); });
request.end(); request.end();
return promise; return promise;
}, },
/** /**
* @param {string} platform * @param {string} platform
* @param {string} revision * @param {string} revision
* @param {?function(number, number)} progressCallback * @param {?function(number, number)} progressCallback
* @return {!Promise} * @return {!Promise}
*/ */
downloadRevision: async function(platform, revision, progressCallback) { downloadRevision: async function(platform, revision, progressCallback) {
var url = downloadURLs[platform]; var url = downloadURLs[platform];
console.assert(url, `Unsupported platform: ${platform}`); console.assert(url, `Unsupported platform: ${platform}`);
url = util.format(url, revision); url = util.format(url, revision);
var zipPath = path.join(CHROMIUM_PATH, `download-${platform}-${revision}.zip`); var zipPath = path.join(CHROMIUM_PATH, `download-${platform}-${revision}.zip`);
var folderPath = getFolderPath(platform, revision); var folderPath = getFolderPath(platform, revision);
if (fs.existsSync(folderPath)) if (fs.existsSync(folderPath))
return; return;
try { try {
if (!fs.existsSync(CHROMIUM_PATH)) if (!fs.existsSync(CHROMIUM_PATH))
fs.mkdirSync(CHROMIUM_PATH); fs.mkdirSync(CHROMIUM_PATH);
await downloadFile(url, zipPath, progressCallback); await downloadFile(url, zipPath, progressCallback);
await extractZip(zipPath, folderPath); await extractZip(zipPath, folderPath);
} finally { } finally {
if (fs.existsSync(zipPath)) if (fs.existsSync(zipPath))
fs.unlinkSync(zipPath); fs.unlinkSync(zipPath);
} }
}, },
/** /**
* @param {string} platform * @param {string} platform
* @param {string} revision * @param {string} revision
* @return {?{executablePath: string}} * @return {?{executablePath: string}}
*/ */
revisionInfo: function(platform, revision) { revisionInfo: function(platform, revision) {
var folderPath = getFolderPath(platform, revision); var folderPath = getFolderPath(platform, revision);
if (!fs.existsSync(folderPath)) if (!fs.existsSync(folderPath))
return null; return null;
var executablePath = ''; var executablePath = '';
if (platform === 'mac') if (platform === 'mac')
executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
else if (platform === 'linux') else if (platform === 'linux')
executablePath = path.join(folderPath, 'chrome-linux', 'chrome'); executablePath = path.join(folderPath, 'chrome-linux', 'chrome');
else if (platform === 'win32' || platform === 'win64') else if (platform === 'win32' || platform === 'win64')
executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe'); executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe');
else else
throw 'Unsupported platfrom: ' + platfrom; throw 'Unsupported platfrom: ' + platfrom;
return { return {
executablePath: executablePath executablePath: executablePath
}; };
}, },
}; };
/** /**
@ -134,7 +134,7 @@ module.exports = {
* @return {string} * @return {string}
*/ */
function getFolderPath(platform, revision) { function getFolderPath(platform, revision) {
return path.join(CHROMIUM_PATH, platform + '-' + revision); return path.join(CHROMIUM_PATH, platform + '-' + revision);
} }
/** /**
@ -144,30 +144,30 @@ function getFolderPath(platform, revision) {
* @return {!Promise} * @return {!Promise}
*/ */
function downloadFile(url, destinationPath, progressCallback) { function downloadFile(url, destinationPath, progressCallback) {
var fulfill, reject; var fulfill, reject;
var promise = new Promise((x, y) => { fulfill = x; reject = y; }); var promise = new Promise((x, y) => { fulfill = x; reject = y; });
var request = https.get(url, response => { var request = https.get(url, response => {
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
var error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`); var error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
// consume response data to free up memory // consume response data to free up memory
response.resume(); response.resume();
reject(error); reject(error);
return; return;
}
var file = fs.createWriteStream(destinationPath);
file.on('finish', () => fulfill());
file.on('error', error => reject(error));
response.pipe(file);
var totalBytes = parseInt(response.headers['content-length'], 10);
if (progressCallback)
response.on('data', onData.bind(null, totalBytes));
});
request.on('error', error => reject(error));
return promise;
function onData(totalBytes, chunk) {
progressCallback(totalBytes, chunk.length);
} }
var file = fs.createWriteStream(destinationPath);
file.on('finish', () => fulfill());
file.on('error', error => reject(error));
response.pipe(file);
var totalBytes = parseInt(response.headers['content-length'], 10);
if (progressCallback)
response.on('data', onData.bind(null, totalBytes));
});
request.on('error', error => reject(error));
return promise;
function onData(totalBytes, chunk) {
progressCallback(totalBytes, chunk.length);
}
} }
/** /**
@ -176,5 +176,5 @@ function downloadFile(url, destinationPath, progressCallback) {
* @return {!Promise<?Error>} * @return {!Promise<?Error>}
*/ */
function extractZip(zipPath, folderPath) { function extractZip(zipPath, folderPath) {
return new Promise(fulfill => extract(zipPath, {dir: folderPath}, fulfill)); return new Promise(fulfill => extract(zipPath, {dir: folderPath}, fulfill));
} }

View File

@ -20,43 +20,43 @@ var https = require('https');
var OMAHA_PROXY = 'https://omahaproxy.appspot.com/all.json'; var OMAHA_PROXY = 'https://omahaproxy.appspot.com/all.json';
var colors = { var colors = {
reset: '\x1b[0m', reset: '\x1b[0m',
red: '\x1b[31m', red: '\x1b[31m',
green: '\x1b[32m', green: '\x1b[32m',
yellow: '\x1b[33m' yellow: '\x1b[33m'
}; };
class Table { class Table {
/** /**
* @param {!Array<number>} columnWidths * @param {!Array<number>} columnWidths
*/ */
constructor(columnWidths) { constructor(columnWidths) {
this.widths = columnWidths; this.widths = columnWidths;
} }
/** /**
* @param {!Array<string>} values * @param {!Array<string>} values
*/ */
drawRow(values) { drawRow(values) {
console.assert(values.length === this.widths.length); console.assert(values.length === this.widths.length);
var row = ''; var row = '';
for (var i = 0; i < values.length; ++i) for (var i = 0; i < values.length; ++i)
row += padCenter(values[i], this.widths[i]); row += padCenter(values[i], this.widths[i]);
console.log(row); console.log(row);
} }
} }
if (process.argv.length === 2) { if (process.argv.length === 2) {
checkOmahaProxyAvailability(); checkOmahaProxyAvailability();
return; return;
} }
if (process.argv.length !== 4) { if (process.argv.length !== 4) {
console.log(` console.log(`
Usage: node check_revisions.js [fromRevision] [toRevision] Usage: node check_revisions.js [fromRevision] [toRevision]
This script checks availability of different prebuild chromium revisions. This script checks availability of different prebuild chromium revisions.
Running command without arguments will check against omahaproxy revisions.`); Running command without arguments will check against omahaproxy revisions.`);
return; return;
} }
var fromRevision = parseInt(process.argv[2], 10); var fromRevision = parseInt(process.argv[2], 10);
@ -67,27 +67,27 @@ checkRangeAvailability(fromRevision, toRevision);
* @return {!Promise} * @return {!Promise}
*/ */
async function checkOmahaProxyAvailability() { async function checkOmahaProxyAvailability() {
console.log('Fetching revisions from ' + OMAHA_PROXY); console.log('Fetching revisions from ' + OMAHA_PROXY);
var platforms = await loadJSON(OMAHA_PROXY); var platforms = await loadJSON(OMAHA_PROXY);
if (!platforms) { if (!platforms) {
console.error('ERROR: failed to fetch chromium revisions from omahaproxy.'); console.error('ERROR: failed to fetch chromium revisions from omahaproxy.');
return; return;
} }
var table = new Table([27, 7, 7, 7, 7]); var table = new Table([27, 7, 7, 7, 7]);
table.drawRow([''].concat(Downloader.supportedPlatforms())); table.drawRow([''].concat(Downloader.supportedPlatforms()));
for (var platform of platforms) { for (var platform of platforms) {
// Trust only to the main platforms. // Trust only to the main platforms.
if (platform.os !== 'mac' && platform.os !== 'win' && platform.os !== 'win64' && platform.os !== 'linux') if (platform.os !== 'mac' && platform.os !== 'win' && platform.os !== 'win64' && platform.os !== 'linux')
continue; continue;
var osName = platform.os === 'win' ? 'win32' : platform.os; var osName = platform.os === 'win' ? 'win32' : platform.os;
for (var version of platform.versions) { for (var version of platform.versions) {
if (version.channel !== 'dev' && version.channel !== 'beta' && version.channel !== 'canary' && version.channel !== 'stable') if (version.channel !== 'dev' && version.channel !== 'beta' && version.channel !== 'canary' && version.channel !== 'stable')
continue; continue;
var revisionName = padLeft('[' + osName + ' ' + version.channel + ']', 15); var revisionName = padLeft('[' + osName + ' ' + version.channel + ']', 15);
var revision = parseInt(version.branch_base_position, 10); var revision = parseInt(version.branch_base_position, 10);
await checkAndDrawRevisionAvailability(table, revisionName, revision); await checkAndDrawRevisionAvailability(table, revisionName, revision);
}
} }
}
} }
/** /**
@ -96,11 +96,11 @@ async function checkOmahaProxyAvailability() {
* @return {!Promise} * @return {!Promise}
*/ */
async function checkRangeAvailability(fromRevision, toRevision) { async function checkRangeAvailability(fromRevision, toRevision) {
var table = new Table([10, 7, 7, 7, 7]); var table = new Table([10, 7, 7, 7, 7]);
table.drawRow([''].concat(Downloader.supportedPlatforms())); table.drawRow([''].concat(Downloader.supportedPlatforms()));
var inc = fromRevision < toRevision ? 1 : -1; var inc = fromRevision < toRevision ? 1 : -1;
for (var revision = fromRevision; revision !== toRevision; revision += inc) for (var revision = fromRevision; revision !== toRevision; revision += inc)
await checkAndDrawRevisionAvailability(table, '', revision); await checkAndDrawRevisionAvailability(table, '', revision);
} }
/** /**
@ -110,18 +110,18 @@ async function checkRangeAvailability(fromRevision, toRevision) {
* @return {!Promise} * @return {!Promise}
*/ */
async function checkAndDrawRevisionAvailability(table, name, revision) { async function checkAndDrawRevisionAvailability(table, name, revision) {
var promises = []; var promises = [];
for (var platform of Downloader.supportedPlatforms()) for (var platform of Downloader.supportedPlatforms())
promises.push(Downloader.canDownloadRevision(platform, revision)); promises.push(Downloader.canDownloadRevision(platform, revision));
var availability = await Promise.all(promises); var availability = await Promise.all(promises);
var allAvailable = availability.every(e => !!e); var allAvailable = availability.every(e => !!e);
var values = [name + ' ' + (allAvailable ? colors.green + revision + colors.reset : revision)]; var values = [name + ' ' + (allAvailable ? colors.green + revision + colors.reset : revision)];
for (var i = 0; i < availability.length; ++i) { for (var i = 0; i < availability.length; ++i) {
var decoration = availability[i] ? '+' : '-'; var decoration = availability[i] ? '+' : '-';
var color = availability[i] ? colors.green : colors.red; var color = availability[i] ? colors.green : colors.red;
values.push(color + decoration + colors.reset); values.push(color + decoration + colors.reset);
} }
table.drawRow(values); table.drawRow(values);
} }
/** /**
@ -129,26 +129,26 @@ async function checkAndDrawRevisionAvailability(table, name, revision) {
* @return {!Promise<?Object>} * @return {!Promise<?Object>}
*/ */
function loadJSON(url) { function loadJSON(url) {
var resolve; var resolve;
var promise = new Promise(x => resolve = x); var promise = new Promise(x => resolve = x);
https.get(url, response => { https.get(url, response => {
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
resolve(null); resolve(null);
return; return;
} }
var body = ''; var body = '';
response.on('data', function(chunk){ response.on('data', function(chunk){
body += chunk; body += chunk;
});
response.on('end', function(){
var json = JSON.parse(body);
resolve(json);
});
}).on('error', function(e){
console.error('Error fetching json: ' + e);
resolve(null);
}); });
return promise; response.on('end', function(){
var json = JSON.parse(body);
resolve(json);
});
}).on('error', function(e){
console.error('Error fetching json: ' + e);
resolve(null);
});
return promise;
} }
/** /**
@ -156,7 +156,7 @@ function loadJSON(url) {
* @return {string} * @return {string}
*/ */
function spaceString(size) { function spaceString(size) {
return new Array(size).fill(' ').join(''); return new Array(size).fill(' ').join('');
} }
/** /**
@ -164,11 +164,11 @@ function spaceString(size) {
* @return {string} * @return {string}
*/ */
function filterOutColors(text) { function filterOutColors(text) {
for (var colorName in colors) { for (var colorName in colors) {
var color = colors[colorName]; var color = colors[colorName];
text = text.replace(color, ''); text = text.replace(color, '');
} }
return text; return text;
} }
/** /**
@ -177,8 +177,8 @@ function filterOutColors(text) {
* @return {string} * @return {string}
*/ */
function padLeft(text, length) { function padLeft(text, length) {
var printableCharacters = filterOutColors(text); var printableCharacters = filterOutColors(text);
return printableCharacters.length >= length ? text : spaceString(length - text.length) + text; return printableCharacters.length >= length ? text : spaceString(length - text.length) + text;
} }
/** /**
@ -187,10 +187,10 @@ function padLeft(text, length) {
* @return {string} * @return {string}
*/ */
function padCenter(text, length) { function padCenter(text, length) {
var printableCharacters = filterOutColors(text); var printableCharacters = filterOutColors(text);
if (printableCharacters.length >= length) if (printableCharacters.length >= length)
return text; return text;
var left = Math.floor((length - printableCharacters.length) / 2); var left = Math.floor((length - printableCharacters.length) / 2);
var right = Math.ceil((length - printableCharacters.length) / 2); var right = Math.ceil((length - printableCharacters.length) / 2);
return spaceString(left) + text + spaceString(right); return spaceString(left) + text + spaceString(right);
} }

View File

@ -753,6 +753,10 @@ table@^4.0.1:
slice-ansi "0.0.4" slice-ansi "0.0.4"
string-width "^2.0.0" string-width "^2.0.0"
text-diff@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/text-diff/-/text-diff-1.0.1.tgz#6c105905435e337857375c9d2f6ca63e453ff565"
text-table@~0.2.0: text-table@~0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"