diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000000..8b2754cd9d4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/node_modules/
+/.local-chromium/
+/.dev_profile*
+.DS_Store
+*.swp
+*.pyc
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000000..ae319c70aca
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,23 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
diff --git a/README.md b/README.md
index 50eee758267..699aa6915f8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,36 @@
-# puppeteer
-Headless Chrome Node API
+### Status
+
+Test results on Mac OS X in headless mode:
+```
+ 111 passed
+ 20 failed as expected
+ 1 skipped
+ 49 unsupported
+```
+
+### Installing
+
+```bash
+npm i
+npm link # this adds puppeteer to $PATH
+```
+
+### Run
+
+```bash
+# run phantomjs script
+puppeteer third_party/phantomjs/examples/colorwheel.js
+
+# run 'headful'
+puppeteer --no-headless third_party/phantomjs/examples/colorwheel.js
+
+# run puppeteer example
+node examples/screenshot.js
+```
+
+### Tests
+
+Run phantom.js tests using puppeteer:
+```
+./third_party/phantomjs/test/run-tests.py
+```
diff --git a/examples/custom-chromium-revision.js b/examples/custom-chromium-revision.js
new file mode 100644
index 00000000000..e49b3f6a80f
--- /dev/null
+++ b/examples/custom-chromium-revision.js
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var Browser = require('../lib/Browser');
+var Downloader = require('../lib/Downloader');
+
+var revision = "464642";
+console.log('Downloading custom chromium revision - ' + revision);
+Downloader.downloadChromium(revision).then(async () => {
+ console.log('Done.');
+ var executablePath = Downloader.executablePath(revision);
+ var browser1 = new Browser({
+ remoteDebuggingPort: 9228,
+ executablePath,
+ });
+ var browser2 = new Browser({
+ remoteDebuggingPort: 9229,
+ });
+ var [version1, version2] = await Promise.all([
+ browser1.version(),
+ browser2.version()
+ ]);
+ console.log('browser1: ' + version1);
+ console.log('browser2: ' + version2);
+ browser1.close();
+ browser2.close();
+});
+
diff --git a/examples/screenshot.js b/examples/screenshot.js
new file mode 100644
index 00000000000..51a2f69f02c
--- /dev/null
+++ b/examples/screenshot.js
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var Browser = require('./lib/Browser');
+var Page = require('./lib/Page');
+var fs = require('fs');
+var browser = new Browser();
+
+browser.newPage().then(async page => {
+ await page.navigate('http://example.com');
+ var screenshotBuffer = await page.screenshot(Page.ScreenshotTypes.PNG);
+ fs.writeFileSync('example.png', screenshotBuffer);
+ browser.close();
+});
diff --git a/index.js b/index.js
new file mode 100755
index 00000000000..1053460736b
--- /dev/null
+++ b/index.js
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var vm = require('vm');
+var fs = require('fs');
+var path = require('path');
+var Browser = require('./lib/Browser');
+var argv = require('minimist')(process.argv.slice(2), {
+ alias: { v: 'version' },
+ boolean: ['headless'],
+ default: {'headless': true },
+});
+
+if (argv.version) {
+ console.log('Puppeteer v' + require('./package.json').version);
+ return;
+}
+
+if (argv['ssl-certificates-path']) {
+ console.error('Flag --ssl-certificates-path is not currently supported.\nMore information at https://github.com/aslushnikov/puppeteer/issues/1');
+ process.exit(1);
+ return;
+}
+
+var scriptArguments = argv._;
+if (!scriptArguments.length) {
+ console.log('puppeteer [scriptfile]');
+ return;
+}
+
+var scriptPath = path.resolve(process.cwd(), scriptArguments[0]);
+if (!fs.existsSync(scriptPath)) {
+ console.error(`script not found: ${scriptPath}`);
+ process.exit(1);
+ return;
+}
+
+var browser = new Browser({
+ remoteDebuggingPort: 9229,
+ headless: argv.headless,
+});
+
+var PhatomJs = require('./phantomjs');
+var context = PhatomJs.createContext(browser, scriptPath, argv);
+var scriptContent = fs.readFileSync(scriptPath, 'utf8');
+vm.runInContext(scriptContent, context);
diff --git a/install.js b/install.js
new file mode 100644
index 00000000000..1a0cfae017b
--- /dev/null
+++ b/install.js
@@ -0,0 +1,32 @@
+var Downloader = require('./lib/Downloader');
+var revision = require('./package').puppeteer.chromium_revision;
+var fs = require('fs');
+var ProgressBar = require('progress');
+
+var executable = Downloader.executablePath(revision);
+if (fs.existsSync(executable))
+ return;
+
+Downloader.downloadChromium(revision, onProgress)
+ .catch(error => {
+ console.error('Download failed: ' + error.message);
+ });
+
+var progressBar = null;
+function onProgress(bytesTotal, delta) {
+ if (!progressBar) {
+ progressBar = new ProgressBar(`Downloading Chromium - ${toMegabytes(bytesTotal)} [:bar] :percent :etas `, {
+ complete: '=',
+ incomplete: ' ',
+ width: 20,
+ total: bytesTotal,
+ });
+ }
+ progressBar.tick(delta);
+}
+
+function toMegabytes(bytes) {
+ var mb = bytes / 1024 / 1024;
+ return (Math.round(mb * 10) / 10) + ' Mb';
+}
+
diff --git a/lib/Browser.js b/lib/Browser.js
new file mode 100644
index 00000000000..20b09ecfa9c
--- /dev/null
+++ b/lib/Browser.js
@@ -0,0 +1,142 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var CDP = require('chrome-remote-interface');
+var http = require('http');
+var path = require('path');
+var removeRecursive = require('rimraf').sync;
+var Page = require('./Page');
+var childProcess = require('child_process');
+var Downloader = require('./Downloader');
+
+var CHROME_PROFILE_PATH = path.resolve(__dirname, '..', '.dev_profile');
+var browserId = 0;
+
+var DEFAULT_ARGS = [
+ '--disable-background-timer-throttling',
+ '--no-first-run',
+];
+
+class Browser {
+ /**
+ * @param {(!Object|undefined)} options
+ */
+ constructor(options) {
+ options = options || {};
+ ++browserId;
+ this._userDataDir = CHROME_PROFILE_PATH + browserId;
+ this._remoteDebuggingPort = 9229;
+ if (typeof options.remoteDebuggingPort === 'number')
+ this._remoteDebuggingPort = options.remoteDebuggingPort;
+ this._chromeArguments = DEFAULT_ARGS.concat([
+ `--user-data-dir=${this._userDataDir}`,
+ `--remote-debugging-port=${this._remoteDebuggingPort}`,
+ ]);
+ if (typeof options.headless !== 'boolean' || options.headless) {
+ this._chromeArguments.push(...[
+ `--headless`,
+ `--disable-gpu`,
+ ]);
+ }
+ if (typeof options.executablePath === 'string') {
+ this._chromeExecutable = options.executablePath;
+ } else {
+ var chromiumRevision = require('../package.json').puppeteer.chromium_revision;
+ this._chromeExecutable = Downloader.executablePath(chromiumRevision);
+ }
+ if (Array.isArray(options.args))
+ this._chromeArguments.push(...options.args);
+ this._chromeProcess = null;
+ this._tabSymbol = Symbol('Browser.TabSymbol');
+ }
+
+ /**
+ * @return {!Promise}
+ */
+ async newPage() {
+ await this._ensureChromeIsRunning();
+ if (!this._chromeProcess || this._chromeProcess.killed)
+ throw new Error('ERROR: this chrome instance is not alive any more!');
+ var tab = await CDP.New({port: this._remoteDebuggingPort});
+ var client = await CDP({tab: tab, port: this._remoteDebuggingPort});
+ var page = await Page.create(this, client);
+ page[this._tabSymbol] = tab;
+ return page;
+ }
+
+ /**
+ * @param {!Page} page
+ */
+ async closePage(page) {
+ if (!this._chromeProcess || this._chromeProcess.killed)
+ throw new Error('ERROR: this chrome instance is not running');
+ var tab = page[this._tabSymbol];
+ if (!tab)
+ throw new Error('ERROR: page does not belong to this chrome instance');
+ await CDP.Close({id: tab.id, port: this._remoteDebuggingPort});
+ }
+
+ /**
+ * @return {string}
+ */
+ async version() {
+ await this._ensureChromeIsRunning();
+ var version = await CDP.Version({port: this._remoteDebuggingPort});
+ return version.Browser;
+ }
+
+ async _ensureChromeIsRunning() {
+ if (this._chromeProcess)
+ return;
+ this._chromeProcess = childProcess.spawn(this._chromeExecutable, this._chromeArguments, {});
+ // Cleanup as processes exit.
+ process.on('exit', () => this._chromeProcess.kill());
+ this._chromeProcess.on('exit', () => removeRecursive(this._userDataDir));
+
+ await waitForChromeResponsive(this._remoteDebuggingPort);
+ }
+
+ close() {
+ if (!this._chromeProcess)
+ return;
+ this._chromeProcess.kill();
+ }
+}
+
+module.exports = Browser;
+
+function waitForChromeResponsive(remoteDebuggingPort) {
+ var fulfill;
+ var promise = new Promise(x => fulfill = x);
+ var options = {
+ method: 'GET',
+ host: 'localhost',
+ port: remoteDebuggingPort,
+ path: '/json/list'
+ };
+ var probeTimeout = 100;
+ var probeAttempt = 1;
+ sendRequest();
+ return promise;
+
+ function sendRequest() {
+ var req = http.request(options, res => {
+ fulfill()
+ });
+ req.on('error', e => setTimeout(sendRequest, probeTimeout));
+ req.end();
+ }
+}
diff --git a/lib/Downloader.js b/lib/Downloader.js
new file mode 100644
index 00000000000..5b63cfefc41
--- /dev/null
+++ b/lib/Downloader.js
@@ -0,0 +1,123 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var os = require('os');
+var https = require('https');
+var fs = require('fs');
+var path = require('path');
+var extract = require('extract-zip');
+var util = require('util');
+
+var CHROMIUM_PATH = path.join(__dirname, '..', '.local-chromium');
+
+var downloadURLs = {
+ linux: 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip',
+ darwin: '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',
+ win64: 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip',
+};
+
+module.exports = {
+ downloadChromium,
+ executablePath,
+};
+
+/**
+ * @param {string} revision
+ * @param {?function(number, number)} progressCallback
+ * @return {!Promise}
+ */
+async function downloadChromium(revision, progressCallback) {
+ var url = null;
+ var platform = os.platform();
+ if (platform === 'darwin')
+ url = downloadURLs.darwin;
+ else if (platform === 'linux')
+ url = downloadURLs.linux;
+ else if (platform === 'win32')
+ url = os.arch() === 'x64' ? downloadURLs.win64 : downloadURLs.win32;
+ console.assert(url, `Unsupported platform: ${platform}`);
+ url = util.format(url, revision);
+ var zipPath = path.join(CHROMIUM_PATH, `download-${revision}.zip`);
+ var folderPath = path.join(CHROMIUM_PATH, revision);
+ if (fs.existsSync(folderPath))
+ return;
+ try {
+ if (!fs.existsSync(CHROMIUM_PATH))
+ fs.mkdirSync(CHROMIUM_PATH);
+ await downloadFile(url, zipPath, progressCallback);
+ await extractZip(zipPath, folderPath);
+ } finally {
+ if (fs.existsSync(zipPath))
+ fs.unlinkSync(zipPath);
+ }
+}
+
+/**
+ * @return {string}
+ */
+function executablePath(revision) {
+ var platform = os.platform();
+ if (platform === 'darwin')
+ return path.join(CHROMIUM_PATH, revision, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
+ if (platform === 'linux')
+ return path.join(CHROMIUM_PATH, revision, 'chrome-linux', 'chrome');
+ if (platform === 'win32')
+ return path.join(CHROMIUM_PATH, revision, 'chrome-win32', 'chrome.exe');
+ throw new Error(`Unsupported platform: ${platform}`);
+}
+
+/**
+ * @param {string} url
+ * @param {string} destinationPath
+ * @param {?function(number, number)} progressCallback
+ * @return {!Promise}
+ */
+function downloadFile(url, destinationPath, progressCallback) {
+ var fulfill, reject;
+ var promise = new Promise((x, y) => { fulfill = x; reject = y; });
+ var request = https.get(url, response => {
+ if (response.statusCode !== 200) {
+ var error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
+ // consume response data to free up memory
+ response.resume();
+ reject(error);
+ 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);
+ }
+}
+
+/**
+ * @param {string} zipPath
+ * @param {string} folderPath
+ * @return {!Promise}
+ */
+function extractZip(zipPath, folderPath) {
+ return new Promise(fulfill => extract(zipPath, {dir: folderPath}, fulfill));
+}
diff --git a/lib/Page.js b/lib/Page.js
new file mode 100644
index 00000000000..5553cd16c50
--- /dev/null
+++ b/lib/Page.js
@@ -0,0 +1,350 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fs = require('fs');
+var EventEmitter = require('events');
+var helpers = require('./helpers');
+
+class Page extends EventEmitter {
+ /**
+ * @param {!Browser} browser
+ * @param {!CDP} client
+ * @return {!Promise}
+ */
+ static async create(browser, client) {
+ await Promise.all([
+ client.send('Network.enable', {}),
+ client.send('Page.enable', {}),
+ client.send('Runtime.enable', {}),
+ client.send('Security.enable', {}),
+ ]);
+ var screenDPI = await helpers.evaluate(client, () => window.devicePixelRatio, []);
+ var page = new Page(browser, client, screenDPI.result.value);
+ // Initialize default page size.
+ await page.setSize({width: 400, height: 300});
+ return page;
+ }
+
+ /**
+ * @param {!Browser} browser
+ * @param {!CDP} client
+ * @param {number} screenDPI
+ */
+ constructor(browser, client, screenDPI) {
+ super();
+ this._browser = browser;
+ this._client = client;
+ this._screenDPI = screenDPI;
+ this._extraHeaders = {};
+ /** @type {!Map} */
+ this._scriptIdToPageCallback = new Map();
+ /** @type {!Map} */
+ this._scriptIdToCallbackName = new Map();
+
+ client.on('Debugger.paused', event => this._onDebuggerPaused(event));
+ client.on('Network.responseReceived', event => this.emit(Page.Events.ResponseReceived, event.response));
+ client.on('Network.loadingFailed', event => this.emit(Page.Events.ResourceLoadingFailed, event));
+ client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
+ client.on('Page.javascriptDialogOpening', dialog => this.emit(Page.Events.DialogOpened, dialog));
+ client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails));
+ }
+
+ /**
+ * @param {string} name
+ * @param {function(?)} callback
+ */
+ async setInPageCallback(name, callback) {
+ var hasCallback = await this.evaluate(function(name) {
+ return !!window[name];
+ }, name);
+ if (hasCallback)
+ throw new Error(`Failed to set in-page callback with name ${name}: window['${name}'] already exists!`);
+
+ var sourceURL = '__in_page_callback__' + name;
+ // Ensure debugger is enabled.
+ await this._client.send('Debugger.enable', {});
+ var scriptPromise = helpers.waitForScriptWithURL(this._client, sourceURL);
+ helpers.evaluate(this._client, inPageCallback, [name], false /* awaitPromise */, sourceURL);
+ var script = await scriptPromise;
+ if (!script)
+ throw new Error(`Failed to set in-page callback with name "${name}"`);
+ this._scriptIdToPageCallback.set(script.scriptId, callback);
+ this._scriptIdToCallbackName.set(script.scriptId, name);
+
+ function inPageCallback(callbackName) {
+ window[callbackName] = (...args) => {
+ window[callbackName].__args = args;
+ window[callbackName].__result = undefined;
+ debugger;
+ return window[callbackName].__result;
+ };
+ }
+ }
+
+ /**
+ * @param {string} scriptId
+ */
+ async _handleInPageCallback(scriptId) {
+ var name = /** @type {string} */ (this._scriptIdToCallbackName.get(scriptId));
+ var callback = /** @type {function()} */ (this._scriptIdToPageCallback.get(scriptId));
+ var args = await this.evaluate(callbackName => window[callbackName].__args, name);
+ var result = callback.apply(null, args);
+ await this.evaluate(assignResult, name, result);
+ this._client.send('Debugger.resume');
+
+ /**
+ * @param {string} callbackName
+ * @param {string} callbackResult
+ */
+ function assignResult(callbackName, callbackResult) {
+ window[callbackName].__result = callbackResult;
+ }
+ }
+
+ _onDebuggerPaused(event) {
+ var location = event.callFrames[0] ? event.callFrames[0].location : null;
+ if (location && this._scriptIdToPageCallback.has(location.scriptId)) {
+ this._handleInPageCallback(location.scriptId);
+ return;
+ }
+ this._client.send('Debugger.resume');
+ }
+
+ /**
+ * @param {!Object} headers
+ * @return {!Promise}
+ */
+ async setExtraHTTPHeaders(headers) {
+ this._extraHeaders = {};
+ // Note: header names are case-insensitive.
+ for (var key of Object.keys(headers))
+ this._extraHeaders[key.toLowerCase()] = headers[key];
+ return this._client.send('Network.setExtraHTTPHeaders', { headers });
+ }
+
+ /**
+ * @return {!Object}
+ */
+ extraHTTPHeaders() {
+ return Object.assign({}, this._extraHeaders);
+ }
+
+ /**
+ * @param {string} userAgent
+ * @return {!Promise}
+ */
+ async setUserAgentOverride(userAgent) {
+ this._userAgent = userAgent;
+ return this._client.send('Network.setUserAgentOverride', { userAgent });
+ }
+
+ /**
+ * @return {string}
+ */
+ userAgentOverride() {
+ return this._userAgent;
+ }
+
+ _handleException(exceptionDetails) {
+ var stack = [];
+ if (exceptionDetails.stackTrace) {
+ stack = exceptionDetails.stackTrace.callFrames.map(cf => cf.url);
+ }
+ var stackTrace = exceptionDetails.stackTrace;
+ this.emit(Page.Events.ExceptionThrown, exceptionDetails.exception.description, stack);
+ }
+
+ _onConsoleAPI(event) {
+ var values = event.args.map(arg => arg.value || arg.description || '');
+ this.emit(Page.Events.ConsoleMessageAdded, values.join(' '));
+ }
+
+ /**
+ * @param {boolean} accept
+ * @param {string} promptText
+ * @return {!Promise}
+ */
+ async handleDialog(accept, promptText) {
+ return this._client.send('Page.handleJavaScriptDialog', {accept, promptText});
+ }
+
+ /**
+ * @return {!Promise}
+ */
+ async url() {
+ return this.evaluate(function() {
+ return window.location.href;
+ });
+ }
+
+ /**
+ * @param {string} html
+ * @return {!Promise}
+ */
+ async setContent(html) {
+ var resourceTree = await this._client.send('Page.getResourceTree', {});
+ await this._client.send('Page.setDocumentContent', {
+ frameId: resourceTree.frameTree.frame.id,
+ html: html
+ });
+ }
+
+ /**
+ * @param {string} html
+ * @return {!Promise}
+ */
+ async navigate(url) {
+ var loadPromise = new Promise(fulfill => this._client.once('Page.loadEventFired', fulfill)).then(() => true);
+ var interstitialPromise = new Promise(fulfill => this._client.once('Security.certificateError', fulfill)).then(() => false);
+ var referrer = this._extraHeaders.referer;
+ // Await for the command to throw exception in case of illegal arguments.
+ await this._client.send('Page.navigate', {url, referrer});
+ return await Promise.race([loadPromise, interstitialPromise]);
+ }
+
+ /**
+ * @param {!{width: number, height: number}} size
+ * @return {!Promise}
+ */
+ async setSize(size) {
+ this._size = size;
+ var width = size.width;
+ var height = size.height;
+ var zoom = this._screenDPI;
+ return Promise.all([
+ this._client.send('Emulation.setDeviceMetricsOverride', {
+ width,
+ height,
+ deviceScaleFactor: 1,
+ scale: 1 / zoom,
+ mobile: false,
+ fitWindow: false
+ }),
+ this._client.send('Emulation.setVisibleSize', {
+ width: width / zoom,
+ height: height / zoom,
+ })
+ ]);
+ }
+
+ /**
+ * @return {!{width: number, height: number}}
+ */
+ size() {
+ return this._size;
+ }
+
+ /**
+ * @param {function()} fun
+ * @param {!Array<*>} args
+ * @return {!Promise<(!Object|udndefined)>}
+ */
+ async evaluate(fun, ...args) {
+ var response = await helpers.evaluate(this._client, fun, args, false /* awaitPromise */);
+ if (response.exceptionDetails) {
+ this._handleException(response.exceptionDetails);
+ return;
+ }
+ return response.result.value;
+ }
+
+ /**
+ * @param {function()} fun
+ * @param {!Array<*>} args
+ * @return {!Promise<(!Object|udndefined)>}
+ */
+ async evaluateAsync(fun, ...args) {
+ var response = await helpers.evaluate(this._client, fun, args, true /* awaitPromise */);
+ if (response.exceptionDetails) {
+ this._handleException(response.exceptionDetails);
+ return;
+ }
+ return response.result.value;
+ }
+
+ /**
+ * @param {!Page.ScreenshotType} screenshotType
+ * @param {?{x: number, y: number, width: number, height: number}} clipRect
+ * @return {!Promise}
+ */
+ async screenshot(screenshotType, clipRect) {
+ if (clipRect) {
+ await Promise.all([
+ this._client.send('Emulation.setVisibleSize', {
+ width: clipRect.width / this._screenDPI,
+ height: clipRect.height / this._screenDPI,
+ }),
+ this._client.send('Emulation.forceViewport', {
+ x: clipRect.x / this._screenDPI,
+ y: clipRect.y / this._screenDPI,
+ scale: 1,
+ })
+ ]);
+ }
+ var result = await this._client.send('Page.captureScreenshot', {
+ fromSurface: true,
+ format: screenshotType,
+ });
+ if (clipRect) {
+ await Promise.all([
+ this.setSize(this.size()),
+ this._client.send('Emulation.resetViewport')
+ ]);
+ }
+ return new Buffer(result.data, 'base64');
+ }
+
+ /**
+ * @return {!Promise}
+ */
+ async plainText() {
+ return this.evaluate(function() {
+ return document.body.innerText;
+ });
+ }
+
+ /**
+ * @return {!Promise}
+ */
+ async title() {
+ return this.evaluate(function() {
+ return document.title;
+ });
+ }
+
+ /**
+ * @return {!Promise}
+ */
+ async close() {
+ return this._browser.closePage(this);
+ }
+}
+
+/** @enum {string} */
+Page.ScreenshotTypes = {
+ PNG: "png",
+ JPG: "jpeg",
+};
+
+Page.Events = {
+ ConsoleMessageAdded: 'Page.Events.ConsoleMessageAdded',
+ DialogOpened: 'Page.Events.DialogOpened',
+ ExceptionThrown: 'Page.Events.ExceptionThrown',
+ ResourceLoadingFailed: 'Page.Events.ResourceLoadingFailed',
+ ResponseReceived: 'Page.Events.ResponseReceived',
+};
+
+module.exports = Page;
diff --git a/lib/helpers.js b/lib/helpers.js
new file mode 100644
index 00000000000..b5a868e0f3b
--- /dev/null
+++ b/lib/helpers.js
@@ -0,0 +1,68 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = {
+ /**
+ * @param {!CDP} client
+ * @param {string} url
+ * @return {!Promise}
+ */
+ waitForScriptWithURL: function(client, url) {
+ var fulfill;
+ var promise = new Promise(x => fulfill = x);
+ client.on('Debugger.scriptParsed', onScriptParsed);
+ client.on('Debugger.scriptFailedToParse', onScriptFailedToParse);
+ return promise;
+
+ function onScriptParsed(event) {
+ if (event.url !== url)
+ return;
+ client.removeListener('Debugger.scriptParsed', onScriptParsed);
+ client.removeListener('Debugger.scriptFailedToParse', onScriptFailedToParse);
+ fulfill(event);
+ }
+
+ function onScriptFailedToParse(event) {
+ if (event.url !== url)
+ return;
+ client.removeListener('Debugger.scriptParsed', onScriptParsed);
+ client.removeListener('Debugger.scriptFailedToParse', onScriptFailedToParse);
+ fulfill(null);
+ }
+ },
+
+ /**
+ * @param {!CDP} client
+ * @param {function()} fun
+ * @param {!Array<*>} args
+ * @param {boolean} awaitPromise
+ * @param {string=} sourceURL
+ * @return {!Promise}
+ */
+ evaluate: function(client, fun, args, awaitPromise, sourceURL) {
+ var argsString = args.map(x => JSON.stringify(x)).join(',');
+ var code = `(${fun.toString()})(${argsString})`;
+ if (awaitPromise)
+ code = `Promise.resolve(${code})`;
+ if (sourceURL)
+ code += `\n//# sourceURL=${sourceURL}`;
+ return client.send('Runtime.evaluate', {
+ expression: code,
+ awaitPromise: awaitPromise,
+ returnByValue: true
+ });
+ },
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000000..4fa90178da6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "puppeteer",
+ "version": "0.0.1",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "python third_party/phantomjs/test/run-tests.py",
+ "install": "node install.js"
+ },
+ "author": "The Chromium Authors",
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "chrome-remote-interface": "^0.18.0",
+ "deasync": "^0.1.9",
+ "extract-zip": "^1.6.5",
+ "mime": "^1.3.4",
+ "minimist": "^1.2.0",
+ "ncp": "^2.0.0",
+ "progress": "^2.0.0",
+ "rimraf": "^2.6.1"
+ },
+ "bin": {
+ "puppeteer": "index.js"
+ },
+ "puppeteer": {
+ "chromium_revision": "468266"
+ }
+}
diff --git a/phantomjs/FileSystem.js b/phantomjs/FileSystem.js
new file mode 100644
index 00000000000..bbc5d575ccd
--- /dev/null
+++ b/phantomjs/FileSystem.js
@@ -0,0 +1,365 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var path = require('path');
+var fs = require('fs');
+var deasync = require('deasync');
+var removeRecursive = require('rimraf').sync;
+var copyRecursive = deasync(require('ncp').ncp);
+
+class FileSystem {
+ constructor() {
+ this.separator = path.sep;
+ }
+
+ /**
+ * @return {string}
+ */
+ get workingDirectory() {
+ return process.cwd();
+ }
+
+ /**
+ * @param {string} directoryPath
+ */
+ changeWorkingDirectory(directoryPath) {
+ try {
+ process.chdir(directoryPath);
+ return true;
+ } catch (e){
+ return false;
+ }
+ }
+
+ /**
+ * @param {string} relativePath
+ * @return {string}
+ */
+ absolute(relativePath) {
+ relativePath = path.normalize(relativePath);
+ if (path.isAbsolute(relativePath))
+ return relativePath;
+ return path.resolve(path.join(process.cwd(), relativePath));
+ }
+
+ /**
+ * @param {string} filePath
+ * @return {boolean}
+ */
+ exists(filePath) {
+ return fs.existsSync(filePath);
+ }
+
+ /**
+ * @param {string} fromPath
+ * @param {string} toPath
+ */
+ copy(fromPath, toPath) {
+ var content = fs.readFileSync(fromPath);
+ fs.writeFileSync(toPath, content);
+ }
+
+ /**
+ * @param {string} fromPath
+ * @param {string} toPath
+ */
+ move(fromPath, toPath) {
+ var content = fs.readFileSync(fromPath);
+ fs.writeFileSync(toPath, content);
+ fs.unlinkSync(fromPath);
+ }
+
+ /**
+ * @param {string} filePath
+ * @return {number}
+ */
+ size(filePath) {
+ return fs.statSync(filePath).size;
+ }
+
+ /**
+ * @param {string} filePath
+ */
+ touch(filePath) {
+ fs.closeSync(fs.openSync(filePath, 'a'));
+ }
+
+ /**
+ * @param {string} filePath
+ */
+ remove(filePath) {
+ fs.unlinkSync(filePath);
+ }
+
+ /**
+ * @param {string} filePath
+ */
+ lastModified(filePath) {
+ return fs.statSync(filePath).mtime;
+ }
+
+ /**
+ * @param {string} dirPath
+ */
+ makeDirectory(dirPath) {
+ try {
+ fs.mkdirSync(dirPath);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /**
+ * @param {string} dirPath
+ */
+ removeTree(dirPath) {
+ removeRecursive(dirPath);
+ }
+
+ /**
+ * @param {string} fromPath
+ * @param {string} toPath
+ */
+ copyTree(fromPath, toPath) {
+ copyRecursive(fromPath, toPath);
+ }
+
+ /**
+ * @param {string} dirPath
+ * @return {!Array}
+ */
+ list(dirPath) {
+ return fs.readdirSync(dirPath);
+ }
+
+ /**
+ * @param {string} linkPath
+ * @return {string}
+ */
+ readLink(linkPath) {
+ return fs.readlinkSync(linkPath);
+ }
+
+ /**
+ * @param {string} filePath
+ * @param {Object} data
+ * @param {string} mode
+ */
+ write(filePath, data, mode) {
+ var fd = new FileDescriptor(filePath, mode, 'utf8');
+ fd.write(data);
+ fd.close();
+ }
+
+ /**
+ * @param {string} somePath
+ * @return {boolean}
+ */
+ isAbsolute(somePath) {
+ return path.isAbsolute(somePath);
+ }
+
+ /**
+ * @return {string}
+ */
+ read(filePath) {
+ return fs.readFileSync(filePath, 'utf8');
+ }
+
+ /**
+ * @param {string} filePath
+ * @return {boolean}
+ */
+ isFile(filePath) {
+ return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile();
+ }
+
+ /**
+ * @param {string} dirPath
+ * @return {boolean}
+ */
+ isDirectory(dirPath) {
+ return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
+ }
+
+ /**
+ * @param {string} filePath
+ * @return {boolean}
+ */
+ isLink(filePath) {
+ return fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink();
+ }
+
+ /**
+ * @param {string} filePath
+ * @return {boolean}
+ */
+ isReadable(filePath) {
+ try {
+ fs.accessSync(filePath, fs.constants.R_OK);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /**
+ * @param {string} filePath
+ * @return {boolean}
+ */
+ isWritable(filePath) {
+ try {
+ fs.accessSync(filePath, fs.constants.W_OK);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /**
+ * @param {string} filePath
+ * @return {boolean}
+ */
+ isExecutable(filePath) {
+ try {
+ fs.accessSync(filePath, fs.constants.X_OK);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /**
+ * @param {string} somePath
+ * @return {!Array}
+ */
+ split(somePath) {
+ somePath = path.normalize(somePath);
+ if (somePath.endsWith(path.sep))
+ somePath = somePath.substring(0, somePath.length - path.sep.length);
+ return somePath.split(path.sep);
+ }
+
+ /**
+ * @param {string} path1
+ * @param {string} path2
+ * @return {string}
+ */
+ join(...args) {
+ if (args[0] === '' && args.length > 1)
+ args[0] = path.sep;
+ args = args.filter(part => typeof part === 'string');
+ return path.join.apply(path, args);
+ }
+
+ /**
+ * @param {string} filePath
+ * @param {(string|!Object)} option
+ * @return {!FileDescriptor}
+ */
+ open(filePath, option) {
+ if (typeof option === 'string')
+ return new FileDescriptor(filePath, option);
+ return new FileDescriptor(filePath, option.mode);
+ }
+}
+
+var fdwrite = deasync(fs.write);
+var fdread = deasync(fs.read);
+
+class FileDescriptor {
+ /**
+ * @param {string} filePath
+ * @param {string} mode
+ */
+ constructor(filePath, mode) {
+ this._position = 0;
+ this._encoding = 'utf8';
+ if (mode === 'rb') {
+ this._mode = 'r';
+ this._encoding = 'latin1';
+ } else if (mode === 'wb' || mode === 'b') {
+ this._mode = 'w';
+ this._encoding = 'latin1';
+ } else if (mode === 'rw+') {
+ this._mode = 'a+';
+ this._position = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
+ } else {
+ this._mode = mode;
+ }
+ this._fd = fs.openSync(filePath, this._mode);
+ }
+
+ /**
+ * @param {string} data
+ */
+ write(data) {
+ var buffer = Buffer.from(data, this._encoding);
+ var written = fdwrite(this._fd, buffer, 0, buffer.length, this._position);
+ this._position += written;
+ }
+
+ getEncoding() {
+ return 'UTF-8';
+ }
+
+ /**
+ * @param {string} data
+ */
+ writeLine(data) {
+ this.write(data + '\n');
+ }
+
+ /**
+ * @param {number=} size
+ * @return {string}
+ */
+ read(size) {
+ var position = this._position;
+ if (!size) {
+ size = fs.fstatSync(this._fd).size;
+ position = 0;
+ }
+ var buffer = new Buffer(size);
+ var bytesRead = fdread(this._fd, buffer, 0, size, position);
+ this._position += bytesRead;
+ return buffer.toString(this._encoding);
+ }
+
+ flush() {
+ // noop.
+ }
+
+ /**
+ * @param {number} position
+ */
+ seek(position) {
+ this._position = position;
+ }
+
+ close() {
+ fs.closeSync(this._fd);
+ }
+
+ /**
+ * @return {boolean}
+ */
+ atEnd() {
+ }
+}
+
+module.exports = FileSystem;
diff --git a/phantomjs/Phantom.js b/phantomjs/Phantom.js
new file mode 100644
index 00000000000..ed0269681ee
--- /dev/null
+++ b/phantomjs/Phantom.js
@@ -0,0 +1,139 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fs = require('fs');
+var path = require('path');
+var vm = require('vm');
+var url = require('url');
+
+/**
+ * @param {!Object} context
+ * @param {string} scriptPath
+ */
+module.exports.create = function(context, scriptPath) {
+ var phantom = {
+ page: {
+ onConsoleMessage: null,
+ },
+
+ /**
+ * @param {string} relative
+ * @param {string} base
+ * @return {string}
+ */
+ resolveRelativeUrl: function(relative, base) {
+ return url.resolve(base, relative);
+ },
+
+ /**
+ * @param {string} url
+ * @return {string}
+ */
+ fullyDecodeUrl: function(url) {
+ return decodeURI(url);
+ },
+
+ libraryPath: path.dirname(scriptPath),
+
+ onError: null,
+
+ /**
+ * @return {string}
+ */
+ get outputEncoding() {
+ return 'UTF-8';
+ },
+
+ /**
+ * @param {string} value
+ */
+ set outputEncoding(value) {
+ throw new Error('Phantom.outputEncoding setter is not implemented');
+ },
+
+ /**
+ * @return {boolean}
+ */
+ get cookiesEnabled() {
+ return true;
+ },
+
+ /**
+ * @param {boolean} value
+ */
+ set cookiesEnabled(value) {
+ throw new Error('Phantom.cookiesEnabled setter is not implemented');
+ },
+
+ /**
+ * @return {!{major: number, minor: number, patch: number}}
+ */
+ get version() {
+ var versionParts = require('../package.json').version.split('.');
+ return {
+ major: parseInt(versionParts[0], 10),
+ minor: parseInt(versionParts[1], 10),
+ patch: parseInt(versionParts[2], 10),
+ };
+ },
+
+ /**
+ * @param {number=} code
+ */
+ exit: function(code) {
+ process.exit(code);
+ },
+
+ /**
+ * @param {string} filePath
+ * @return {boolean}
+ */
+ injectJs: function(filePath) {
+ filePath = path.resolve(phantom.libraryPath, filePath);
+ if (!fs.existsSync(filePath))
+ return false;
+ var code = fs.readFileSync(filePath, 'utf8');
+ if (code.startsWith('#!'))
+ code = code.substring(code.indexOf('\n'));
+ vm.runInContext(code, context, {
+ filename: filePath,
+ displayErrors: true
+ });
+ return true;
+ },
+
+ /**
+ * @param {string} moduleSource
+ * @param {string} filename
+ */
+ loadModule: function(moduleSource, filename) {
+ var code = [
+ "(function(require, exports, module) {\n",
+ moduleSource,
+ "\n}.call({},",
+ "require.cache['" + filename + "']._getRequire(),",
+ "require.cache['" + filename + "'].exports,",
+ "require.cache['" + filename + "']",
+ "));"
+ ].join('');
+ vm.runInContext(code, context, {
+ filename: filename,
+ displayErrors: true
+ });
+ },
+ };
+ return phantom;
+}
diff --git a/phantomjs/System.js b/phantomjs/System.js
new file mode 100644
index 00000000000..770929350ba
--- /dev/null
+++ b/phantomjs/System.js
@@ -0,0 +1,118 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var readline = require('readline');
+var await = require('./utilities').await;
+var os = require('os');
+
+class System {
+ /**
+ * @param {!Array} args
+ */
+ constructor(args) {
+ this.args = args;
+ this.env = {};
+ Object.assign(this.env, process.env);
+ this.stdin = new StandardInput(process.stdin);
+ this.stdout = new StandardOutput(process.stdout);
+ this.stderr = new StandardOutput(process.stderr);
+ this.platform = "phantomjs";
+ this.pid = process.pid;
+ this.isSSLSupported = false;
+ this.os = {
+ architecture: os.arch(),
+ name: os.type(),
+ version: os.release()
+ };
+ }
+}
+
+class StandardInput {
+ /**
+ * @param {!Readable} readableStream
+ */
+ constructor(readableStream) {
+ this._readline = readline.createInterface({
+ input: readableStream
+ });
+ this._lines = [];
+ this._closed = false;
+ this._readline.on('line', line => this._lines.push(line));
+ this._readline.on('close', () => this._closed = true);
+ }
+
+ /**
+ * @return {string}
+ */
+ readLine() {
+ if (this._closed && !this._lines.length)
+ return '';
+ if (!this._lines.length) {
+ var linePromise = new Promise(fulfill => this._readline.once('line', fulfill));
+ await(linePromise);
+ }
+ return this._lines.shift();
+ }
+
+ /**
+ * @return {string}
+ */
+ read() {
+ if (!this._closed) {
+ var closePromise = new Promise(fulfill => this._readline.once('close', fulfill));
+ await(closePromise);
+ }
+ var text = this._lines.join('\n');
+ this._lines = [];
+ return text;
+ }
+
+ close() {
+ this._readline.close();
+ }
+}
+
+class StandardOutput {
+ /**
+ * @param {!Writable} writableStream
+ */
+ constructor(writableStream) {
+ this._stream = writableStream;
+ }
+
+ /**
+ * @param {string} data
+ */
+ write(data) {
+ this._stream.write(data);
+ }
+
+ /**
+ * @param {string} data
+ */
+ writeLine(data) {
+ this._stream.write(data + '\n');
+ }
+
+ flush() {
+ }
+
+ close() {
+ this._stream.end();
+ }
+}
+
+module.exports = System;
diff --git a/phantomjs/WebPage.js b/phantomjs/WebPage.js
new file mode 100644
index 00000000000..2ea290a2311
--- /dev/null
+++ b/phantomjs/WebPage.js
@@ -0,0 +1,327 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var await = require('./utilities').await;
+var EventEmitter = require('events');
+var fs = require('fs');
+var path = require('path');
+var mime = require('mime');
+
+var PageEvents = require('../lib/Page').Events;
+var ScreenshotTypes = require('../lib/Page').ScreenshotTypes;
+
+var noop = function() { };
+
+class WebPage {
+ /**
+ * @param {!Browser} browser
+ * @param {string} scriptPath
+ * @param {!Object=} options
+ */
+ constructor(browser, scriptPath, options) {
+ this._page = await(browser.newPage());
+ this.settings = new WebPageSettings(this._page);
+
+ options = options || {};
+ options.settings = options.settings || {};
+ if (options.settings.userAgent)
+ this.settings.userAgent = options.settings.userAgent;
+ if (options.viewportSize)
+ await(this._page.setSize(options.viewportSize));
+
+ await(this._page.setInPageCallback('callPhantom', (...args) => {
+ return this.onCallback.apply(null, args);
+ }));
+
+ this.clipRect = options.clipRect || {left: 0, top: 0, width: 0, height: 0};
+ this.onCallback = null;
+ this.onConsoleMessage = null;
+ this.onLoadFinished = null;
+ this.onResourceError = null;
+ this.onResourceReceived = null;
+ this.libraryPath = path.dirname(scriptPath);
+
+ this._onConfirm = undefined;
+ this._onError = noop;
+
+ this._pageEvents = new AsyncEmitter(this._page);
+ this._pageEvents.on(PageEvents.ResponseReceived, response => this._onResponseReceived(response));
+ this._pageEvents.on(PageEvents.ResourceLoadingFailed, event => (this.onResourceError || noop).call(null, event));
+ this._pageEvents.on(PageEvents.ConsoleMessageAdded, msg => (this.onConsoleMessage || noop).call(null, msg));
+ this._pageEvents.on(PageEvents.DialogOpened, dialog => this._onDialog(dialog));
+ this._pageEvents.on(PageEvents.ExceptionThrown, (exception, stack) => (this._onError || noop).call(null, exception, stack));
+ }
+
+ _onResponseReceived(response) {
+ if (!this.onResourceReceived)
+ return;
+ var headers = [];
+ for (var key in response.headers) {
+ headers.push({
+ name: key,
+ value: response.headers[key]
+ });
+ }
+ response.headers = headers;
+ this.onResourceReceived.call(null, response);
+ }
+
+ /**
+ * @param {string} url
+ * @param {function()} callback
+ */
+ includeJs(url, callback) {
+ this._page.evaluateAsync(include, url).then(callback);
+
+ function include(url) {
+ var script = document.createElement('script');
+ script.src = url;
+ var promise = new Promise(x => script.onload = x);
+ document.head.appendChild(script);
+ return promise;
+ }
+ }
+
+ /**
+ * @return {!{width: number, height: number}}
+ */
+ get viewportSize() {
+ return this._page.size();
+ }
+
+ /**
+ * @return {!Object}
+ */
+ get customHeaders() {
+ return this._page.extraHTTPHeaders();
+ }
+
+ /**
+ * @param {!Object} value
+ */
+ set customHeaders(value) {
+ await(this._page.setExtraHTTPHeaders(value));
+ }
+
+ /**
+ * @param {string} filePath
+ */
+ injectJs(filePath) {
+ if (!fs.existsSync(filePath))
+ filePath = path.resolve(this.libraryPath, filePath);
+ if (!fs.existsSync(filePath))
+ return;
+ var code = fs.readFileSync(filePath, 'utf8');
+ this.evaluate(code);
+ }
+
+ /**
+ * @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._onConfirm;
+ }
+
+ /**
+ * @param {function()} handler
+ */
+ set onConfirm(handler) {
+ if (typeof handler !== 'function')
+ handler = undefined;
+ this._onConfirm = handler;
+ }
+
+ _onDialog(dialog) {
+ if (!this._onConfirm)
+ return;
+ var accept = this._onConfirm.call(null);
+ await(this._page.handleDialog(accept));
+ }
+
+ /**
+ * @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._page.navigate(url).then(result => {
+ var status = result ? 'success' : 'fail';
+ if (!result) {
+ this.onResourceError.call(null, {
+ url,
+ errorString: 'SSL handshake failed'
+ });
+ }
+ if (this.onLoadFinished)
+ this.onLoadFinished.call(null, status);
+ if (callback)
+ callback.call(null, status);
+ });
+ }
+
+ /**
+ * @param {!{width: number, height: number}} options
+ */
+ set viewportSize(options) {
+ await(this._page.setSize(options));
+ }
+
+ /**
+ * @param {function()} fun
+ * @param {!Array} args
+ */
+ evaluate(fun, ...args) {
+ return await(this._page.evaluate(fun, ...args));
+ }
+
+ /**
+ * {string} fileName
+ */
+ render(fileName) {
+ var mimeType = mime.lookup(fileName);
+ var screenshotType = null;
+ if (mimeType === 'image/png')
+ screenshotType = ScreenshotTypes.PNG;
+ else if (mimeType === 'image/jpeg')
+ screenshotType = ScreenshotTypes.JPG;
+ if (!screenshotType)
+ throw new Error(`Cannot render to file ${fileName} - unsupported mimeType ${mimeType}`);
+ var clipRect = null;
+ if (this.clipRect && (this.clipRect.left || this.clipRect.top || this.clipRect.width || this.clipRect.height)) {
+ clipRect = {
+ x: this.clipRect.left,
+ y: this.clipRect.top,
+ width: this.clipRect.width,
+ height: this.clipRect.height
+ };
+ }
+ var imageBuffer = await(this._page.screenshot(screenshotType, clipRect));
+ fs.writeFileSync(fileName, imageBuffer);
+ }
+
+ release() {
+ this._page.close();
+ }
+
+ close() {
+ this._page.close();
+ }
+}
+
+class WebPageSettings {
+ /**
+ * @param {!Page} page
+ */
+ constructor(page) {
+ this._page = page;
+ }
+
+ /**
+ * @param {string} value
+ */
+ set userAgent(value) {
+ await(this._page.setUserAgentOverride(value));
+ }
+
+ /**
+ * @return {string}
+ */
+ get userAgent() {
+ return this._page.userAgentOverride();
+ }
+}
+
+// To prevent reenterability, eventemitters should emit events
+// only being in a consistent state.
+// This is not the case for 'ws' npm module: https://goo.gl/sy3dJY
+//
+// Since out phantomjs environment uses nested event loops, we
+// exploit this condition in 'ws', which probably never happens
+// in case of regular I/O.
+//
+// This class is a wrapper around EventEmitter which re-emits events asynchronously,
+// helping to overcome the issue.
+class AsyncEmitter extends EventEmitter {
+ /**
+ * @param {!Page} page
+ */
+ constructor(page) {
+ super();
+ this._page = page;
+ this._symbol = Symbol('AsyncEmitter');
+ this.on('newListener', this._onListenerAdded);
+ this.on('removeListener', this._onListenerRemoved);
+ }
+
+ _onListenerAdded(event, listener) {
+ // Async listener calls original listener on next tick.
+ var asyncListener = (...args) => {
+ process.nextTick(() => listener.apply(null, args));
+ };
+ listener[this._symbol] = asyncListener;
+ this._page.on(event, asyncListener);
+ }
+
+ _onListenerRemoved(event, listener) {
+ this._page.removeListener(event, listener[this._symbol]);
+ }
+}
+
+module.exports = WebPage;
diff --git a/phantomjs/WebServer.js b/phantomjs/WebServer.js
new file mode 100644
index 00000000000..4d99b3246d7
--- /dev/null
+++ b/phantomjs/WebServer.js
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var http = require('http');
+var await = require('./utilities').await;
+
+class WebServer {
+ constructor() {
+ this._server = http.createServer();
+ this.objectName = 'WebServer';
+ this.listenOnPort = this.listen;
+ this.newRequest = function(req, res) { }
+ Object.defineProperty(this, 'port', {
+ 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)
+ return false;
+
+ this._server.on('request', (req, res) => {
+ res.close = res.end.bind(res);
+ var headers = res.getHeaders();
+ res.headers = [];
+ for (var key in headers) {
+ res.headers.push({
+ name: key,
+ value: headers[key]
+ });
+ }
+ 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;
diff --git a/phantomjs/index.js b/phantomjs/index.js
new file mode 100644
index 00000000000..4dca15dfd02
--- /dev/null
+++ b/phantomjs/index.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var vm = require('vm');
+var path = require('path');
+var fs = require('fs');
+var Phantom = require('./Phantom');
+var FileSystem = require('./FileSystem');
+var System = require('./System');
+var WebPage = require('./WebPage');
+var WebServer = require('./WebServer');
+var child_process = require('child_process');
+
+var bootstrapPath = path.join(__dirname, '..', 'third_party', 'phantomjs', 'bootstrap.js');
+var bootstrapCode = fs.readFileSync(bootstrapPath, 'utf8');
+
+module.exports = {
+ /**
+ * @param {!Browser} browser
+ * @param {string} scriptPath
+ * @param {!Array} argv
+ * @return {!Object}
+ */
+ createContext(browser, scriptPath, argv) {
+ var context = {};
+ context.setInterval = setInterval;
+ context.setTimeout = setTimeout;
+ context.clearInterval = clearInterval;
+ context.clearTimeout = clearTimeout;
+
+ context.phantom = Phantom.create(context, scriptPath);
+ context.console = console;
+ context.window = context;
+ context.WebPage = (options) => new WebPage(browser, scriptPath, options);
+
+ vm.createContext(context);
+
+ var nativeExports = {
+ fs: new FileSystem(),
+ system: new System(argv._),
+ webpage: {
+ create: context.WebPage,
+ },
+ webserver: {
+ create: () => new WebServer(),
+ },
+ cookiejar: {
+ create: () => {},
+ },
+ child_process: child_process
+ };
+ vm.runInContext(bootstrapCode, context, {
+ filename: 'bootstrap.js'
+ })(nativeExports);
+ return context;
+ }
+}
+
diff --git a/phantomjs/utilities.js b/phantomjs/utilities.js
new file mode 100644
index 00000000000..9663d196611
--- /dev/null
+++ b/phantomjs/utilities.js
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var loopWhile = require('deasync').loopWhile;
+
+module.exports = {
+ await: function(promise) {
+ var error;
+ var result;
+ var done = false;
+ promise.then(r => result = r)
+ .catch(err => error = err)
+ .then(() => done = true);
+ loopWhile(() => !done);
+ if (error)
+ throw error;
+ return result;
+ }
+}
+
diff --git a/third_party/phantomjs/CHANGES.md b/third_party/phantomjs/CHANGES.md
new file mode 100644
index 00000000000..d5256cfec4c
--- /dev/null
+++ b/third_party/phantomjs/CHANGES.md
@@ -0,0 +1,19 @@
+Short Name: phantomjs
+URL: https://github.com/ariya/phantomjs/tree/2.1.1
+Version: 2.1.1
+License: BSD
+License File: LICENSE.BSD
+Security Critical: no
+
+Description:
+This package is used to aid puppeteer in running phantom.js scripts:
+- test/ - testsuite is used to validate puppeteer running phantom.js scripts
+- boostrap.js - used to bootstrap puppeteer environment
+
+Local Modifications:
+
+- test/run_test.py was changed to run puppeteer instead of phantomjs
+- Certain tests under test/ were changed where tests were unreasonably strict in their expectations
+ (e.g. validating the exact format of error messages)
+- bootstrap.js was changed to accept native modules as function arguments.
+- test/run_test.py was enhanced to support "unsupported" directive
diff --git a/third_party/phantomjs/LICENSE.BSD b/third_party/phantomjs/LICENSE.BSD
new file mode 100644
index 00000000000..d5dfdd1f661
--- /dev/null
+++ b/third_party/phantomjs/LICENSE.BSD
@@ -0,0 +1,22 @@
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/third_party/phantomjs/bootstrap.js b/third_party/phantomjs/bootstrap.js
new file mode 100644
index 00000000000..f3a69c55009
--- /dev/null
+++ b/third_party/phantomjs/bootstrap.js
@@ -0,0 +1,235 @@
+/*jslint sloppy: true, nomen: true */
+/*global window:true,phantom:true */
+
+/*
+ This file is part of the PhantomJS project from Ofi Labs.
+
+ Copyright (C) 2011 Ariya Hidayat
+ Copyright (C) 2011 Ivan De Marino
+ Copyright (C) 2011 James Roe
+ Copyright (C) 2011 execjosh, http://execjosh.blogspot.com
+ Copyright (C) 2012 James M. Greene
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+(function(nativeExports) {
+ // CommonJS module implementation follows
+
+ window.global = window;
+ // fs is loaded at the end, when everything is ready
+ var fs;
+ var cache = {};
+ var paths = [];
+ var extensions = {
+ '.js': function(module, filename) {
+ var code = fs.read(filename);
+ module._compile(code);
+ },
+
+ '.json': function(module, filename) {
+ module.exports = JSON.parse(fs.read(filename));
+ }
+ };
+
+ function dirname(path) {
+ var replaced = path.replace(/\/[^\/]*\/?$/, '');
+ if (replaced == path) {
+ replaced = '';
+ }
+ return replaced;
+ }
+
+ function basename(path) {
+ return path.replace(/.*\//, '');
+ }
+
+ function joinPath() {
+ // It should be okay to hard-code a slash here.
+ // The FileSystem module returns a platform-specific
+ // separator, but the JavaScript engine only expects
+ // the slash.
+ var args = Array.prototype.slice.call(arguments);
+ return args.join('/');
+ }
+
+ function tryFile(path) {
+ if (fs.isFile(path)) return path;
+ return null;
+ }
+
+ function tryExtensions(path) {
+ var filename, exts = Object.keys(extensions);
+ for (var i=0; i