feat: introduce puppeteer-firefox (#3628)

This adds a proof-of-concept of `puppeteer-firefox`.
This consists of two parts:
- `//experimental/juggler` - patches to apply to Firefox.
- `//experimental/puppeteer-firefox` - front-end code to
be merged with Puppeteer.

As things become more stable, we'll gradually move it out of
the experimental folder.
This commit is contained in:
Andrey Lushnikov 2018-12-06 11:24:00 -08:00 committed by GitHub
parent 8613e871fc
commit 45ab3e0332
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
164 changed files with 12861 additions and 0 deletions

View File

@ -6,3 +6,4 @@ utils/testrunner/examples/
node6/*
node6-test/*
node6-testrunner/*
experimental/

8
experimental/README.md Normal file
View File

@ -0,0 +1,8 @@
#### What's this?
This is a **highly experimental** Puppeteer API to drive Firefox browser.
Beware, alligators live here.
`/juggler` - firefox-puppeteer backend
`/puppeteer-firefox` - puppeteer API for Firefox

View File

@ -0,0 +1,38 @@
task:
timeout_in: 120m
env:
CIRRUS_WORKING_DIR: /usr/local/src
SOURCE: /usr/local/src/
GS_AUTH: ENCRYPTED[c4b5b0404f5bfdc1b663a1eb5b70f3187b5d470d02eec3265b06b8e0d30226781523630931c1da6db06714c0d359f71f]
PATH: /root/.cargo/bin:$PATH:$SOURCE/gcloud/google-cloud-sdk/bin
SHELL: /bin/bash
container:
dockerfile: Dockerfile
# image: ubuntu
cpu: 8
memory: 24
name: linux
# install_deps_script: apt-get update && apt-get install -y wget python clang llvm git curl
install_gcloud_script: ./scripts/install_gcloud.sh
check_gcloud_script:
- echo "REVISION: $(git rev-parse HEAD)"
- gsutil cp FIREFOX_SHA gs://juggler-builds/$(git rev-parse HEAD)/
clone_firefox_script: ./scripts/fetch_firefox.sh
apply_patches_script:
- cd $SOURCE/firefox
- git config --global user.name "cirrus-ci-builder"
- git config --global user.email "aslushnikov@gmail.com"
- git am ../patches/*
- ln -s $PWD/../juggler testing/juggler
bootstrap_firefox_script:
- cd $SOURCE/firefox
- ./mach bootstrap --application-choice=browser --no-interactive
build_firefox_script:
- cd $SOURCE/firefox
- ./mach build
package_firefox_script:
- cd $SOURCE/firefox
- ./mach package
upload_build_to_gcloud_script:
- bash $SOURCE/scripts/upload_linux.sh

1
experimental/juggler/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
firefox/

View File

@ -0,0 +1,27 @@
FROM ubuntu:trusty
MAINTAINER Andrey Lushnikov <aslushnikov@gmail.com>
ENV SHELL=/bin/bash
# Install generic deps
RUN apt-get update -y && apt-get install -y wget python clang llvm git curl
# Install gcc7 (mach requires 6.1+)
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get dist-upgrade -y && \
apt-get install build-essential software-properties-common -y && \
add-apt-repository ppa:ubuntu-toolchain-r/test -y && \
apt-get update -y && \
apt-get install gcc-7 g++-7 -y && \
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 60 --slave /usr/bin/g++ g++ /usr/bin/g++-7 && \
update-alternatives --config gcc
# Install llvm 3.9.0 (mach requires 3.9.0+)
RUN echo "deb http://apt.llvm.org/trusty/ llvm-toolchain-trusty-3.9 main" >> /etc/apt/sources.list && \
echo "deb-src http://apt.llvm.org/trusty/ llvm-toolchain-trusty-3.9 main" >> /etc/apt/sources.list && \
apt-get install clang-3.9 lldb-3.9 -y
# Install python 3.6 (mach requires 3.5+)
RUN add-apt-repository ppa:deadsnakes/ppa -y && \
apt-get update -y && apt-get install python3.6 -y

View File

@ -0,0 +1 @@
663997bb1dd09a5d93135b1707feb59024eb9db4

View File

@ -0,0 +1,77 @@
# Juggler
> Juggler - Firefox Automation Protocol for implementing the Puppeteer API.
## Protocol
See [`//src/Protocol.js`](https://github.com/GoogleChrome/puppeteer/blob/master/experimental/juggler/src/Protocol.js).
## Building FF with Juggler
1. Clone Juggler repository
```bash
git clone https://github.com/aslushnikov/juggler
cd juggler
```
2. Checkout [pinned Firefox revision](https://github.com/aslushnikov/juggler/blob/master/FIREFOX_SHA) from mozilla [github mirror](https://github.com/mozilla/gecko-dev) into `//firefox` folder.
```bash
SOURCE=$PWD bash scripts/fetch_firefox.sh
```
3. Apply juggler patches to Firefox source code
```bash
cd firefox
git am ../patches/*
ln -s $PWD/../src $PWD/testing/juggler
```
4. Bootstrap host environment for Firefox build and compile firefox locally
```bash
# OPTIONAL - bootstrap host environment.
./mach bootstrap --application-choice=browser --no-interactive
# Compile browser
./mach build
```
## Running Firefox with Juggler
Juggle adds a `-juggler` CLI flag that accepts a port to expose a remote protocol on.
Pass `0` to pick a random port - Juggler will print its port to STDOUT.
```
./mach run -- -juggler 0
```
## Uploading builds to Google Storage
Firefox builds with Juggler support are uploaded to gs://juggler-builds/ bucket.
Project maintainers can upload builds.
To upload a build, do the following:
1. Install [gcloud](https://cloud.google.com/sdk/install) if you haven't yet.
2. Authenticate in the cloud and select project
```bash
gcloud auth login
gcloud config set project juggler-builds
```
3. Make sure **firefox is compiled**; after that, package a build for a redistribution:
```bash
cd firefox
./mach package
```
4. Archive build and copy to the gbucket
We want to ship `*.zip` archives so that it's easy to decompress them on the node-side.
- Linux: `./scripts/upload_linux.sh`
- Mac: `./scripts/upload_mac.sh`

View File

@ -0,0 +1,160 @@
From fb96032ad20cb0dc5fbabe52a80d13d6e6808bb8 Mon Sep 17 00:00:00 2001
From: Andrey Lushnikov <lushnikov@chromium.org>
Date: Tue, 27 Nov 2018 13:37:12 -0800
Subject: [PATCH 1/3] Introduce nsIWebProgressListener2::onFrameLocationChange
event
The event is fired when subframes commit navigation.
Juggler uses this event to track same-document iframe navigations.
---
docshell/base/nsDocShell.cpp | 1 +
.../statusfilter/nsBrowserStatusFilter.cpp | 10 ++++++++
uriloader/base/nsDocLoader.cpp | 20 ++++++++++++++++
uriloader/base/nsDocLoader.h | 5 ++++
uriloader/base/nsIWebProgress.idl | 7 +++++-
uriloader/base/nsIWebProgressListener2.idl | 23 +++++++++++++++++++
6 files changed, 65 insertions(+), 1 deletion(-)
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index ea0926732..3f738d39c 100644
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -1349,6 +1349,7 @@ nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest,
mLSHE->GetIsSubFrame(&isSubFrame);
}
+ FireOnFrameLocationChange(this, aRequest, aURI, aLocationFlags);
if (!isSubFrame && !isRoot) {
/*
* We don't want to send OnLocationChange notifications when
diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
index c4d04dcc4..bb9e40cca 100644
--- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
+++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
@@ -188,6 +188,16 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress *aWebProgress,
return NS_OK;
}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnFrameLocationChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ nsIURI *aLocation,
+ uint32_t aFlags)
+{
+ return NS_OK;
+}
+
NS_IMETHODIMP
nsBrowserStatusFilter::OnProgressChange(nsIWebProgress *aWebProgress,
nsIRequest *aRequest,
diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp
index 524681ad8..68d3f976c 100644
--- a/uriloader/base/nsDocLoader.cpp
+++ b/uriloader/base/nsDocLoader.cpp
@@ -1330,6 +1330,26 @@ nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress,
}
}
+void
+nsDocLoader::FireOnFrameLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI *aUri,
+ uint32_t aFlags)
+{
+ NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_FRAME_LOCATION,
+ nsCOMPtr<nsIWebProgressListener2> listener2 =
+ do_QueryReferent(info.mWeakListener);
+ if (!listener2)
+ continue;
+ listener2->OnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags);
+ );
+
+ // Pass the notification up to the parent...
+ if (mParent) {
+ mParent->FireOnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags);
+ }
+}
+
void
nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress,
nsIRequest* aRequest,
diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h
index 2dc1d0cae..05f8b2877 100644
--- a/uriloader/base/nsDocLoader.h
+++ b/uriloader/base/nsDocLoader.h
@@ -167,6 +167,11 @@ protected:
nsIURI *aUri,
uint32_t aFlags);
+ void FireOnFrameLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI *aUri,
+ uint32_t aFlags);
+
MOZ_MUST_USE bool RefreshAttempted(nsIWebProgress* aWebProgress,
nsIURI *aURI,
int32_t aDelay,
diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl
index 0549f32e1..3078e35d7 100644
--- a/uriloader/base/nsIWebProgress.idl
+++ b/uriloader/base/nsIWebProgress.idl
@@ -84,17 +84,22 @@ interface nsIWebProgress : nsISupports
* NOTIFY_REFRESH
* Receive onRefreshAttempted events.
* This is defined on nsIWebProgressListener2.
+ *
+ * NOTIFY_FRAME_LOCATION
+ * Receive onFrameLocationChange events.
+ * This is defined on nsIWebProgressListener2.
*/
const unsigned long NOTIFY_PROGRESS = 0x00000010;
const unsigned long NOTIFY_STATUS = 0x00000020;
const unsigned long NOTIFY_SECURITY = 0x00000040;
const unsigned long NOTIFY_LOCATION = 0x00000080;
const unsigned long NOTIFY_REFRESH = 0x00000100;
+ const unsigned long NOTIFY_FRAME_LOCATION = 0x00000200;
/**
* This flag enables all notifications.
*/
- const unsigned long NOTIFY_ALL = 0x000001ff;
+ const unsigned long NOTIFY_ALL = 0x000002ff;
/**
* Registers a listener to receive web progress events.
diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl
index 87701f8d2..8a69e6b29 100644
--- a/uriloader/base/nsIWebProgressListener2.idl
+++ b/uriloader/base/nsIWebProgressListener2.idl
@@ -66,4 +66,27 @@ interface nsIWebProgressListener2 : nsIWebProgressListener {
in nsIURI aRefreshURI,
in long aMillis,
in boolean aSameURI);
+
+ /**
+ * Called when the location of the window or its subframes changes. This is not
+ * when a load is requested, but rather once it is verified that the load is
+ * going to occur in the given window. For instance, a load that starts in a
+ * window might send progress and status messages for the new site, but it
+ * will not send the onLocationChange until we are sure that we are loading
+ * this new page here.
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRequest
+ * The associated nsIRequest. This may be null in some cases.
+ * @param aLocation
+ * The URI of the location that is being loaded.
+ * @param aFlags
+ * This is a value which explains the situation or the reason why
+ * the location has changed.
+ */
+ void onFrameLocationChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in nsIURI aLocation,
+ [optional] in unsigned long aFlags);
};
--
2.19.0.605.g01d371f741-goog

View File

@ -0,0 +1,24 @@
From c6f975dbc28b902cc271f79dedc42073ab1bde7d Mon Sep 17 00:00:00 2001
From: Andrey Lushnikov <lushnikov@chromium.org>
Date: Tue, 27 Nov 2018 13:39:00 -0800
Subject: [PATCH 2/3] Add Juggler to gecko build system
---
toolkit/toolkit.mozbuild | 1 +
1 file changed, 1 insertion(+)
diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild
index 4a0e5f172..b8abc1e72 100644
--- a/toolkit/toolkit.mozbuild
+++ b/toolkit/toolkit.mozbuild
@@ -163,6 +163,7 @@ if CONFIG['ENABLE_MARIONETTE']:
DIRS += [
'/testing/firefox-ui',
'/testing/marionette',
+ '/testing/juggler',
]
if CONFIG['ENABLE_GECKODRIVER'] and not CONFIG['MOZ_TSAN']:
--
2.19.0.605.g01d371f741-goog

View File

@ -0,0 +1,43 @@
From 1449495af094fbc5e1bb351f8387c3a341977763 Mon Sep 17 00:00:00 2001
From: Andrey Lushnikov <lushnikov@chromium.org>
Date: Thu, 29 Nov 2018 11:40:32 -0800
Subject: [PATCH 3/3] Add Juggler to mozilla packaging script
---
browser/installer/allowed-dupes.mn | 6 ++++++
browser/installer/package-manifest.in | 5 +++++
2 files changed, 11 insertions(+)
diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn
index 5685a30d9..32ba241b8 100644
--- a/browser/installer/allowed-dupes.mn
+++ b/browser/installer/allowed-dupes.mn
@@ -154,3 +154,9 @@ browser/defaults/settings/main/example.json
# Bug 1463748 - Fork and pref-off the new error pages
browser/chrome/browser/content/browser/aboutNetError-new.xhtml
browser/chrome/browser/content/browser/aboutNetError.xhtml
+
+# Juggler/marionette files
+chrome/juggler/content/content/floating-scrollbars.css
+browser/chrome/devtools/skin/floating-scrollbars-responsive-design.css
+chrome/juggler/content/server/stream-utils.js
+chrome/marionette/content/stream-utils.js
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
index 5b828784a..a5d9f9741 100644
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -338,6 +338,11 @@
@RESPATH@/defaults/pref/marionette.js
#endif
+@RESPATH@/chrome/juggler@JAREXT@
+@RESPATH@/chrome/juggler.manifest
+@RESPATH@/components/juggler.manifest
+@RESPATH@/components/juggler.js
+
@RESPATH@/components/nsAsyncShutdown.manifest
@RESPATH@/components/nsAsyncShutdown.js
--
2.19.0.605.g01d371f741-goog

View File

@ -0,0 +1,18 @@
set -e
set -x
if [ -d $SOURCE/firefox ]; then
echo ERROR! Directory "${SOURCE}/firefox" exists. Remove it and re-run this script.
exit 1;
fi
mkdir -p $SOURCE/firefox
cd $SOURCE/firefox
git init
git remote add origin https://github.com/mozilla/gecko-dev.git
git fetch --depth 50 origin release
git reset --hard $(cat $SOURCE/FIREFOX_SHA)
if [[ $? == 0 ]]; then
echo SUCCESS
else
echo FAILED TO CHECKOUT PINNED REVISION
fi

View File

@ -0,0 +1,9 @@
# auth
echo $GS_AUTH > $SOURCE/gsauth
# install gcloud sdk
curl https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz > /tmp/google-cloud-sdk.tar.gz
mkdir -p $SOURCE/gcloud \
&& tar -C $SOURCE/gcloud -xvf /tmp/google-cloud-sdk.tar.gz \
&& CLOUDSDK_CORE_DISABLE_PROMPTS=1 $SOURCE/gcloud/google-cloud-sdk/install.sh
gcloud auth activate-service-account --key-file=$SOURCE/gsauth
gcloud config set project juggler-builds

View File

@ -0,0 +1,13 @@
set -e
if [ -e ./FIREFOX_SHA ]; then
echo Checking Juggler root - OK
else
echo Please run this script from the Juggler root
exit 1;
fi
cd firefox/obj-x86_64-pc-linux-gnu/dist/
zip -r firefox-linux.zip firefox
mv firefox-linux.zip ../../../
cd -
gsutil mv firefox-linux.zip gs://juggler-builds/$(git rev-parse HEAD)/

View File

@ -0,0 +1,13 @@
set -e
if [ -e ./FIREFOX_SHA ]; then
echo Checking Juggler root - OK
else
echo Please run this script from the Juggler root
exit 1;
fi
cd firefox/obj-x86_64-apple-darwin17.7.0/dist/
zip -r firefox-mac.zip firefox
mv firefox-mac.zip ../../../
cd -
gsutil mv firefox-mac.zip gs://juggler-builds/$(git rev-parse HEAD)/

View File

@ -0,0 +1,149 @@
"use strict";
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {PageHandler} = ChromeUtils.import("chrome://juggler/content/PageHandler.jsm");
const {InsecureSweepingOverride} = ChromeUtils.import("chrome://juggler/content/InsecureSweepingOverride.js");
class BrowserHandler {
constructor(session) {
this._session = session;
this._mainWindowPromise = waitForBrowserWindow();
this._pageHandlers = new Map();
this._tabsToPageHandlers = new Map();
this._initializePages();
this._sweepingOverride = null;
}
async setIgnoreHTTPSErrors({enabled}) {
if (!enabled && this._sweepingOverride) {
this._sweepingOverride.unregister();
this._sweepingOverride = null;
Services.prefs.setBoolPref('security.mixed_content.block_active_content', true);
} else if (enabled && !this._sweepingOverride) {
this._sweepingOverride = new InsecureSweepingOverride();
this._sweepingOverride.register();
Services.prefs.setBoolPref('security.mixed_content.block_active_content', false);
}
}
async getInfo() {
const win = await this._mainWindowPromise;
const version = Components.classes["@mozilla.org/xre/app-info;1"]
.getService(Components.interfaces.nsIXULAppInfo)
.version;
const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"]
.getService(Components.interfaces.nsIHttpProtocolHandler)
.userAgent;
return {version: 'Firefox/' + version, userAgent};
}
async _initializePages() {
const win = await this._mainWindowPromise;
const tabs = win.gBrowser.tabs;
for (const tab of win.gBrowser.tabs)
this._ensurePageHandler(tab);
win.gBrowser.tabContainer.addEventListener('TabOpen', event => {
this._ensurePageHandler(event.target);
});
win.gBrowser.tabContainer.addEventListener('TabClose', event => {
this._removePageHandlerForTab(event.target);
});
}
pageForId(pageId) {
return this._pageHandlers.get(pageId) || null;
}
_ensurePageHandler(tab) {
if (this._tabsToPageHandlers.has(tab))
return this._tabsToPageHandlers.get(tab);
const pageHandler = new PageHandler(this._session, tab);
this._pageHandlers.set(pageHandler.id(), pageHandler);
this._tabsToPageHandlers.set(tab, pageHandler);
this._session.emitEvent('Browser.tabOpened', {
url: pageHandler.url(),
pageId: pageHandler.id()
});
return pageHandler;
}
_removePageHandlerForTab(tab) {
const pageHandler = this._tabsToPageHandlers.get(tab);
this._tabsToPageHandlers.delete(tab);
this._pageHandlers.delete(pageHandler.id());
pageHandler.dispose();
this._session.emitEvent('Browser.tabClosed', {pageId: pageHandler.id()});
}
async newPage() {
const win = await this._mainWindowPromise;
const tab = win.gBrowser.addTab('about:blank', {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
win.gBrowser.selectedTab = tab;
// Await navigation to about:blank
await new Promise(resolve => {
const wpl = {
onLocationChange: function(aWebProgress, aRequest, aLocation) {
tab.linkedBrowser.removeProgressListener(wpl);
resolve();
},
QueryInterface: ChromeUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
]),
};
tab.linkedBrowser.addProgressListener(wpl);
});
const pageHandler = this._ensurePageHandler(tab);
return {pageId: pageHandler.id()};
}
async closePage({pageId}) {
const win = await this._mainWindowPromise;
const pageHandler = this._pageHandlers.get(pageId);
await win.gBrowser.removeTab(pageHandler.tab());
}
}
/**
* @return {!Promise<Ci.nsIDOMChromeWindow>}
*/
async function waitForBrowserWindow() {
const windowsIt = Services.wm.getEnumerator('navigator:browser');
if (windowsIt.hasMoreElements())
return waitForWindowLoaded(windowsIt.getNext().QueryInterface(Ci.nsIDOMChromeWindow));
let fulfill;
let promise = new Promise(x => fulfill = x);
const listener = {
onOpenWindow: window => {
if (window instanceof Ci.nsIDOMChromeWindow) {
Services.wm.removeListener(listener);
fulfill(waitForWindowLoaded(window));
}
},
onCloseWindow: () => {}
};
Services.wm.addListener(listener);
return promise;
/**
* @param {!Ci.nsIDOMChromeWindow} window
* @return {!Promise<Ci.nsIDOMChromeWindow>}
*/
function waitForWindowLoaded(window) {
if (window.document.readyState === 'complete')
return window;
return new Promise(fulfill => {
window.addEventListener('load', function listener() {
window.removeEventListener('load', listener);
fulfill(window);
});
});
}
}
var EXPORTED_SYMBOLS = ['BrowserHandler'];
this.BrowserHandler = BrowserHandler;

View File

@ -0,0 +1,72 @@
const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/BrowserHandler.jsm");
const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/Protocol.js");
class ChromeSession {
constructor(connection) {
this._connection = connection;
this._connection.onmessage = this._dispatch.bind(this);
this._browserHandler = new BrowserHandler(this);
}
emitEvent(eventName, params) {
const scheme = protocol.events[eventName];
if (!scheme)
throw new Error(`ERROR: event '${eventName}' is not supported`);
const details = {};
if (!checkScheme(scheme, params || {}, details))
throw new Error(`ERROR: event '${eventName}' is called with ${details.errorType} parameter '${details.propertyName}': ${details.propertyValue}`);
this._connection.send({method: eventName, params});
}
async _dispatch(data) {
const id = data.id;
try {
const method = data.method;
const params = data.params || {};
if (!id)
throw new Error(`ERROR: every message must have an 'id' parameter`);
if (!method)
throw new Error(`ERROR: every message must have a 'method' parameter`);
const descriptor = protocol.methods[method];
if (!descriptor)
throw new Error(`ERROR: method '${method}' is not supported`);
let details = {};
if (!checkScheme(descriptor.params || {}, params, details))
throw new Error(`ERROR: method '${method}' is called with ${details.errorType} parameter '${details.propertyName}': ${details.propertyValue}`);
const result = await this._innerDispatch(method, params);
details = {};
if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details))
throw new Error(`ERROR: method '${method}' returned ${details.errorType} parameter '${details.propertyName}': ${details.propertyValue}`);
this._connection.send({id, result});
} catch (e) {
this._connection.send({id, error: {
message: e.message,
data: e.stack
}});
}
}
async _innerDispatch(method, params) {
const [domainName, methodName] = method.split('.');
if (domainName === 'Browser')
return await this._browserHandler[methodName](params);
if (domainName === 'Page') {
if (!params.pageId)
throw new Error('Parameter "pageId" must be present for Page.* methods');
const pageHandler = this._browserHandler.pageForId(params.pageId);
if (!pageHandler)
throw new Error('Failed to find page for id = ' + pageId);
return await pageHandler[methodName](params);
}
throw new Error(`INTERNAL ERROR: failed to dispatch '${method}'`);
}
}
this.EXPORTED_SYMBOLS = ['ChromeSession'];
this.ChromeSession = ChromeSession;

View File

@ -0,0 +1,46 @@
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
class Helper {
addObserver(handler, topic) {
Services.obs.addObserver(handler, topic);
return () => Services.obs.removeObserver(handler, topic);
}
addMessageListener(receiver, eventName, handler) {
receiver.addMessageListener(eventName, handler);
return () => receiver.removeMessageListener(eventName, handler);
}
addEventListener(receiver, eventName, handler) {
receiver.addEventListener(eventName, handler);
return () => receiver.removeEventListener(eventName, handler);
}
on(receiver, eventName, handler) {
// The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument.
// Fire event listeners without it for convenience.
const handlerWrapper = (_, ...args) => handler(...args);
receiver.on(eventName, handlerWrapper);
return () => receiver.off(eventName, handlerWrapper);
}
addProgressListener(progress, listener, flags) {
progress.addProgressListener(listener, flags);
return () => progress.removeProgressListener(listener);
}
removeListeners(listeners) {
for (const tearDown of listeners)
tearDown.call(null);
listeners.splice(0, listeners.length);
}
generateId() {
return uuidGen.generateUUID().toString();
}
}
var EXPORTED_SYMBOLS = [ "Helper" ];
this.Helper = Helper;

View File

@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
ChromeUtils.import("resource://gre/modules/Preferences.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const registrar =
Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
const sss = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
const CERT_PINNING_ENFORCEMENT_PREF = "security.cert_pinning.enforcement_level";
const CID = Components.ID("{4b67cce0-a51c-11e6-9598-0800200c9a66}");
const CONTRACT_ID = "@mozilla.org/security/certoverride;1";
const DESC = "All-encompassing cert service that matches on a bitflag";
const HSTS_PRELOAD_LIST_PREF = "network.stricttransportsecurity.preloadlist";
const Error = {
Untrusted: 1,
Mismatch: 2,
Time: 4,
};
/**
* Certificate override service that acts in an all-inclusive manner
* on TLS certificates.
*
* @throws {Components.Exception}
* If there are any problems registering the service.
*/
function InsecureSweepingOverride() {
// This needs to be an old-style class with a function constructor
// and prototype assignment because... XPCOM. Any attempt at
// modernisation will be met with cryptic error messages which will
// make your life miserable.
let service = function() {};
service.prototype = {
hasMatchingOverride(
aHostName, aPort, aCert, aOverrideBits, aIsTemporary) {
aIsTemporary.value = false;
aOverrideBits.value = Error.Untrusted | Error.Mismatch | Error.Time;
return true;
},
QueryInterface: ChromeUtils.generateQI([Ci.nsICertOverrideService]),
};
let factory = XPCOMUtils.generateSingletonFactory(service);
return {
register() {
// make it possible to register certificate overrides for domains
// that use HSTS or HPKP
Preferences.set(HSTS_PRELOAD_LIST_PREF, false);
Preferences.set(CERT_PINNING_ENFORCEMENT_PREF, 0);
registrar.registerFactory(CID, DESC, CONTRACT_ID, factory);
},
unregister() {
registrar.unregisterFactory(CID, factory);
Preferences.reset(HSTS_PRELOAD_LIST_PREF);
Preferences.reset(CERT_PINNING_ENFORCEMENT_PREF);
// clear collected HSTS and HPKP state
// through the site security service
sss.clearAll();
sss.clearPreloads();
},
};
}
this.EXPORTED_SYMBOLS = ["InsecureSweepingOverride"];
this.InsecureSweepingOverride = InsecureSweepingOverride;

View File

@ -0,0 +1,337 @@
"use strict";
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
const FRAME_SCRIPT = "chrome://juggler/content/content/ContentSession.js";
const helper = new Helper();
class PageHandler {
constructor(chromeSession, tab) {
this._pageId = helper.generateId();
this._chromeSession = chromeSession;
this._tab = tab;
this._browser = tab.linkedBrowser;
this._enabled = false;
this.QueryInterface = ChromeUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
]);
this._browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
this._dialogs = new Map();
// First navigation always happens to about:blank - do not report it.
this._skipNextNavigation = true;
}
async setViewport({viewport}) {
if (viewport) {
const {width, height} = viewport;
this._browser.style.setProperty('min-width', width + 'px');
this._browser.style.setProperty('min-height', height + 'px');
this._browser.style.setProperty('max-width', width + 'px');
this._browser.style.setProperty('max-height', height + 'px');
} else {
this._browser.style.removeProperty('min-width');
this._browser.style.removeProperty('min-height');
this._browser.style.removeProperty('max-width');
this._browser.style.removeProperty('max-height');
}
const dimensions = this._browser.getBoundingClientRect();
await Promise.all([
this._contentSession.send('setViewport', {
deviceScaleFactor: viewport ? viewport.deviceScaleFactor : 0,
isMobile: viewport && viewport.isMobile,
hasTouch: viewport && viewport.hasTouch,
}),
this._contentSession.send('awaitViewportDimensions', {
width: dimensions.width,
height: dimensions.height
}),
]);
}
_initializeDialogEvents() {
this._browser.addEventListener('DOMWillOpenModalDialog', async (event) => {
// wait for the dialog to be actually added to DOM.
await Promise.resolve();
this._updateModalDialogs();
});
this._browser.addEventListener('DOMModalDialogClosed', (event) => {
this._updateModalDialogs();
});
this._updateModalDialogs();
}
_updateModalDialogs() {
const elements = new Set(this._browser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"));
for (const dialog of this._dialogs.values()) {
if (!elements.has(dialog.element())) {
this._dialogs.delete(dialog.id());
this._chromeSession.emitEvent('Page.dialogClosed', {
pageId: this._pageId,
dialogId: dialog.id(),
});
} else {
elements.delete(dialog.element());
}
}
for (const element of elements) {
const dialog = Dialog.createIfSupported(element);
if (!dialog)
continue;
this._dialogs.set(dialog.id(), dialog);
this._chromeSession.emitEvent('Page.dialogOpened', {
pageId: this._pageId,
dialogId: dialog.id(),
type: dialog.type(),
message: dialog.message(),
defaultValue: dialog.defaultValue(),
});
}
}
onLocationChange(aWebProgress, aRequest, aLocation) {
if (this._skipNextNavigation) {
this._skipNextNavigation = false;
return;
}
this._chromeSession.emitEvent('Browser.tabNavigated', {
pageId: this._pageId,
url: aLocation.spec
});
}
url() {
return this._browser.currentURI.spec;
}
tab() {
return this._tab;
}
id() {
return this._pageId;
}
async enable() {
if (this._enabled)
return;
this._enabled = true;
this._initializeDialogEvents();
this._contentSession = new ContentSession(this._chromeSession, this._browser, this._pageId);
await this._contentSession.send('enable');
}
async screenshot(options) {
return await this._contentSession.send('screenshot', options);
}
async getBoundingBox(options) {
return await this._contentSession.send('getBoundingBox', options);
}
async getContentQuads(options) {
return await this._contentSession.send('getContentQuads', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async navigate(options) {
return await this._contentSession.send('navigate', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async goBack(options) {
return await this._contentSession.send('goBack', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async goForward(options) {
return await this._contentSession.send('goForward', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async reload(options) {
return await this._contentSession.send('reload', options);
}
/**
* @param {{functionText: String, frameId: String}} options
* @return {!Promise<*>}
*/
async evaluate(options) {
return await this._contentSession.send('evaluate', options);
}
async getObjectProperties(options) {
return await this._contentSession.send('getObjectProperties', options);
}
async addScriptToEvaluateOnNewDocument(options) {
return await this._contentSession.send('addScriptToEvaluateOnNewDocument', options);
}
async removeScriptToEvaluateOnNewDocument(options) {
return await this._contentSession.send('removeScriptToEvaluateOnNewDocument', options);
}
async disposeObject(options) {
return await this._contentSession.send('disposeObject', options);
}
async dispatchKeyEvent(options) {
return await this._contentSession.send('dispatchKeyEvent', options);
}
async dispatchMouseEvent(options) {
return await this._contentSession.send('dispatchMouseEvent', options);
}
async insertText(options) {
return await this._contentSession.send('insertText', options);
}
async handleDialog({dialogId, accept, promptText}) {
const dialog = this._dialogs.get(dialogId);
if (!dialog)
throw new Error('Failed to find dialog with id = ' + dialogId);
if (accept)
dialog.accept(promptText);
else
dialog.dismiss();
}
dispose() {
this._browser.removeProgressListener(this);
if (this._contentSession) {
this._contentSession.dispose();
this._contentSession = null;
}
}
}
class ContentSession {
constructor(chromeSession, browser, pageId) {
this._chromeSession = chromeSession;
this._browser = browser;
this._pageId = pageId;
this._messageId = 0;
this._pendingMessages = new Map();
this._sessionId = helper.generateId();
this._browser.messageManager.sendAsyncMessage('juggler:create-content-session', this._sessionId);
this._eventListeners = [
helper.addMessageListener(this._browser.messageManager, this._sessionId, {
receiveMessage: message => this._onMessage(message)
}),
];
}
dispose() {
helper.removeListeners(this._eventListeners);
for (const {resolve, reject} of this._pendingMessages.values())
reject(new Error('Page closed.'));
this._pendingMessages.clear();
}
/**
* @param {string} methodName
* @param {*} params
* @return {!Promise<*>}
*/
send(methodName, params) {
const id = ++this._messageId;
const promise = new Promise((resolve, reject) => {
this._pendingMessages.set(id, {resolve, reject});
});
this._browser.messageManager.sendAsyncMessage(this._sessionId, {id, methodName, params});
return promise;
}
_onMessage({data}) {
if (data.id) {
let id = data.id;
const {resolve, reject} = this._pendingMessages.get(data.id);
this._pendingMessages.delete(data.id);
if (data.error)
reject(new Error(data.error));
else
resolve(data.result);
} else {
const {
eventName,
params = {}
} = data;
params.pageId = this._pageId;
this._chromeSession.emitEvent(eventName, params);
}
}
}
class Dialog {
static createIfSupported(element) {
const type = element.Dialog.args.promptType;
switch (type) {
case 'alert':
case 'prompt':
case 'confirm':
return new Dialog(element, type);
case 'confirmEx':
return new Dialog(element, 'beforeunload');
default:
return null;
};
}
constructor(element, type) {
this._id = helper.generateId();
this._type = type;
this._element = element;
}
id() {
return this._id;
}
message() {
return this._element.ui.infoBody.textContent;
}
type() {
return this._type;
}
element() {
return this._element;
}
dismiss() {
if (this._element.ui.button1)
this._element.ui.button1.click();
else
this._element.ui.button0.click();
}
defaultValue() {
return this._element.ui.loginTextbox.value;
}
accept(promptValue) {
if (typeof promptValue === 'string' && this._type === 'prompt')
this._element.ui.loginTextbox.value = promptValue;
this._element.ui.button0.click();
}
}
var EXPORTED_SYMBOLS = ['PageHandler'];
this.PageHandler = PageHandler;

View File

@ -0,0 +1,371 @@
const t = {
String: x => typeof x === 'string' || typeof x === 'String',
Number: x => typeof x === 'number',
Boolean: x => typeof x === 'boolean',
Null: x => Object.is(x, null),
Enum: values => x => values.indexOf(x) !== -1,
Undefined: x => Object.is(x, undefined),
Or: (...schemes) => x => schemes.some(scheme => checkScheme(scheme, x)),
Either: (...schemes) => x => schemes.map(scheme => checkScheme(scheme, x)).reduce((acc, x) => acc + (x ? 1 : 0)) === 1,
Array: scheme => x => Array.isArray(x) && x.every(element => checkScheme(scheme, element)),
Nullable: scheme => x => Object.is(x, null) || checkScheme(scheme, x),
Optional: scheme => x => Object.is(x, undefined) || checkScheme(scheme, x),
Any: x => true,
}
const RemoteObject = t.Either(
{
type: t.Enum(['object', 'function', 'undefined', 'string', 'number', 'boolean', 'symbol', 'bigint']),
subtype: t.Optional(t.Enum(['array', 'null', 'node', 'regexp', 'date', 'map', 'set', 'weakmap', 'weakset', 'error', 'proxy', 'promise', 'typedarray'])),
objectId: t.String,
},
{
unserializableValue: t.Enum(['Infinity', '-Infinity', '-0', 'NaN']),
},
{
value: t.Any
},
);
const DOMPoint = {
x: t.Number,
y: t.Number,
};
const DOMQuad = {
p1: DOMPoint,
p2: DOMPoint,
p3: DOMPoint,
p4: DOMPoint
};
const protocol = {
methods: {
'Browser.getInfo': {
returns: {
userAgent: t.String,
version: t.String,
},
},
'Browser.setIgnoreHTTPSErrors': {
params: {
enabled: t.Boolean,
},
},
'Browser.newPage': {
returns: {
pageId: t.String,
}
},
'Browser.closePage': {
params: {
pageId: t.String,
},
},
'Page.enable': {
params: {
pageId: t.String,
},
},
'Page.setViewport': {
params: {
pageId: t.String,
viewport: t.Nullable({
width: t.Number,
height: t.Number,
deviceScaleFactor: t.Number,
isMobile: t.Boolean,
hasTouch: t.Boolean,
isLandscape: t.Boolean,
}),
},
},
'Page.evaluate': {
params: t.Either({
pageId: t.String,
frameId: t.String,
functionText: t.String,
returnByValue: t.Optional(t.Boolean),
args: t.Array(t.Either(
{ objectId: t.String },
{ unserializableValue: t.Enum(['Infinity', '-Infinity', '-0', 'NaN']) },
{ value: t.Any },
)),
}, {
pageId: t.String,
frameId: t.String,
script: t.String,
returnByValue: t.Optional(t.Boolean),
}),
returns: {
result: t.Optional(RemoteObject),
exceptionDetails: t.Optional({
text: t.Optional(t.String),
stack: t.Optional(t.String),
value: t.Optional(t.Any),
}),
}
},
'Page.addScriptToEvaluateOnNewDocument': {
params: {
pageId: t.String,
script: t.String,
},
returns: {
scriptId: t.String,
}
},
'Page.removeScriptToEvaluateOnNewDocument': {
params: {
pageId: t.String,
scriptId: t.String,
},
},
'Page.disposeObject': {
params: {
pageId: t.String,
frameId: t.String,
objectId: t.String,
},
},
'Page.getObjectProperties': {
params: {
pageId: t.String,
frameId: t.String,
objectId: t.String,
},
returns: {
properties: t.Array({
name: t.String,
value: RemoteObject,
}),
}
},
'Page.navigate': {
params: {
pageId: t.String,
frameId: t.String,
url: t.String,
},
returns: {
navigationId: t.Nullable(t.String),
navigationURL: t.Nullable(t.String),
}
},
'Page.goBack': {
params: {
pageId: t.String,
frameId: t.String,
},
returns: {
navigationId: t.Nullable(t.String),
navigationURL: t.Nullable(t.String),
}
},
'Page.goForward': {
params: {
pageId: t.String,
frameId: t.String,
},
returns: {
navigationId: t.Nullable(t.String),
navigationURL: t.Nullable(t.String),
}
},
'Page.reload': {
params: {
pageId: t.String,
frameId: t.String,
},
returns: {
navigationId: t.String,
navigationURL: t.String,
}
},
'Page.getBoundingBox': {
params: {
pageId: t.String,
frameId: t.String,
objectId: t.String,
},
returns: t.Nullable({
x: t.Number,
y: t.Number,
width: t.Number,
height: t.Number,
}),
},
'Page.screenshot': {
params: {
pageId: t.String,
mimeType: t.Enum(['image/png', 'image/jpeg']),
fullPage: t.Optional(t.Boolean),
clip: t.Optional({
x: t.Number,
y: t.Number,
width: t.Number,
height: t.Number,
})
},
returns: {
data: t.String,
}
},
'Page.getContentQuads': {
params: {
pageId: t.String,
frameId: t.String,
objectId: t.String,
},
returns: {
quads: t.Array(DOMQuad),
},
},
'Page.dispatchKeyEvent': {
params: {
pageId: t.String,
type: t.String,
key: t.String,
keyCode: t.Number,
location: t.Number,
code: t.String,
repeat: t.Boolean,
}
},
'Page.dispatchMouseEvent': {
params: {
pageId: t.String,
type: t.String,
button: t.Number,
x: t.Number,
y: t.Number,
modifiers: t.Number,
clickCount: t.Optional(t.Number),
buttons: t.Number,
}
},
'Page.insertText': {
params: {
pageId: t.String,
text: t.String,
}
},
'Page.handleDialog': {
params: {
pageId: t.String,
dialogId: t.String,
accept: t.Boolean,
promptText: t.Optional(t.String),
},
},
},
events: {
'Browser.tabOpened': {
pageId: t.String,
url: t.String,
},
'Browser.tabClosed': { pageId: t.String, },
'Browser.tabNavigated': {
pageId: t.String,
url: t.String
},
'Page.eventFired': {
pageId: t.String,
frameId: t.String,
name: t.Enum(['load', 'DOMContentLoaded']),
},
'Page.uncaughtError': {
pageId: t.String,
frameId: t.String,
message: t.String,
stack: t.String,
},
'Page.frameAttached': {
pageId: t.String,
frameId: t.String,
parentFrameId: t.Optional(t.String),
},
'Page.frameDetached': {
pageId: t.String,
frameId: t.String,
},
'Page.navigationStarted': {
pageId: t.String,
frameId: t.String,
navigationId: t.String,
url: t.String,
},
'Page.navigationCommitted': {
pageId: t.String,
frameId: t.String,
navigationId: t.String,
url: t.String,
// frame.id or frame.name
name: t.String,
},
'Page.navigationAborted': {
pageId: t.String,
frameId: t.String,
navigationId: t.String,
errorText: t.String,
},
'Page.sameDocumentNavigation': {
pageId: t.String,
frameId: t.String,
url: t.String,
},
'Page.consoleAPICalled': {
pageId: t.String,
frameId: t.String,
args: t.Array(RemoteObject),
type: t.String,
},
'Page.dialogOpened': {
pageId: t.String,
dialogId: t.String,
type: t.Enum(['prompt', 'alert', 'confirm', 'beforeunload']),
message: t.String,
defaultValue: t.Optional(t.String),
},
'Page.dialogClosed': {
pageId: t.String,
dialogId: t.String,
},
},
}
function checkScheme(scheme, x, details = {}, path = []) {
if (typeof scheme === 'object') {
for (const [propertyName, check] of Object.entries(scheme)) {
path.push(propertyName);
const result = checkScheme(check, x[propertyName], details, path);
path.pop();
if (!result)
return false;
}
for (const propertyName of Object.keys(x)) {
if (!scheme[propertyName]) {
path.push(propertyName);
details.propertyName = path.join('.');
details.propertyValue = x[propertyName];
details.errorType = 'extra';
return false;
}
}
return true;
}
const result = scheme(x);
if (!result) {
details.propertyName = path.join('.');
details.propertyValue = x;
details.errorType = 'unsupported';
}
return result;
}
this.protocol = protocol;
this.checkScheme = checkScheme;
this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];

View File

@ -0,0 +1,63 @@
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {TCPListener} = ChromeUtils.import("chrome://juggler/content/server/server.js");
const {ChromeSession} = ChromeUtils.import("chrome://juggler/content/ChromeSession.js");
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const FRAME_SCRIPT = "chrome://juggler/content/content/main.js";
// Command Line Handler
function CommandLineHandler() {
this._port = 0;
};
CommandLineHandler.prototype = {
classDescription: "Sample command-line handler",
classID: Components.ID('{f7a74a33-e2ab-422d-b022-4fb213dd2639}'),
contractID: "@mozilla.org/remote/juggler;1",
_xpcom_categories: [{
category: "command-line-handler",
entry: "m-juggler"
}],
/* nsICommandLineHandler */
handle: async function(cmdLine) {
const jugglerFlag = cmdLine.handleFlagWithParam("juggler", false);
if (!jugglerFlag || isNaN(jugglerFlag))
return;
this._port = parseInt(jugglerFlag, 10);
Services.obs.addObserver(this, 'sessionstore-windows-restored');
},
observe: function(subject, topic) {
Services.obs.removeObserver(this, 'sessionstore-windows-restored');
this._server = new TCPListener();
this._sessions = new Map();
this._server.onconnectioncreated = connection => {
this._sessions.set(connection, new ChromeSession(connection));
}
this._server.onconnectionclosed = connection => {
this._sessions.delete(connection);
}
const runningPort = this._server.start(this._port);
Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */);
dump('Juggler listening on ' + runningPort + '\n');
},
QueryInterface: ChromeUtils.generateQI([ Ci.nsICommandLineHandler ]),
// CHANGEME: change the help info as appropriate, but
// follow the guidelines in nsICommandLineHandler.idl
// specifically, flag descriptions should start at
// character 24, and lines should be wrapped at
// 72 characters with embedded newlines,
// and finally, the string should end with a newline
helpInfo : " --juggler Enable Juggler automation\n"
};
var NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandLineHandler]);

View File

@ -0,0 +1,3 @@
component {f7a74a33-e2ab-422d-b022-4fb213dd2639} juggler.js
contract @mozilla.org/remote/juggler;1 {f7a74a33-e2ab-422d-b022-4fb213dd2639}
category command-line-handler m-juggler @mozilla.org/remote/juggler;1

View File

@ -0,0 +1,9 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_COMPONENTS += [
"juggler.js",
"juggler.manifest",
]

View File

@ -0,0 +1,53 @@
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {RuntimeAgent} = ChromeUtils.import('chrome://juggler/content/content/RuntimeAgent.js');
const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
const helper = new Helper();
class ContentSession {
/**
* @param {string} sessionId
* @param {!ContentFrameMessageManager} messageManager
* @param {!FrameTree} frameTree
*/
constructor(sessionId, messageManager, frameTree, scrollbarManager) {
this._sessionId = sessionId;
this._runtimeAgent = new RuntimeAgent();
this._messageManager = messageManager;
this._pageAgent = new PageAgent(this, this._runtimeAgent, frameTree, scrollbarManager);
this._eventListeners = [
helper.addMessageListener(messageManager, this._sessionId, this._onMessage.bind(this)),
];
}
emitEvent(eventName, params) {
this._messageManager.sendAsyncMessage(this._sessionId, {eventName, params});
}
mm() {
return this._messageManager;
}
async _onMessage(msg) {
const id = msg.data.id;
try {
const handler = this._pageAgent[msg.data.methodName];
if (!handler)
throw new Error('unknown method: "' + msg.data.methodName + '"');
const result = await handler.call(this._pageAgent, msg.data.params);
this._messageManager.sendAsyncMessage(this._sessionId, {id, result});
} catch (e) {
this._messageManager.sendAsyncMessage(this._sessionId, {id, error: e.message + '\n' + e.stack});
}
}
dispose() {
helper.removeListeners(this._eventListeners);
this._pageAgent.dispose();
this._runtimeAgent.dispose();
}
}
var EXPORTED_SYMBOLS = ['ContentSession'];
this.ContentSession = ContentSession;

View File

@ -0,0 +1,287 @@
"use strict";
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
const helper = new Helper();
class FrameTree {
constructor(rootDocShell) {
EventEmitter.decorate(this);
this._docShellToFrame = new Map();
this._frameIdToFrame = new Map();
this._mainFrame = this._createFrame(rootDocShell);
const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
this.QueryInterface = ChromeUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsIWebProgressListener2,
Ci.nsISupportsWeakReference,
]);
const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION;
this._eventListeners = [
helper.addObserver(subject => this._onDocShellCreated(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-create'),
helper.addObserver(subject => this._onDocShellDestroyed(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-destroy'),
helper.addProgressListener(webProgress, this, flags),
];
}
frameForDocShell(docShell) {
return this._docShellToFrame.get(docShell) || null;
}
frame(frameId) {
return this._frameIdToFrame.get(frameId) || null;
}
frames() {
let result = [];
collect(this._mainFrame);
return result;
function collect(frame) {
result.push(frame);
for (const subframe of frame._children)
collect(subframe);
}
}
mainFrame() {
return this._mainFrame;
}
dispose() {
helper.removeListeners(this._eventListeners);
}
onStateChange(progress, request, flag, status) {
if (!(request instanceof Ci.nsIChannel))
return;
const channel = request.QueryInterface(Ci.nsIChannel);
const docShell = progress.DOMWindow.docShell;
const frame = this._docShellToFrame.get(docShell);
if (!frame) {
dump(`ERROR: got a state changed event for un-tracked docshell!\n`);
return;
}
const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING;
const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
if (isStart) {
// Starting a new navigation.
frame._pendingNavigationId = helper.generateId();
frame._pendingNavigationURL = channel.URI.spec;
this.emit(FrameTree.Events.NavigationStarted, frame);
} else if (isTransferring || (isStop && frame._pendingNavigationId && !status)) {
// Navigation is committed.
for (const subframe of frame._children)
this._detachFrame(subframe);
const navigationId = frame._pendingNavigationId;
frame._pendingNavigationId = null;
frame._pendingNavigationURL = null;
frame._lastCommittedNavigationId = navigationId;
frame._url = channel.URI.spec;
this.emit(FrameTree.Events.NavigationCommitted, frame);
} else if (isStop && frame._pendingNavigationId && status) {
// Navigation is aborted.
const navigationId = frame._pendingNavigationId;
frame._pendingNavigationId = null;
frame._pendingNavigationURL = null;
this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, getErrorStatusText(status));
}
}
onFrameLocationChange(progress, request, location, flags) {
const docShell = progress.DOMWindow.docShell;
const frame = this._docShellToFrame.get(docShell);
const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
if (frame && sameDocumentNavigation) {
frame._url = location.spec;
this.emit(FrameTree.Events.SameDocumentNavigation, frame);
}
}
_onDocShellCreated(docShell) {
// Bug 1142752: sometimes, the docshell appears to be immediately
// destroyed, bailout early to prevent random exceptions.
if (docShell.isBeingDestroyed())
return;
// If this docShell doesn't belong to our frame tree - do nothing.
let root = docShell;
while (root.parent)
root = root.parent;
if (root === this._mainFrame._docShell)
this._createFrame(docShell);
}
_createFrame(docShell) {
const parentFrame = this._docShellToFrame.get(docShell.parent) || null;
const frame = new Frame(this, docShell, parentFrame);
this._docShellToFrame.set(docShell, frame);
this._frameIdToFrame.set(frame.id(), frame);
this.emit(FrameTree.Events.FrameAttached, frame);
return frame;
}
_onDocShellDestroyed(docShell) {
const frame = this._docShellToFrame.get(docShell);
if (frame)
this._detachFrame(frame);
}
_detachFrame(frame) {
// Detach all children first
for (const subframe of frame._children)
this._detachFrame(subframe);
this._docShellToFrame.delete(frame._docShell);
this._frameIdToFrame.delete(frame.id());
if (frame._parentFrame)
frame._parentFrame._children.delete(frame);
frame._parentFrame = null;
this.emit(FrameTree.Events.FrameDetached, frame);
}
}
FrameTree.Events = {
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
NavigationStarted: 'navigationstarted',
NavigationCommitted: 'navigationcommitted',
NavigationAborted: 'navigationaborted',
SameDocumentNavigation: 'samedocumentnavigation',
};
class Frame {
constructor(frameTree, docShell, parentFrame) {
this._frameTree = frameTree;
this._docShell = docShell;
this._children = new Set();
this._frameId = helper.generateId();
this._parentFrame = null;
this._url = '';
if (parentFrame) {
this._parentFrame = parentFrame;
parentFrame._children.add(this);
}
this._lastCommittedNavigationId = null;
this._pendingNavigationId = null;
this._pendingNavigationURL = null;
this._textInputProcessor = null;
}
textInputProcessor() {
if (!this._textInputProcessor) {
this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor);
this._textInputProcessor.beginInputTransactionForTests(this._docShell.DOMWindow);
}
return this._textInputProcessor;
}
pendingNavigationId() {
return this._pendingNavigationId;
}
pendingNavigationURL() {
return this._pendingNavigationURL;
}
lastCommittedNavigationId() {
return this._lastCommittedNavigationId;
}
docShell() {
return this._docShell;
}
domWindow() {
return this._docShell.DOMWindow;
}
name() {
const frameElement = this._docShell.domWindow.frameElement;
let name = '';
if (frameElement)
name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || '';
return name;
}
parentFrame() {
return this._parentFrame;
}
id() {
return this._frameId;
}
url() {
return this._url;
}
}
function getErrorStatusText(status) {
if (!status)
return null;
for (const key of Object.keys(Cr)) {
if (Cr[key] === status)
return key;
}
// Security module. The following is taken from
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL
if ((status & 0xff0000) === 0x5a0000) {
// NSS_SEC errors (happen below the base value because of negative vals)
if ((status & 0xffff) < Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)) {
// The bases are actually negative, so in our positive numeric space, we
// need to subtract the base off our value.
const nssErr = Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
switch (nssErr) {
case 11:
return 'SEC_ERROR_EXPIRED_CERTIFICATE';
case 12:
return 'SEC_ERROR_REVOKED_CERTIFICATE';
case 13:
return 'SEC_ERROR_UNKNOWN_ISSUER';
case 20:
return 'SEC_ERROR_UNTRUSTED_ISSUER';
case 21:
return 'SEC_ERROR_UNTRUSTED_CERT';
case 36:
return 'SEC_ERROR_CA_CERT_INVALID';
case 90:
return 'SEC_ERROR_INADEQUATE_KEY_USAGE';
case 176:
return 'SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED';
default:
return 'SEC_ERROR_UNKNOWN';
}
}
const sslErr = Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
switch (sslErr) {
case 3:
return 'SSL_ERROR_NO_CERTIFICATE';
case 4:
return 'SSL_ERROR_BAD_CERTIFICATE';
case 8:
return 'SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE';
case 9:
return 'SSL_ERROR_UNSUPPORTED_VERSION';
case 12:
return 'SSL_ERROR_BAD_CERT_DOMAIN';
default:
return 'SSL_ERROR_UNKNOWN';
}
}
return '<unknown error>';
}
var EXPORTED_SYMBOLS = ['FrameTree'];
this.FrameTree = FrameTree;

View File

@ -0,0 +1,460 @@
"use strict";
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
const helper = new Helper();
class PageAgent {
constructor(session, runtimeAgent, frameTree, scrollbarManager) {
this._session = session;
this._runtime = runtimeAgent;
this._frameTree = frameTree;
this._scrollbarManager = scrollbarManager;
this._frameToExecutionContext = new Map();
this._scriptsToEvaluateOnNewDocument = new Map();
this._eventListeners = [];
this._enabled = false;
const docShell = frameTree.mainFrame().docShell();
this._initialDPPX = docShell.contentViewer.overrideDPPX;
this._customScrollbars = null;
}
async awaitViewportDimensions({width, height}) {
const win = this._frameTree.mainFrame().domWindow();
if (win.innerWidth === width && win.innerHeight === height)
return;
await new Promise(resolve => {
const listener = helper.addEventListener(win, 'resize', () => {
if (win.innerWidth === width && win.innerHeight === height) {
helper.removeListeners([listener]);
resolve();
}
});
});
}
async setViewport({deviceScaleFactor, isMobile, hasTouch}) {
const docShell = this._frameTree.mainFrame().docShell();
docShell.contentViewer.overrideDPPX = deviceScaleFactor || this._initialDPPX;
docShell.deviceSizeIsPageSize = isMobile;
docShell.touchEventsOverride = hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE;
this._scrollbarManager.setFloatingScrollbars(isMobile);
}
addScriptToEvaluateOnNewDocument({script}) {
const scriptId = helper.generateId();
this._scriptsToEvaluateOnNewDocument.set(scriptId, script);
return {scriptId};
}
removeScriptToEvaluateOnNewDocument({scriptId}) {
this._scriptsToEvaluateOnNewDocument.delete(scriptId);
}
enable() {
if (this._enabled)
return;
this._enabled = true;
this._eventListeners = [
helper.addObserver(this._consoleAPICalled.bind(this), "console-api-log-event"),
helper.addEventListener(this._session.mm(), 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)),
helper.addEventListener(this._session.mm(), 'pageshow', this._onLoad.bind(this)),
helper.addEventListener(this._session.mm(), 'DOMWindowCreated', this._onDOMWindowCreated.bind(this)),
helper.addEventListener(this._session.mm(), 'error', this._onError.bind(this)),
helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)),
helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)),
helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)),
helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)),
helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)),
helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)),
];
// Dispatch frameAttached events for all initial frames
for (const frame of this._frameTree.frames()) {
this._onFrameAttached(frame);
if (frame.url())
this._onNavigationCommitted(frame);
if (frame.pendingNavigationId())
this._onNavigationStarted(frame);
}
}
_onDOMContentLoaded(event) {
const docShell = event.target.ownerGlobal.docShell;
const frame = this._frameTree.frameForDocShell(docShell);
if (!frame)
return;
this._session.emitEvent('Page.eventFired', {
frameId: frame.id(),
name: 'DOMContentLoaded',
});
}
_onError(errorEvent) {
const docShell = errorEvent.target.ownerGlobal.docShell;
const frame = this._frameTree.frameForDocShell(docShell);
if (!frame)
return;
this._session.emitEvent('Page.uncaughtError', {
frameId: frame.id(),
message: errorEvent.message,
stack: errorEvent.error.stack
});
}
_onLoad(event) {
const docShell = event.target.ownerGlobal.docShell;
const frame = this._frameTree.frameForDocShell(docShell);
if (!frame)
return;
this._session.emitEvent('Page.eventFired', {
frameId: frame.id(),
name: 'load'
});
}
_onNavigationStarted(frame) {
this._session.emitEvent('Page.navigationStarted', {
frameId: frame.id(),
navigationId: frame.pendingNavigationId(),
url: frame.pendingNavigationURL(),
});
}
_onNavigationAborted(frame, navigationId, errorText) {
this._session.emitEvent('Page.navigationAborted', {
frameId: frame.id(),
navigationId,
errorText,
});
}
_onSameDocumentNavigation(frame) {
this._session.emitEvent('Page.sameDocumentNavigation', {
frameId: frame.id(),
url: frame.url(),
});
}
_onNavigationCommitted(frame) {
const context = this._frameToExecutionContext.get(frame);
if (context) {
this._runtime.destroyExecutionContext(context);
this._frameToExecutionContext.delete(frame);
}
this._session.emitEvent('Page.navigationCommitted', {
frameId: frame.id(),
navigationId: frame.lastCommittedNavigationId(),
url: frame.url(),
name: frame.name(),
});
}
_onDOMWindowCreated(event) {
if (!this._scriptsToEvaluateOnNewDocument.size)
return;
const docShell = event.target.ownerGlobal.docShell;
const frame = this._frameTree.frameForDocShell(docShell);
if (!frame)
return;
const executionContext = this._ensureExecutionContext(frame);
for (const script of this._scriptsToEvaluateOnNewDocument.values()) {
try {
let result = executionContext.evaluateScript(script);
if (result && result.objectId)
executionContext.disposeObject(result.objectId);
} catch (e) {
}
}
}
_onFrameAttached(frame) {
this._session.emitEvent('Page.frameAttached', {
frameId: frame.id(),
parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined,
});
}
_onFrameDetached(frame) {
this._session.emitEvent('Page.frameDetached', {
frameId: frame.id(),
});
}
_ensureExecutionContext(frame) {
let executionContext = this._frameToExecutionContext.get(frame);
if (!executionContext) {
executionContext = this._runtime.createExecutionContext(frame.domWindow());
this._frameToExecutionContext.set(frame, executionContext);
}
return executionContext;
}
dispose() {
helper.removeListeners(this._eventListeners);
}
_consoleAPICalled({wrappedJSObject}, topic, data) {
const levelToType = {
'dir': 'dir',
'log': 'log',
'debug': 'debug',
'info': 'info',
'error': 'error',
'warn': 'warning',
'dirxml': 'dirxml',
'table': 'table',
'trace': 'trace',
'clear': 'clear',
'group': 'startGroup',
'groupCollapsed': 'startGroupCollapsed',
'groupEnd': 'endGroup',
'assert': 'assert',
'profile': 'profile',
'profileEnd': 'profileEnd',
'count': 'count',
'countReset': 'countReset',
'time': null,
'timeLog': 'timeLog',
'timeEnd': 'timeEnd',
'timeStamp': 'timeStamp',
};
const type = levelToType[wrappedJSObject.level];
if (!type) return;
let messageFrame = null;
for (const frame of this._frameTree.frames()) {
const domWindow = frame.domWindow();
if (domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID) {
messageFrame = frame;
break;
}
}
if (!messageFrame)
return;
const executionContext = this._ensureExecutionContext(messageFrame);
const args = wrappedJSObject.arguments.map(arg => executionContext.rawValueToRemoteObject(arg));
this._session.emitEvent('Page.consoleAPICalled', {args, type, frameId: messageFrame.id()});
}
async navigate({frameId, url}) {
try {
const uri = NetUtil.newURI(url);
} catch (e) {
throw new Error(`Invalid url: "${url}"`);
}
const frame = this._frameTree.frame(frameId);
const docShell = frame.docShell();
docShell.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null /* referrer */, null /* postData */, null /* headers */);
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
}
async reload({frameId, url}) {
const frame = this._frameTree.frame(frameId);
const docShell = frame.docShell();
docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
}
async goBack({frameId, url}) {
const frame = this._frameTree.frame(frameId);
const docShell = frame.docShell();
if (!docShell.canGoBack)
return {navigationId: null, navigationURL: null};
docShell.goBack();
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
}
async goForward({frameId, url}) {
const frame = this._frameTree.frame(frameId);
const docShell = frame.docShell();
if (!docShell.canGoForward)
return {navigationId: null, navigationURL: null};
docShell.goForward();
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
}
async disposeObject({frameId, objectId}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
return executionContext.disposeObject(objectId);
}
getContentQuads({objectId, frameId}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
const unsafeObject = executionContext.unsafeObject(objectId);
if (!unsafeObject.getBoxQuads)
throw new Error('RemoteObject is not a node');
const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}).map(quad => {
return {
p1: {x: quad.p1.x, y: quad.p1.y},
p2: {x: quad.p2.x, y: quad.p2.y},
p3: {x: quad.p3.x, y: quad.p3.y},
p4: {x: quad.p4.x, y: quad.p4.y},
};
});
return {quads};
}
async getBoundingBox({frameId, objectId}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
const unsafeObject = executionContext.unsafeObject(objectId);
if (!unsafeObject.getBoxQuads)
throw new Error('RemoteObject is not a node');
const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document});
if (!quads.length)
return null;
let x1 = Infinity;
let y1 = Infinity;
let x2 = -Infinity;
let y2 = -Infinity;
for (const quad of quads) {
const boundingBox = quad.getBounds();
x1 = Math.min(boundingBox.x, x1);
y1 = Math.min(boundingBox.y, y1);
x2 = Math.max(boundingBox.x + boundingBox.width, x2);
y2 = Math.max(boundingBox.y + boundingBox.height, y2);
}
return {x: x1 + frame.domWindow().scrollX, y: y1 + frame.domWindow().scrollY, width: x2 - x1, height: y2 - y1};
}
async evaluate({frameId, functionText, args, script, returnByValue}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
const exceptionDetails = {};
let result = null;
if (script)
result = await executionContext.evaluateScript(script, exceptionDetails);
else
result = await executionContext.evaluateFunction(functionText, args, exceptionDetails);
if (!result)
return {exceptionDetails};
let isNode = undefined;
if (returnByValue)
result = executionContext.ensureSerializedToValue(result);
return {result};
}
async getObjectProperties({frameId, objectId}) {
const frame = this._frameTree.frame(frameId);
if (!frame)
throw new Error('Failed to find frame with id = ' + frameId);
const executionContext = this._ensureExecutionContext(frame);
return {properties: executionContext.getObjectProperties(objectId)};
}
async screenshot({mimeType, fullPage, frameId, objectId, clip}) {
const content = this._session.mm().content;
if (clip) {
const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType);
return {data};
}
if (fullPage) {
const rect = content.document.documentElement.getBoundingClientRect();
const width = content.innerWidth + content.scrollMaxX - content.scrollMinX;
const height = content.innerHeight + content.scrollMaxY - content.scrollMinY;
const data = takeScreenshot(content, 0, 0, width, height, mimeType);
return {data};
}
const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType);
return {data};
}
async dispatchKeyEvent({type, keyCode, code, key, repeat, location}) {
const frame = this._frameTree.mainFrame();
const tip = frame.textInputProcessor();
let keyEvent = new (frame.domWindow().KeyboardEvent)("", {
key,
code,
location,
repeat,
keyCode
});
const flags = 0;
if (type === 'keydown')
tip.keydown(keyEvent, flags);
else if (type === 'keyup')
tip.keyup(keyEvent, flags);
else
throw new Error(`Unknown type ${type}`);
}
async dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) {
const frame = this._frameTree.mainFrame();
frame.domWindow().windowUtils.sendMouseEvent(
type,
x,
y,
button,
clickCount,
modifiers,
false /*aIgnoreRootScrollFrame*/,
undefined /*pressure*/,
undefined /*inputSource*/,
undefined /*isDOMEventSynthesized*/,
undefined /*isWidgetEventSynthesized*/,
buttons);
if (type === 'mousedown' && button === 2) {
frame.domWindow().windowUtils.sendMouseEvent(
'contextmenu',
x,
y,
button,
clickCount,
modifiers,
false /*aIgnoreRootScrollFrame*/,
undefined /*pressure*/,
undefined /*inputSource*/,
undefined /*isDOMEventSynthesized*/,
undefined /*isWidgetEventSynthesized*/,
buttons);
}
}
async insertText({text}) {
const frame = this._frameTree.mainFrame();
frame.textInputProcessor().commitCompositionWith(text);
}
}
function takeScreenshot(win, left, top, width, height, mimeType) {
const MAX_SKIA_DIMENSIONS = 32767;
const scale = win.devicePixelRatio;
const canvasWidth = width * scale;
const canvasHeight = height * scale;
if (canvasWidth > MAX_SKIA_DIMENSIONS || canvasHeight > MAX_SKIA_DIMENSIONS)
throw new Error('Cannot take screenshot larger than ' + MAX_SKIA_DIMENSIONS);
const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
let ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_CARET);
const dataURL = canvas.toDataURL(mimeType);
return dataURL.substring(dataURL.indexOf(',') + 1);
};
var EXPORTED_SYMBOLS = ['PageAgent'];
this.PageAgent = PageAgent;

View File

@ -0,0 +1,275 @@
"use strict";
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {});
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
addDebuggerToGlobal(Cu.getGlobalForObject(this));
const helper = new Helper();
class RuntimeAgent {
constructor() {
this._debugger = new Debugger();
this._pendingPromises = new Map();
}
dispose() {}
async _awaitPromise(executionContext, obj, exceptionDetails = {}) {
if (obj.promiseState === 'fulfilled')
return {success: true, obj: obj.promiseValue};
if (obj.promiseState === 'rejected') {
const global = executionContext._global;
exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
return {success: false, obj: null};
}
let resolve, reject;
const promise = new Promise((a, b) => {
resolve = a;
reject = b;
});
this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails});
if (this._pendingPromises.size === 1)
this._debugger.onPromiseSettled = this._onPromiseSettled.bind(this);
return await promise;
}
_onPromiseSettled(obj) {
const pendingPromise = this._pendingPromises.get(obj.promiseID);
if (!pendingPromise)
return;
this._pendingPromises.delete(obj.promiseID);
if (!this._pendingPromises.size)
this._debugger.onPromiseSettled = undefined;
if (obj.promiseState === 'fulfilled') {
pendingPromise.resolve({success: true, obj: obj.promiseValue});
return;
};
const global = pendingPromise.executionContext._global;
pendingPromise.exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
pendingPromise.exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
pendingPromise.resolve({success: false, obj: null});
}
createExecutionContext(domWindow) {
return new ExecutionContext(this, domWindow, this._debugger.addDebuggee(domWindow));
}
destroyExecutionContext(destroyedContext) {
for (const [promiseID, {reject, executionContext}] of this._pendingPromises) {
if (executionContext === destroyedContext) {
reject(new Error('Execution context was destroyed!'));
this._pendingPromises.delete(promiseID);
}
}
if (!this._pendingPromises.size)
this._debugger.onPromiseSettled = undefined;
this._debugger.removeDebuggee(destroyedContext._domWindow);
}
}
class ExecutionContext {
constructor(runtime, DOMWindow, global) {
this._runtime = runtime;
this._domWindow = DOMWindow;
this._global = global;
this._remoteObjects = new Map();
}
async evaluateScript(script, exceptionDetails = {}) {
const userInputHelper = this._domWindow.windowUtils.setHandlingUserInput(true);
let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails);
userInputHelper.destruct();
if (!success)
return null;
if (obj && obj.isPromise) {
const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails);
if (!awaitResult.success)
return null;
obj = awaitResult.obj;
}
return this._createRemoteObject(obj);
}
async evaluateFunction(functionText, args, exceptionDetails = {}) {
const funEvaluation = this._getResult(this._global.executeInGlobal('(' + functionText + ')'), exceptionDetails);
if (!funEvaluation.success)
return null;
if (!funEvaluation.obj.callable)
throw new Error('functionText does not evaluate to a function!');
args = args.map(arg => {
if (arg.objectId) {
if (!this._remoteObjects.has(arg.objectId))
throw new Error('Cannot find object with id = ' + arg.objectId);
return this._remoteObjects.get(arg.objectId);
}
switch (arg.unserializableValue) {
case 'Infinity': return Infinity;
case '-Infinity': return -Infinity;
case '-0': return -0;
case 'NaN': return NaN;
default: return this._toDebugger(arg.value);
}
});
const userInputHelper = this._domWindow.windowUtils.setHandlingUserInput(true);
let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails);
userInputHelper.destruct();
if (!success)
return null;
if (obj && obj.isPromise) {
const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails);
if (!awaitResult.success)
return null;
obj = awaitResult.obj;
}
return this._createRemoteObject(obj);
}
unsafeObject(objectId) {
if (!this._remoteObjects.has(objectId))
throw new Error('Cannot find object with id = ' + objectId);
return this._remoteObjects.get(objectId).unsafeDereference();
}
rawValueToRemoteObject(rawValue) {
const debuggerObj = this._global.makeDebuggeeValue(rawValue);
return this._createRemoteObject(debuggerObj);
}
_createRemoteObject(debuggerObj) {
if (debuggerObj instanceof Debugger.Object) {
const objectId = helper.generateId();
this._remoteObjects.set(objectId, debuggerObj);
const rawObj = debuggerObj.unsafeDereference();
const type = typeof rawObj;
let subtype = undefined;
if (debuggerObj.isProxy)
subtype = 'proxy';
else if (Array.isArray(rawObj))
subtype = 'array';
else if (Object.is(rawObj, null))
subtype = 'null';
else if (rawObj instanceof this._domWindow.Node)
subtype = 'node';
else if (rawObj instanceof this._domWindow.RegExp)
subtype = 'regexp';
else if (rawObj instanceof this._domWindow.Date)
subtype = 'date';
else if (rawObj instanceof this._domWindow.Map)
subtype = 'map';
else if (rawObj instanceof this._domWindow.Set)
subtype = 'set';
else if (rawObj instanceof this._domWindow.WeakMap)
subtype = 'weakmap';
else if (rawObj instanceof this._domWindow.WeakSet)
subtype = 'weakset';
else if (rawObj instanceof this._domWindow.Error)
subtype = 'error';
else if (rawObj instanceof this._domWindow.Promise)
subtype = 'promise';
else if ((rawObj instanceof this._domWindow.Int8Array) || (rawObj instanceof this._domWindow.Uint8Array) ||
(rawObj instanceof this._domWindow.Uint8ClampedArray) || (rawObj instanceof this._domWindow.Int16Array) ||
(rawObj instanceof this._domWindow.Uint16Array) || (rawObj instanceof this._domWindow.Int32Array) ||
(rawObj instanceof this._domWindow.Uint32Array) || (rawObj instanceof this._domWindow.Float32Array) ||
(rawObj instanceof this._domWindow.Float64Array)) {
subtype = 'typedarray';
}
const isNode = debuggerObj.unsafeDereference() instanceof this._domWindow.Node;
return {objectId, type, subtype};
}
if (typeof debuggerObj === 'symbol') {
const objectId = helper.generateId();
this._remoteObjects.set(objectId, debuggerObj);
return {objectId, type: 'symbol'};
}
let unserializableValue = undefined;
if (Object.is(debuggerObj, NaN))
unserializableValue = 'NaN';
else if (Object.is(debuggerObj, -0))
unserializableValue = '-0';
else if (Object.is(debuggerObj, Infinity))
unserializableValue = 'Infinity';
else if (Object.is(debuggerObj, -Infinity))
unserializableValue = '-Infinity';
return unserializableValue ? {unserializableValue} : {value: debuggerObj};
}
ensureSerializedToValue(protocolObject) {
if (!protocolObject.objectId)
return protocolObject;
const obj = this._remoteObjects.get(protocolObject.objectId);
this._remoteObjects.delete(protocolObject.objectId);
return {value: this._serialize(obj)};
}
_toDebugger(obj) {
if (typeof obj !== 'object')
return obj;
const properties = {};
for (let [key, value] of Object.entries(obj)) {
properties[key] = {
writable: true,
enumerable: true,
value: this._toDebugger(value),
};
}
const baseObject = Array.isArray(obj) ? '([])' : '({})';
const debuggerObj = this._global.executeInGlobal(baseObject).return;
debuggerObj.defineProperties(properties);
return debuggerObj;
}
_serialize(obj) {
const result = this._global.executeInGlobalWithBindings('JSON.stringify(e)', {e: obj});
if (result.throw)
throw new Error('Object is not serializable');
return JSON.parse(result.return);
}
disposeObject(objectId) {
this._remoteObjects.delete(objectId);
}
getObjectProperties(objectId) {
if (!this._remoteObjects.has(objectId))
throw new Error('Cannot find object with id = ' + arg.objectId);
const result = [];
for (let obj = this._remoteObjects.get(objectId); obj; obj = obj.proto) {
for (const propertyName of obj.getOwnPropertyNames()) {
const descriptor = obj.getOwnPropertyDescriptor(propertyName);
if (!descriptor.enumerable)
continue;
result.push({
name: propertyName,
value: this._createRemoteObject(descriptor.value),
});
}
}
return result;
}
_getResult(completionValue, exceptionDetails = {}) {
if (!completionValue) {
exceptionDetails.text = 'Evaluation terminated!';
exceptionDetails.stack = '';
return {success: false, obj: null};
}
if (completionValue.throw) {
if (this._global.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) {
exceptionDetails.text = this._global.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return;
exceptionDetails.stack = this._global.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return;
} else {
exceptionDetails.value = this._serialize(completionValue.throw);
}
return {success: false, obj: null};
}
return {success: true, obj: completionValue.return};
}
}
var EXPORTED_SYMBOLS = ['RuntimeAgent'];
this.RuntimeAgent = RuntimeAgent;

View File

@ -0,0 +1,58 @@
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const Cc = Components.classes;
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const HIDDEN_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/hidden-scrollbars.css');
const FLOATING_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/floating-scrollbars.css');
const isHeadless = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless;
const helper = new Helper();
class ScrollbarManager {
constructor(mm, docShell) {
this._docShell = docShell;
this._customScrollbars = null;
if (isHeadless)
this._setCustomScrollbars(HIDDEN_SCROLLBARS);
this._eventListeners = [
helper.addEventListener(mm, 'DOMWindowCreated', this._onDOMWindowCreated.bind(this)),
];
}
setFloatingScrollbars(enabled) {
if (this._customScrollbars === HIDDEN_SCROLLBARS)
return;
this._setCustomScrollbars(enabled ? FLOATING_SCROLLBARS : null);
}
_setCustomScrollbars(customScrollbars) {
if (this._customScrollbars === customScrollbars)
return;
if (this._customScrollbars)
this._docShell.domWindow.windowUtils.removeSheet(this._customScrollbars, this._docShell.domWindow.AGENT_SHEET);
this._customScrollbars = customScrollbars;
if (this._customScrollbars)
this._docShell.domWindow.windowUtils.loadSheet(this._customScrollbars, this._docShell.domWindow.AGENT_SHEET);
}
dispose() {
this._setCustomScrollbars(null);
helper.removeListeners(this._eventListeners);
}
_onDOMWindowCreated(event) {
const docShell = event.target.ownerGlobal.docShell;
if (this._customScrollbars)
docShell.domWindow.windowUtils.loadSheet(this._customScrollbars, docShell.domWindow.AGENT_SHEET);
}
}
var EXPORTED_SYMBOLS = ['ScrollbarManager'];
this.ScrollbarManager = ScrollbarManager;

View File

@ -0,0 +1,47 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
inside a <select> are excluded (including them hides the select arrow on
Windows). We want to include both the root scrollbars for the document as
well as any overflow: scroll elements within the page, while excluding
<select>. */
*|*:not(html|select) > scrollbar {
-moz-appearance: none !important;
position: relative;
background-color: transparent;
background-image: none;
z-index: 2147483647;
padding: 2px;
border: none;
}
/* Scrollbar code will reset the margin to the correct side depending on
where layout actually puts the scrollbar */
*|*:not(html|select) > scrollbar[orient="vertical"] {
margin-left: -10px;
min-width: 10px;
max-width: 10px;
}
*|*:not(html|select) > scrollbar[orient="horizontal"] {
margin-top: -10px;
min-height: 10px;
max-height: 10px;
}
*|*:not(html|select) > scrollbar slider {
-moz-appearance: none !important;
}
*|*:not(html|select) > scrollbar thumb {
-moz-appearance: none !important;
background-color: rgba(0,0,0,0.2);
border-width: 0px !important;
border-radius: 3px !important;
}
*|*:not(html|select) > scrollbar scrollbarbutton,
*|*:not(html|select) > scrollbar gripper {
display: none;
}

View File

@ -0,0 +1,13 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
inside a <select> are excluded (including them hides the select arrow on
Windows). We want to include both the root scrollbars for the document as
well as any overflow: scroll elements within the page, while excluding
<select>. */
*|*:not(html|select) > scrollbar {
-moz-appearance: none !important;
display: none;
}

View File

@ -0,0 +1,27 @@
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {ContentSession} = ChromeUtils.import('chrome://juggler/content/content/ContentSession.js');
const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js');
const sessions = new Map();
const frameTree = new FrameTree(docShell);
const scrollbarManager = new ScrollbarManager(this, docShell);
const helper = new Helper();
const gListeners = [
helper.addMessageListener(this, 'juggler:create-content-session', msg => {
const sessionId = msg.data;
sessions.set(sessionId, new ContentSession(sessionId, this, frameTree, scrollbarManager));
}),
helper.addEventListener(this, 'unload', msg => {
helper.removeListeners(gListeners);
for (const session of sessions.values())
session.dispose();
sessions.clear();
scrollbarManager.dispose();
frameTree.dispose();
}),
];

View File

@ -0,0 +1,25 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
juggler.jar:
% content juggler %content/
content/ChromeSession.js (ChromeSession.js)
content/InsecureSweepingOverride.js (InsecureSweepingOverride.js)
content/Protocol.js (Protocol.js)
content/Helper.js (Helper.js)
content/PageHandler.jsm (PageHandler.jsm)
content/BrowserHandler.jsm (BrowserHandler.jsm)
content/content/main.js (content/main.js)
content/content/ContentSession.js (content/ContentSession.js)
content/content/FrameTree.js (content/FrameTree.js)
content/content/PageAgent.js (content/PageAgent.js)
content/content/RuntimeAgent.js (content/RuntimeAgent.js)
content/content/ScrollbarManager.js (content/ScrollbarManager.js)
content/content/floating-scrollbars.css (content/floating-scrollbars.css)
content/content/hidden-scrollbars.css (content/hidden-scrollbars.css)
content/server/server.js (server/server.js)
content/server/transport.js (server/transport.js)
content/server/stream-utils.js (server/stream-utils.js)
content/server/packets.js (server/packets.js)

View File

@ -0,0 +1,15 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += ["components"]
JAR_MANIFESTS += ["jar.mn"]
#JS_PREFERENCE_FILES += ["prefs/marionette.js"]
#MARIONETTE_UNIT_MANIFESTS += ["harness/marionette_harness/tests/unit/unit-tests.ini"]
#XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
with Files("**"):
BUG_COMPONENT = ("Testing", "Juggler")

View File

@ -0,0 +1,407 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* Packets contain read / write functionality for the different packet types
* supported by the debugging protocol, so that a transport can focus on
* delivery and queue management without worrying too much about the specific
* packet types.
*
* They are intended to be "one use only", so a new packet should be
* instantiated for each incoming or outgoing packet.
*
* A complete Packet type should expose at least the following:
* * read(stream, scriptableStream)
* Called when the input stream has data to read
* * write(stream)
* Called when the output stream is ready to write
* * get done()
* Returns true once the packet is done being read / written
* * destroy()
* Called to clean up at the end of use
*/
const {StreamUtils} =
ChromeUtils.import("chrome://juggler/content/server/stream-utils.js", {});
const unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
unicodeConverter.charset = "UTF-8";
const defer = function() {
let deferred = {
promise: new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
}),
};
return deferred;
};
this.EXPORTED_SYMBOLS = ["RawPacket", "Packet", "JSONPacket", "BulkPacket"];
// The transport's previous check ensured the header length did not
// exceed 20 characters. Here, we opt for the somewhat smaller, but still
// large limit of 1 TiB.
const PACKET_LENGTH_MAX = Math.pow(2, 40);
/**
* A generic Packet processing object (extended by two subtypes below).
*
* @class
*/
function Packet(transport) {
this._transport = transport;
this._length = 0;
}
/**
* Attempt to initialize a new Packet based on the incoming packet header
* we've received so far. We try each of the types in succession, trying
* JSON packets first since they are much more common.
*
* @param {string} header
* Packet header string to attempt parsing.
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*
* @return {Packet}
* Parsed packet of the matching type, or null if no types matched.
*/
Packet.fromHeader = function(header, transport) {
return JSONPacket.fromHeader(header, transport) ||
BulkPacket.fromHeader(header, transport);
};
Packet.prototype = {
get length() {
return this._length;
},
set length(length) {
if (length > PACKET_LENGTH_MAX) {
throw new Error("Packet length " + length +
" exceeds the max length of " + PACKET_LENGTH_MAX);
}
this._length = length;
},
destroy() {
this._transport = null;
},
};
/**
* With a JSON packet (the typical packet type sent via the transport),
* data is transferred as a JSON packet serialized into a string,
* with the string length prepended to the packet, followed by a colon
* ([length]:[packet]). The contents of the JSON packet are specified in
* the Remote Debugging Protocol specification.
*
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*/
function JSONPacket(transport) {
Packet.call(this, transport);
this._data = "";
this._done = false;
}
/**
* Attempt to initialize a new JSONPacket based on the incoming packet
* header we've received so far.
*
* @param {string} header
* Packet header string to attempt parsing.
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*
* @return {JSONPacket}
* Parsed packet, or null if it's not a match.
*/
JSONPacket.fromHeader = function(header, transport) {
let match = this.HEADER_PATTERN.exec(header);
if (!match) {
return null;
}
let packet = new JSONPacket(transport);
packet.length = +match[1];
return packet;
};
JSONPacket.HEADER_PATTERN = /^(\d+):$/;
JSONPacket.prototype = Object.create(Packet.prototype);
Object.defineProperty(JSONPacket.prototype, "object", {
/**
* Gets the object (not the serialized string) being read or written.
*/
get() {
return this._object;
},
/**
* Sets the object to be sent when write() is called.
*/
set(object) {
this._object = object;
let data = JSON.stringify(object);
this._data = unicodeConverter.ConvertFromUnicode(data);
this.length = this._data.length;
},
});
JSONPacket.prototype.read = function(stream, scriptableStream) {
// Read in more packet data.
this._readData(stream, scriptableStream);
if (!this.done) {
// Don't have a complete packet yet.
return;
}
let json = this._data;
try {
json = unicodeConverter.ConvertToUnicode(json);
this._object = JSON.parse(json);
} catch (e) {
let msg = "Error parsing incoming packet: " + json + " (" + e +
" - " + e.stack + ")";
console.error(msg);
dump(msg + "\n");
return;
}
this._transport._onJSONObjectReady(this._object);
};
JSONPacket.prototype._readData = function(stream, scriptableStream) {
let bytesToRead = Math.min(
this.length - this._data.length,
stream.available());
this._data += scriptableStream.readBytes(bytesToRead);
this._done = this._data.length === this.length;
};
JSONPacket.prototype.write = function(stream) {
if (this._outgoing === undefined) {
// Format the serialized packet to a buffer
this._outgoing = this.length + ":" + this._data;
}
let written = stream.write(this._outgoing, this._outgoing.length);
this._outgoing = this._outgoing.slice(written);
this._done = !this._outgoing.length;
};
Object.defineProperty(JSONPacket.prototype, "done", {
get() {
return this._done;
},
});
JSONPacket.prototype.toString = function() {
return JSON.stringify(this._object, null, 2);
};
/**
* With a bulk packet, data is transferred by temporarily handing over
* the transport's input or output stream to the application layer for
* writing data directly. This can be much faster for large data sets,
* and avoids various stages of copies and data duplication inherent in
* the JSON packet type. The bulk packet looks like:
*
* bulk [actor] [type] [length]:[data]
*
* The interpretation of the data portion depends on the kind of actor and
* the packet's type. See the Remote Debugging Protocol Stream Transport
* spec for more details.
*
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*/
function BulkPacket(transport) {
Packet.call(this, transport);
this._done = false;
this._readyForWriting = defer();
}
/**
* Attempt to initialize a new BulkPacket based on the incoming packet
* header we've received so far.
*
* @param {string} header
* Packet header string to attempt parsing.
* @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*
* @return {BulkPacket}
* Parsed packet, or null if it's not a match.
*/
BulkPacket.fromHeader = function(header, transport) {
let match = this.HEADER_PATTERN.exec(header);
if (!match) {
return null;
}
let packet = new BulkPacket(transport);
packet.header = {
actor: match[1],
type: match[2],
length: +match[3],
};
return packet;
};
BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/;
BulkPacket.prototype = Object.create(Packet.prototype);
BulkPacket.prototype.read = function(stream) {
// Temporarily pause monitoring of the input stream
this._transport.pauseIncoming();
let deferred = defer();
this._transport._onBulkReadReady({
actor: this.actor,
type: this.type,
length: this.length,
copyTo: (output) => {
let copying = StreamUtils.copyStream(stream, output, this.length);
deferred.resolve(copying);
return copying;
},
stream,
done: deferred,
});
// Await the result of reading from the stream
deferred.promise.then(() => {
this._done = true;
this._transport.resumeIncoming();
}, this._transport.close);
// Ensure this is only done once
this.read = () => {
throw new Error("Tried to read() a BulkPacket's stream multiple times.");
};
};
BulkPacket.prototype.write = function(stream) {
if (this._outgoingHeader === undefined) {
// Format the serialized packet header to a buffer
this._outgoingHeader = "bulk " + this.actor + " " + this.type + " " +
this.length + ":";
}
// Write the header, or whatever's left of it to write.
if (this._outgoingHeader.length) {
let written = stream.write(this._outgoingHeader,
this._outgoingHeader.length);
this._outgoingHeader = this._outgoingHeader.slice(written);
return;
}
// Temporarily pause the monitoring of the output stream
this._transport.pauseOutgoing();
let deferred = defer();
this._readyForWriting.resolve({
copyFrom: (input) => {
let copying = StreamUtils.copyStream(input, stream, this.length);
deferred.resolve(copying);
return copying;
},
stream,
done: deferred,
});
// Await the result of writing to the stream
deferred.promise.then(() => {
this._done = true;
this._transport.resumeOutgoing();
}, this._transport.close);
// Ensure this is only done once
this.write = () => {
throw new Error("Tried to write() a BulkPacket's stream multiple times.");
};
};
Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", {
get() {
return this._readyForWriting.promise;
},
});
Object.defineProperty(BulkPacket.prototype, "header", {
get() {
return {
actor: this.actor,
type: this.type,
length: this.length,
};
},
set(header) {
this.actor = header.actor;
this.type = header.type;
this.length = header.length;
},
});
Object.defineProperty(BulkPacket.prototype, "done", {
get() {
return this._done;
},
});
BulkPacket.prototype.toString = function() {
return "Bulk: " + JSON.stringify(this.header, null, 2);
};
/**
* RawPacket is used to test the transport's error handling of malformed
* packets, by writing data directly onto the stream.
* @param transport DebuggerTransport
* The transport instance that will own the packet.
* @param data string
* The raw string to send out onto the stream.
*/
function RawPacket(transport, data) {
Packet.call(this, transport);
this._data = data;
this.length = data.length;
this._done = false;
}
RawPacket.prototype = Object.create(Packet.prototype);
RawPacket.prototype.read = function() {
// this has not yet been needed for testing
throw new Error("Not implemented");
};
RawPacket.prototype.write = function(stream) {
let written = stream.write(this._data, this._data.length);
this._data = this._data.slice(written);
this._done = !this._data.length;
};
Object.defineProperty(RawPacket.prototype, "done", {
get() {
return this._done;
},
});

View File

@ -0,0 +1,97 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const CC = Components.Constructor;
const ServerSocket = CC(
"@mozilla.org/network/server-socket;1",
"nsIServerSocket",
"initSpecialConnection");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const {DebuggerTransport} = ChromeUtils.import("chrome://juggler/content/server/transport.js", {});
const {KeepWhenOffline, LoopbackOnly} = Ci.nsIServerSocket;
this.EXPORTED_SYMBOLS = [
"TCPConnection",
"TCPListener",
];
class TCPListener {
constructor() {
this._socket = null;
this._nextConnID = 0;
this.onconnectioncreated = null;
this.onconnectionclosed = null;
}
start(port) {
if (this._socket)
return;
try {
const flags = KeepWhenOffline | LoopbackOnly;
const backlog = 1;
this._socket = new ServerSocket(port, flags, backlog);
} catch (e) {
throw new Error(`Could not bind to port ${port} (${e.name})`);
}
this._socket.asyncListen(this);
return this._socket.port;
}
stop() {
if (!this._socket)
return;
// Note that closing the server socket will not close currently active
// connections.
this._socket.close();
this._socket = null;
}
onSocketAccepted(serverSocket, clientSocket) {
const input = clientSocket.openInputStream(0, 0, 0);
const output = clientSocket.openOutputStream(0, 0, 0);
const transport = new DebuggerTransport(input, output);
const connection = new TCPConnection(this._nextConnID++, transport, () => {
if (this.onconnectionclosed)
this.onconnectionclosed.call(null, connection);
});
transport.ready();
if (this.onconnectioncreated)
this.onconnectioncreated.call(null, connection);
}
}
this.TCPListener = TCPListener;
class TCPConnection {
constructor(id, transport, closeCallback) {
this._id = id;
this._transport = transport;
// transport hooks are TCPConnection#onPacket
// and TCPConnection#onClosed
this._transport.hooks = this;
this._closeCallback = closeCallback;
this.onmessage = null;
}
send(msg) {
this._transport.send(msg);
}
onClosed() {
this._closeCallback.call(null);
}
async onPacket(data) {
if (this.onmessage)
this.onmessage.call(null, data);
}
}
this.TCPConnection = TCPConnection;

View File

@ -0,0 +1,247 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const CC = Components.Constructor;
ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
const IOUtil = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil);
const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
"nsIScriptableInputStream", "init");
this.EXPORTED_SYMBOLS = ["StreamUtils"];
const BUFFER_SIZE = 0x8000;
/**
* This helper function (and its companion object) are used by bulk
* senders and receivers to read and write data in and out of other streams.
* Functions that make use of this tool are passed to callers when it is
* time to read or write bulk data. It is highly recommended to use these
* copier functions instead of the stream directly because the copier
* enforces the agreed upon length. Since bulk mode reuses an existing
* stream, the sender and receiver must write and read exactly the agreed
* upon amount of data, or else the entire transport will be left in a
* invalid state. Additionally, other methods of stream copying (such as
* NetUtil.asyncCopy) close the streams involved, which would terminate
* the debugging transport, and so it is avoided here.
*
* Overall, this *works*, but clearly the optimal solution would be
* able to just use the streams directly. If it were possible to fully
* implement nsIInputStream/nsIOutputStream in JS, wrapper streams could
* be created to enforce the length and avoid closing, and consumers could
* use familiar stream utilities like NetUtil.asyncCopy.
*
* The function takes two async streams and copies a precise number
* of bytes from one to the other. Copying begins immediately, but may
* complete at some future time depending on data size. Use the returned
* promise to know when it's complete.
*
* @param {nsIAsyncInputStream} input
* Stream to copy from.
* @param {nsIAsyncOutputStream} output
* Stream to copy to.
* @param {number} length
* Amount of data that needs to be copied.
*
* @return {Promise}
* Promise is resolved when copying completes or rejected if any
* (unexpected) errors occur.
*/
function copyStream(input, output, length) {
let copier = new StreamCopier(input, output, length);
return copier.copy();
}
/** @class */
function StreamCopier(input, output, length) {
EventEmitter.decorate(this);
this._id = StreamCopier._nextId++;
this.input = input;
// Save off the base output stream, since we know it's async as we've
// required
this.baseAsyncOutput = output;
if (IOUtil.outputStreamIsBuffered(output)) {
this.output = output;
} else {
this.output = Cc["@mozilla.org/network/buffered-output-stream;1"]
.createInstance(Ci.nsIBufferedOutputStream);
this.output.init(output, BUFFER_SIZE);
}
this._length = length;
this._amountLeft = length;
this._deferred = {
promise: new Promise((resolve, reject) => {
this._deferred.resolve = resolve;
this._deferred.reject = reject;
}),
};
this._copy = this._copy.bind(this);
this._flush = this._flush.bind(this);
this._destroy = this._destroy.bind(this);
// Copy promise's then method up to this object.
//
// Allows the copier to offer a promise interface for the simple succeed
// or fail scenarios, but also emit events (due to the EventEmitter)
// for other states, like progress.
this.then = this._deferred.promise.then.bind(this._deferred.promise);
this.then(this._destroy, this._destroy);
// Stream ready callback starts as |_copy|, but may switch to |_flush|
// at end if flushing would block the output stream.
this._streamReadyCallback = this._copy;
}
StreamCopier._nextId = 0;
StreamCopier.prototype = {
copy() {
// Dispatch to the next tick so that it's possible to attach a progress
// event listener, even for extremely fast copies (like when testing).
Services.tm.currentThread.dispatch(() => {
try {
this._copy();
} catch (e) {
this._deferred.reject(e);
}
}, 0);
return this;
},
_copy() {
let bytesAvailable = this.input.available();
let amountToCopy = Math.min(bytesAvailable, this._amountLeft);
this._debug("Trying to copy: " + amountToCopy);
let bytesCopied;
try {
bytesCopied = this.output.writeFrom(this.input, amountToCopy);
} catch (e) {
if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK) {
this._debug("Base stream would block, will retry");
this._debug("Waiting for output stream");
this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
return;
}
throw e;
}
this._amountLeft -= bytesCopied;
this._debug("Copied: " + bytesCopied +
", Left: " + this._amountLeft);
this._emitProgress();
if (this._amountLeft === 0) {
this._debug("Copy done!");
this._flush();
return;
}
this._debug("Waiting for input stream");
this.input.asyncWait(this, 0, 0, Services.tm.currentThread);
},
_emitProgress() {
this.emit("progress", {
bytesSent: this._length - this._amountLeft,
totalBytes: this._length,
});
},
_flush() {
try {
this.output.flush();
} catch (e) {
if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK ||
e.result == Cr.NS_ERROR_FAILURE) {
this._debug("Flush would block, will retry");
this._streamReadyCallback = this._flush;
this._debug("Waiting for output stream");
this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread);
return;
}
throw e;
}
this._deferred.resolve();
},
_destroy() {
this._destroy = null;
this._copy = null;
this._flush = null;
this.input = null;
this.output = null;
},
// nsIInputStreamCallback
onInputStreamReady() {
this._streamReadyCallback();
},
// nsIOutputStreamCallback
onOutputStreamReady() {
this._streamReadyCallback();
},
_debug() {
},
};
/**
* Read from a stream, one byte at a time, up to the next
* <var>delimiter</var> character, but stopping if we've read |count|
* without finding it. Reading also terminates early if there are less
* than <var>count</var> bytes available on the stream. In that case,
* we only read as many bytes as the stream currently has to offer.
*
* @param {nsIInputStream} stream
* Input stream to read from.
* @param {string} delimiter
* Character we're trying to find.
* @param {number} count
* Max number of characters to read while searching.
*
* @return {string}
* Collected data. If the delimiter was found, this string will
* end with it.
*/
// TODO: This implementation could be removed if bug 984651 is fixed,
// which provides a native version of the same idea.
function delimitedRead(stream, delimiter, count) {
let scriptableStream;
if (stream instanceof Ci.nsIScriptableInputStream) {
scriptableStream = stream;
} else {
scriptableStream = new ScriptableInputStream(stream);
}
let data = "";
// Don't exceed what's available on the stream
count = Math.min(count, stream.available());
if (count <= 0) {
return data;
}
let char;
while (char !== delimiter && count > 0) {
char = scriptableStream.readBytes(1);
count--;
data += char;
}
return data;
}
this.StreamUtils = {
copyStream,
delimitedRead,
};

View File

@ -0,0 +1,523 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* global Pipe, ScriptableInputStream */
const CC = Components.Constructor;
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
const {StreamUtils} =
ChromeUtils.import("chrome://juggler/content/server/stream-utils.js", {});
const {Packet, JSONPacket, BulkPacket} =
ChromeUtils.import("chrome://juggler/content/server/packets.js", {});
const executeSoon = function(func) {
Services.tm.dispatchToMainThread(func);
};
const flags = {wantVerbose: false, wantLogging: false};
const dumpv =
flags.wantVerbose ?
function(msg) { dump(msg + "\n"); } :
function() {};
const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init");
const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
"nsIScriptableInputStream", "init");
this.EXPORTED_SYMBOLS = ["DebuggerTransport"];
const PACKET_HEADER_MAX = 200;
/**
* An adapter that handles data transfers between the debugger client
* and server. It can work with both nsIPipe and nsIServerSocket
* transports so long as the properly created input and output streams
* are specified. (However, for intra-process connections,
* LocalDebuggerTransport, below, is more efficient than using an nsIPipe
* pair with DebuggerTransport.)
*
* @param {nsIAsyncInputStream} input
* The input stream.
* @param {nsIAsyncOutputStream} output
* The output stream.
*
* Given a DebuggerTransport instance dt:
* 1) Set dt.hooks to a packet handler object (described below).
* 2) Call dt.ready() to begin watching for input packets.
* 3) Call dt.send() / dt.startBulkSend() to send packets.
* 4) Call dt.close() to close the connection, and disengage from
* the event loop.
*
* A packet handler is an object with the following methods:
*
* - onPacket(packet) - called when we have received a complete packet.
* |packet| is the parsed form of the packet --- a JavaScript value, not
* a JSON-syntax string.
*
* - onBulkPacket(packet) - called when we have switched to bulk packet
* receiving mode. |packet| is an object containing:
* * actor: Name of actor that will receive the packet
* * type: Name of actor's method that should be called on receipt
* * length: Size of the data to be read
* * stream: This input stream should only be used directly if you
* can ensure that you will read exactly |length| bytes and
* will not close the stream when reading is complete
* * done: If you use the stream directly (instead of |copyTo|
* below), you must signal completion by resolving/rejecting
* this deferred. If it's rejected, the transport will
* be closed. If an Error is supplied as a rejection value,
* it will be logged via |dump|. If you do use |copyTo|,
* resolving is taken care of for you when copying completes.
* * copyTo: A helper function for getting your data out of the
* stream that meets the stream handling requirements above,
* and has the following signature:
*
* @param nsIAsyncOutputStream {output}
* The stream to copy to.
*
* @return {Promise}
* The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur. This object
* also emits "progress" events for each chunk that is
* copied. See stream-utils.js.
*
* - onClosed(reason) - called when the connection is closed. |reason|
* is an optional nsresult or object, typically passed when the
* transport is closed due to some error in a underlying stream.
*
* See ./packets.js and the Remote Debugging Protocol specification for
* more details on the format of these packets.
*
* @class
*/
function DebuggerTransport(input, output) {
EventEmitter.decorate(this);
this._input = input;
this._scriptableInput = new ScriptableInputStream(input);
this._output = output;
// The current incoming (possibly partial) header, which will determine
// which type of Packet |_incoming| below will become.
this._incomingHeader = "";
// The current incoming Packet object
this._incoming = null;
// A queue of outgoing Packet objects
this._outgoing = [];
this.hooks = null;
this.active = false;
this._incomingEnabled = true;
this._outgoingEnabled = true;
this.close = this.close.bind(this);
}
DebuggerTransport.prototype = {
/**
* Transmit an object as a JSON packet.
*
* This method returns immediately, without waiting for the entire
* packet to be transmitted, registering event handlers as needed to
* transmit the entire packet. Packets are transmitted in the order they
* are passed to this method.
*/
send(object) {
this.emit("send", object);
let packet = new JSONPacket(this);
packet.object = object;
this._outgoing.push(packet);
this._flushOutgoing();
},
/**
* Transmit streaming data via a bulk packet.
*
* This method initiates the bulk send process by queuing up the header
* data. The caller receives eventual access to a stream for writing.
*
* N.B.: Do *not* attempt to close the stream handed to you, as it
* will continue to be used by this transport afterwards. Most users
* should instead use the provided |copyFrom| function instead.
*
* @param {Object} header
* This is modeled after the format of JSON packets above, but does
* not actually contain the data, but is instead just a routing
* header:
*
* - actor: Name of actor that will receive the packet
* - type: Name of actor's method that should be called on receipt
* - length: Size of the data to be sent
*
* @return {Promise}
* The promise will be resolved when you are allowed to write to
* the stream with an object containing:
*
* - stream: This output stream should only be used directly
* if you can ensure that you will write exactly
* |length| bytes and will not close the stream when
* writing is complete.
* - done: If you use the stream directly (instead of
* |copyFrom| below), you must signal completion by
* resolving/rejecting this deferred. If it's
* rejected, the transport will be closed. If an
* Error is supplied as a rejection value, it will
* be logged via |dump|. If you do use |copyFrom|,
* resolving is taken care of for you when copying
* completes.
* - copyFrom: A helper function for getting your data onto the
* stream that meets the stream handling requirements
* above, and has the following signature:
*
* @param {nsIAsyncInputStream} input
* The stream to copy from.
*
* @return {Promise}
* The promise is resolved when copying completes
* or rejected if any (unexpected) errors occur.
* This object also emits "progress" events for
* each chunkthat is copied. See stream-utils.js.
*/
startBulkSend(header) {
this.emit("startbulksend", header);
let packet = new BulkPacket(this);
packet.header = header;
this._outgoing.push(packet);
this._flushOutgoing();
return packet.streamReadyForWriting;
},
/**
* Close the transport.
*
* @param {(nsresult|object)=} reason
* The status code or error message that corresponds to the reason
* for closing the transport (likely because a stream closed
* or failed).
*/
close(reason) {
this.emit("close", reason);
this.active = false;
this._input.close();
this._scriptableInput.close();
this._output.close();
this._destroyIncoming();
this._destroyAllOutgoing();
if (this.hooks) {
this.hooks.onClosed(reason);
this.hooks = null;
}
if (reason) {
dumpv("Transport closed: " + reason);
} else {
dumpv("Transport closed.");
}
},
/**
* The currently outgoing packet (at the top of the queue).
*/
get _currentOutgoing() {
return this._outgoing[0];
},
/**
* Flush data to the outgoing stream. Waits until the output
* stream notifies us that it is ready to be written to (via
* onOutputStreamReady).
*/
_flushOutgoing() {
if (!this._outgoingEnabled || this._outgoing.length === 0) {
return;
}
// If the top of the packet queue has nothing more to send, remove it.
if (this._currentOutgoing.done) {
this._finishCurrentOutgoing();
}
if (this._outgoing.length > 0) {
let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
this._output.asyncWait(this, 0, 0, threadManager.currentThread);
}
},
/**
* Pause this transport's attempts to write to the output stream.
* This is used when we've temporarily handed off our output stream for
* writing bulk data.
*/
pauseOutgoing() {
this._outgoingEnabled = false;
},
/**
* Resume this transport's attempts to write to the output stream.
*/
resumeOutgoing() {
this._outgoingEnabled = true;
this._flushOutgoing();
},
// nsIOutputStreamCallback
/**
* This is called when the output stream is ready for more data to
* be written. The current outgoing packet will attempt to write some
* amount of data, but may not complete.
*/
onOutputStreamReady(stream) {
if (!this._outgoingEnabled || this._outgoing.length === 0) {
return;
}
try {
this._currentOutgoing.write(stream);
} catch (e) {
if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
this.close(e.result);
return;
}
throw e;
}
this._flushOutgoing();
},
/**
* Remove the current outgoing packet from the queue upon completion.
*/
_finishCurrentOutgoing() {
if (this._currentOutgoing) {
this._currentOutgoing.destroy();
this._outgoing.shift();
}
},
/**
* Clear the entire outgoing queue.
*/
_destroyAllOutgoing() {
for (let packet of this._outgoing) {
packet.destroy();
}
this._outgoing = [];
},
/**
* Initialize the input stream for reading. Once this method has been
* called, we watch for packets on the input stream, and pass them to
* the appropriate handlers via this.hooks.
*/
ready() {
this.active = true;
this._waitForIncoming();
},
/**
* Asks the input stream to notify us (via onInputStreamReady) when it is
* ready for reading.
*/
_waitForIncoming() {
if (this._incomingEnabled) {
let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
this._input.asyncWait(this, 0, 0, threadManager.currentThread);
}
},
/**
* Pause this transport's attempts to read from the input stream.
* This is used when we've temporarily handed off our input stream for
* reading bulk data.
*/
pauseIncoming() {
this._incomingEnabled = false;
},
/**
* Resume this transport's attempts to read from the input stream.
*/
resumeIncoming() {
this._incomingEnabled = true;
this._flushIncoming();
this._waitForIncoming();
},
// nsIInputStreamCallback
/**
* Called when the stream is either readable or closed.
*/
onInputStreamReady(stream) {
try {
while (stream.available() && this._incomingEnabled &&
this._processIncoming(stream, stream.available())) {
// Loop until there is nothing more to process
}
this._waitForIncoming();
} catch (e) {
if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
this.close(e.result);
} else {
throw e;
}
}
},
/**
* Process the incoming data. Will create a new currently incoming
* Packet if needed. Tells the incoming Packet to read as much data
* as it can, but reading may not complete. The Packet signals that
* its data is ready for delivery by calling one of this transport's
* _on*Ready methods (see ./packets.js and the _on*Ready methods below).
*
* @return {boolean}
* Whether incoming stream processing should continue for any
* remaining data.
*/
_processIncoming(stream, count) {
dumpv("Data available: " + count);
if (!count) {
dumpv("Nothing to read, skipping");
return false;
}
try {
if (!this._incoming) {
dumpv("Creating a new packet from incoming");
if (!this._readHeader(stream)) {
// Not enough data to read packet type
return false;
}
// Attempt to create a new Packet by trying to parse each possible
// header pattern.
this._incoming = Packet.fromHeader(this._incomingHeader, this);
if (!this._incoming) {
throw new Error("No packet types for header: " +
this._incomingHeader);
}
}
if (!this._incoming.done) {
// We have an incomplete packet, keep reading it.
dumpv("Existing packet incomplete, keep reading");
this._incoming.read(stream, this._scriptableInput);
}
} catch (e) {
dump(`Error reading incoming packet: (${e} - ${e.stack})\n`);
// Now in an invalid state, shut down the transport.
this.close();
return false;
}
if (!this._incoming.done) {
// Still not complete, we'll wait for more data.
dumpv("Packet not done, wait for more");
return true;
}
// Ready for next packet
this._flushIncoming();
return true;
},
/**
* Read as far as we can into the incoming data, attempting to build
* up a complete packet header (which terminates with ":"). We'll only
* read up to PACKET_HEADER_MAX characters.
*
* @return {boolean}
* True if we now have a complete header.
*/
_readHeader() {
let amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length;
this._incomingHeader +=
StreamUtils.delimitedRead(this._scriptableInput, ":", amountToRead);
if (flags.wantVerbose) {
dumpv("Header read: " + this._incomingHeader);
}
if (this._incomingHeader.endsWith(":")) {
if (flags.wantVerbose) {
dumpv("Found packet header successfully: " + this._incomingHeader);
}
return true;
}
if (this._incomingHeader.length >= PACKET_HEADER_MAX) {
throw new Error("Failed to parse packet header!");
}
// Not enough data yet.
return false;
},
/**
* If the incoming packet is done, log it as needed and clear the buffer.
*/
_flushIncoming() {
if (!this._incoming.done) {
return;
}
if (flags.wantLogging) {
dumpv("Got: " + this._incoming);
}
this._destroyIncoming();
},
/**
* Handler triggered by an incoming JSONPacket completing it's |read|
* method. Delivers the packet to this.hooks.onPacket.
*/
_onJSONObjectReady(object) {
executeSoon(() => {
// Ensure the transport is still alive by the time this runs.
if (this.active) {
this.emit("packet", object);
this.hooks.onPacket(object);
}
});
},
/**
* Handler triggered by an incoming BulkPacket entering the |read|
* phase for the stream portion of the packet. Delivers info about the
* incoming streaming data to this.hooks.onBulkPacket. See the main
* comment on the transport at the top of this file for more details.
*/
_onBulkReadReady(...args) {
executeSoon(() => {
// Ensure the transport is still alive by the time this runs.
if (this.active) {
this.emit("bulkpacket", ...args);
this.hooks.onBulkPacket(...args);
}
});
},
/**
* Remove all handlers and references related to the current incoming
* packet, either because it is now complete or because the transport
* is closing.
*/
_destroyIncoming() {
if (this._incoming) {
this._incoming.destroy();
}
this._incomingHeader = "";
this._incoming = null;
},
};

View File

@ -0,0 +1,17 @@
FROM node:6.12.3
RUN apt-get update && \
apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \
rm -rf /var/lib/apt/lists/*
# Add user so we don't need --no-sandbox.
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
&& mkdir -p /home/pptruser/Downloads \
&& chown -R pptruser:pptruser /home/pptruser
# Run everything after as non-privileged user.
USER pptruser

View File

@ -0,0 +1,17 @@
FROM node:8.11.3-stretch
RUN apt-get update && \
apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \
rm -rf /var/lib/apt/lists/*
# Add user so we don't need --no-sandbox.
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
&& mkdir -p /home/pptruser/Downloads \
&& chown -R pptruser:pptruser /home/pptruser
# Run everything after as non-privileged user.
USER pptruser

View File

@ -0,0 +1,11 @@
FROM microsoft/windowsservercore:latest
ENV NODE_VERSION 8.11.3
RUN setx /m PATH "%PATH%;C:\nodejs"
RUN powershell -Command \
netsh interface ipv4 set subinterface 18 mtu=1460 store=persistent ; \
Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \
Expand-Archive node.zip -DestinationPath C:\ ; \
Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs'

View File

@ -0,0 +1,31 @@
env:
DISPLAY: :99.0
task:
name: node8 (linux)
container:
dockerfile: .ci/node8/Dockerfile.linux
xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24
install_script: npm install
test_script: npm run funit
task:
name: node8 (macOS)
osx_instance:
image: high-sierra-base
env:
HOMEBREW_NO_AUTO_UPDATE: 1
node_install_script:
- brew install node@8
- brew link --force node@8
install_script: npm install
test_script: npm run funit
# task:
# allow_failures: true
# windows_container:
# dockerfile: .ci/node8/Dockerfile.windows
# os_version: 2016
# name: node8 (windows)
# install_script: npm install --unsafe-perm
# test_script: npm run funit

View File

@ -0,0 +1,10 @@
/node_modules/
.DS_Store
*.swp
*.pyc
.vscode
package-lock.json
yarn.lock
.local-browser
/test/output-chromium
/test/output-firefox

View File

@ -0,0 +1,37 @@
# exclude all tests
test
utils/node6-transform
# exclude internal type definition files
/lib/*.d.ts
/node6/lib/*.d.ts
# repeats from .gitignore
node_modules
.local-chromium
.local-browser
.dev_profile*
.DS_Store
*.swp
*.pyc
.vscode
package-lock.json
/node6/test
/node6/utils
/test
/utils
/docs
yarn.lock
# other
/.ci
/examples
.appveyour.yml
.cirrus.yml
.editorconfig
.eslintignore
.eslintrc.js
.travis.yml
README.md
tsconfig.json

View File

@ -0,0 +1 @@
module.exports = require('./lib/Errors');

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Google Inc.
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.

View File

@ -0,0 +1,187 @@
# Puppeteer-Firefox
> Puppeteer-Firefox - Puppeteer API for Firefox
> **BEWARE**: This project is experimental. Alligators live here.
## Getting Started
### Installation
To use Puppeteer-Firefox in your project, run:
```bash
npm i puppeteer-firefox
# or "yarn add puppeteer-firefox"
```
Note: When you install Puppeteer-Firefox, it downloads a [custom-built Firefox](https://github.com/GoogleChrome/puppeteer/tree/master/experimental/juggler) that is guaranteed to work with the API.
### Usage
**Example** - navigating to https://example.com and saving a screenshot as *example.png*:
Save file as **example.js**
```js
const pptrFirefox = require('puppeteer-firefox');
(async () => {
const browser = await pptrFirefox.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'example.png'});
await browser.close();
})();
```
Execute script on the command line
```bash
node example.js
```
### API Status
- class: Puppeteer
* puppeteer.executablePath()
* puppeteer.launch([options])
- class: Browser
* event: 'targetchanged'
* event: 'targetcreated'
* event: 'targetdestroyed'
* browser.close()
* browser.newPage()
* browser.pages()
* browser.process()
* browser.targets()
* browser.userAgent()
* browser.version()
* browser.waitForTarget(predicate[, options])
- class: Target
* target.browser()
* target.page()
* target.type()
* target.url()
- class: Page
* event: 'close'
* event: 'console'
* event: 'dialog'
* event: 'domcontentloaded'
* event: 'frameattached'
* event: 'framedetached'
* event: 'framenavigated'
* event: 'load'
* event: 'pageerror'
* page.$(selector)
* page.$$(selector)
* page.$$eval(selector, pageFunction[, ...args])
* page.$eval(selector, pageFunction[, ...args])
* page.$x(expression)
* page.addScriptTag(options)
* page.addStyleTag(options)
* page.browser()
* page.click(selector[, options])
* page.close(options)
* page.content()
* page.evaluate(pageFunction, ...args)
* page.evaluateOnNewDocument(pageFunction, ...args)
* page.focus(selector)
* page.frames()
* page.goBack(options)
* page.goForward(options)
* page.goto(url, options)
* page.hover(selector)
* page.isClosed()
* page.keyboard
* page.mainFrame()
* page.mouse
* page.reload(options)
* page.screenshot([options])
* page.select(selector, ...values)
* page.setContent(html)
* page.setViewport(viewport)
* page.target()
* page.title()
* page.type(selector, text[, options])
* page.url()
* page.viewport()
* page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
* page.waitForFunction(pageFunction[, options[, ...args]])
* page.waitForNavigation(options)
* page.waitForSelector(selector[, options])
* page.waitForXPath(xpath[, options])
- class: Frame
* frame.$(selector)
* frame.$$(selector)
* frame.$$eval(selector, pageFunction[, ...args])
* frame.$eval(selector, pageFunction[, ...args])
* frame.$x(expression)
* frame.addScriptTag(options)
* frame.addStyleTag(options)
* frame.childFrames()
* frame.click(selector[, options])
* frame.content()
* frame.evaluate(pageFunction, ...args)
* frame.focus(selector)
* frame.hover(selector)
* frame.isDetached()
* frame.name()
* frame.parentFrame()
* frame.select(selector, ...values)
* frame.setContent(html)
* frame.title()
* frame.type(selector, text[, options])
* frame.url()
* frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
* frame.waitForFunction(pageFunction[, options[, ...args]])
* frame.waitForSelector(selector[, options])
* frame.waitForXPath(xpath[, options])
- class: JSHandle
* jsHandle.asElement()
* jsHandle.dispose()
* jsHandle.getProperties()
* jsHandle.getProperty(propertyName)
* jsHandle.jsonValue()
* jsHandle.toString()
- class: ElementHandle
* elementHandle.$(selector)
* elementHandle.$$(selector)
* elementHandle.$$eval(selector, pageFunction, ...args)
* elementHandle.$eval(selector, pageFunction, ...args)
* elementHandle.$x(expression)
* elementHandle.boundingBox()
* elementHandle.click([options])
* elementHandle.dispose()
* elementHandle.focus()
* elementHandle.hover()
* elementHandle.isIntersectingViewport()
* elementHandle.press(key[, options])
* elementHandle.screenshot([options])
* elementHandle.type(text[, options])
- class: Keyboard
* keyboard.down(key[, options])
* keyboard.press(key[, options])
* keyboard.sendCharacter(char)
* keyboard.type(text, options)
* keyboard.up(key)
- class: Mouse
* mouse.click(x, y, [options])
* mouse.down([options])
* mouse.move(x, y, [options])
* mouse.up([options])
- class: Dialog
* dialog.accept([promptText])
* dialog.defaultValue()
* dialog.dismiss()
* dialog.message()
* dialog.type()
- class: ConsoleMessage
* consoleMessage.args()
* consoleMessage.text()
* consoleMessage.type()
- class: TimeoutError
Special thanks to [Amine Zaza](https://bitbucket.org/aminerop/) who volunteered the [`puppeteer-firefox`](https://www.npmjs.com/package/puppeteer-firefox) NPM package.

View File

@ -0,0 +1,7 @@
set -e
total=`git grep ' \* \[' README.md| wc -l`
complete=`git grep ' \* \[x' README.md | wc -l`
ratio=`echo "$complete / $total * 100" | bc -l`
printf "%.2f%%\n" $ratio

View File

@ -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.
*/
'use strict';
const puppeteer = require('puppeteer-firefox');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://example.com');
await page.screenshot({path: 'example.png'});
await browser.close();
})();

View File

@ -0,0 +1,55 @@
/**
* 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.
*/
/**
* @fileoverview Search developers.google.com/web for articles tagged
* "Headless Chrome" and scrape results from the results page.
*/
'use strict';
const puppeteer = require('puppeteer-firefox');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://developers.google.com/web/');
// Type into search box.
await page.type('#searchbox input', 'Headless Chrome');
// Wait for suggest overlay to appear and click "show all results".
const allResultsSelector = '.devsite-suggest-all-results';
await page.waitForSelector(allResultsSelector);
await page.click(allResultsSelector);
// Wait for the results page to load and display the results.
const resultsSelector = '.gsc-results .gsc-thumbnail-inside a.gs-title';
await page.waitForSelector(resultsSelector);
// Extract the results from the page.
const links = await page.evaluate(resultsSelector => {
const anchors = Array.from(document.querySelectorAll(resultsSelector));
return anchors.map(anchor => {
const title = anchor.textContent.split('|')[0].trim();
return `${title} - ${anchor.href}`;
});
}, resultsSelector);
console.log(links.join('\n'));
await browser.close();
})();

View File

@ -0,0 +1,56 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const FirefoxLauncher = require('./lib/firefox/Launcher.js').Launcher;
const BrowserFetcher = require('./lib/firefox/BrowserFetcher.js');
class Puppeteer {
constructor() {
this._firefoxLauncher = new FirefoxLauncher();
}
async launch(options = {}) {
const {
args = [],
dumpio = !!process.env.DUMPIO,
handleSIGHUP = true,
handleSIGINT = true,
handleSIGTERM = true,
headless = (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true',
defaultViewport = {width: 800, height: 600},
ignoreHTTPSErrors = false,
slowMo = 0,
executablePath = this.executablePath(),
} = options;
options = {
args, slowMo, dumpio, executablePath, handleSIGHUP, handleSIGINT, handleSIGTERM, headless, defaultViewport,
ignoreHTTPSErrors
};
return await this._firefoxLauncher.launch(options);
}
createBrowserFetcher(options) {
return new BrowserFetcher(__dirname, options);
}
executablePath() {
const browserFetcher = new BrowserFetcher(__dirname, { product: 'firefox' });
const revision = require('./package.json').puppeteer.firefox_revision;
const revisionInfo = browserFetcher.revisionInfo(revision);
return revisionInfo.executablePath;
}
}
module.exports = new Puppeteer();

View File

@ -0,0 +1,153 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const os = require('os');
const fs = require('fs');
const path = require('path');
// puppeteer-core should not install anything.
if (require('./package.json').name === 'puppeteer-core')
return;
const downloadHost = process.env.PUPPETEER_DOWNLOAD_HOST || process.env.npm_config_puppeteer_download_host || process.env.npm_package_config_puppeteer_download_host;
const puppeteer = require('./index');
const browserFetcher = puppeteer.createBrowserFetcher({ host: downloadHost, product: 'firefox' });
const revision = require('./package.json').puppeteer.firefox_revision;
const revisionInfo = browserFetcher.revisionInfo(revision);
// Do nothing if the revision is already downloaded.
if (revisionInfo.local)
return;
// Override current environment proxy settings with npm configuration, if any.
const NPM_HTTPS_PROXY = process.env.npm_config_https_proxy || process.env.npm_config_proxy;
const NPM_HTTP_PROXY = process.env.npm_config_http_proxy || process.env.npm_config_proxy;
const NPM_NO_PROXY = process.env.npm_config_no_proxy;
if (NPM_HTTPS_PROXY)
process.env.HTTPS_PROXY = NPM_HTTPS_PROXY;
if (NPM_HTTP_PROXY)
process.env.HTTP_PROXY = NPM_HTTP_PROXY;
if (NPM_NO_PROXY)
process.env.NO_PROXY = NPM_NO_PROXY;
browserFetcher.download(revisionInfo.revision, onProgress)
.then(() => browserFetcher.localRevisions())
.then(onSuccess)
.catch(onError);
/**
* @param {!Array<string>}
* @return {!Promise}
*/
function onSuccess(localRevisions) {
console.log('Firefox downloaded to ' + revisionInfo.folderPath);
localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);
// Remove previous firefox revisions.
const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));
return Promise.all([...cleanupOldVersions, installFirefoxPreferences()]);
}
/**
* @param {!Error} error
*/
function onError(error) {
console.error(`ERROR: Failed to download Firefox r${revision}!`);
console.error(error);
process.exit(1);
}
let progressBar = null;
let lastDownloadedBytes = 0;
function onProgress(downloadedBytes, totalBytes) {
if (!progressBar) {
const ProgressBar = require('progress');
progressBar = new ProgressBar(`Downloading Firefox+Puppeteer ${revision.substring(0, 8)} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
complete: '|',
incomplete: ' ',
width: 20,
total: totalBytes,
});
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
}
function toMegabytes(bytes) {
const mb = bytes / 1024 / 1024;
return `${Math.round(mb * 10) / 10} Mb`;
}
// Install browser preferences after downloading and unpacking
// firefox instances.
// Based on: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Enterprise_deployment_before_60#Configuration
async function installFirefoxPreferences() {
const revisionInfo = browserFetcher.revisionInfo(revision);
const firefoxFolder = path.dirname(revisionInfo.executablePath);
const {helper} = require('./lib/firefox/helper');
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
let prefPath = '';
let configPath = '';
if (os.platform() === 'darwin') {
prefPath = path.join(firefoxFolder, '..', 'Resources', 'defaults', 'pref');
configPath = path.join(firefoxFolder, '..', 'Resources');
} else if (os.platform() === 'linux') {
await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults'));
await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences'));
prefPath = path.join(firefoxFolder, 'browser', 'defaults', 'preferences');
configPath = firefoxFolder;
} else if (os.platform() === 'win32') {
prefPath = path.join(firefoxFolder, 'defaults', 'pref');
configPath = firefoxFolder;
} else {
throw new Error('Unsupported platform: ' + os.platform());
}
await Promise.all([
copyFile({
from: path.join(__dirname, 'misc', '00-puppeteer-prefs.js'),
to: path.join(prefPath, '00-puppeteer-prefs.js'),
}),
copyFile({
from: path.join(__dirname, 'misc', 'puppeteer.cfg'),
to: path.join(configPath, 'puppeteer.cfg'),
}),
]).then(() => {
console.log('Firefox preferences installed!');
});
}
function copyFile({from, to}) {
var rd = fs.createReadStream(from);
var wr = fs.createWriteStream(to);
return new Promise(function(resolve, reject) {
rd.on('error', reject);
wr.on('error', reject);
wr.on('finish', resolve);
rd.pipe(wr);
}).catch(function(error) {
rd.destroy();
wr.end();
throw error;
});
}

View File

@ -0,0 +1,29 @@
/**
* Copyright 2018 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.
*/
class CustomError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class TimeoutError extends CustomError {}
module.exports = {
TimeoutError,
};

View File

@ -0,0 +1,20 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const constants = {
DEFAULT_NAVIGATION_TIMEOUT: 30000,
};
module.exports = {constants};

View File

@ -0,0 +1,171 @@
const {helper} = require('./helper');
const {Page} = require('./Page');
const EventEmitter = require('events');
class Browser extends EventEmitter {
/**
* @param {!Puppeteer.Connection} connection
* @param {?Puppeteer.Viewport} defaultViewport
* @param {?Puppeteer.ChildProcess} process
* @param {function():void} closeCallback
*/
constructor(connection, defaultViewport, process, closeCallback) {
super();
this._connection = connection;
this._defaultViewport = defaultViewport;
this._process = process;
this._closeCallback = closeCallback;
/** @type {!Map<string, ?Target>} */
this._pageTargets = new Map();
this._eventListeners = [
helper.addEventListener(this._connection, 'Browser.tabOpened', this._onTabOpened.bind(this)),
helper.addEventListener(this._connection, 'Browser.tabClosed', this._onTabClosed.bind(this)),
helper.addEventListener(this._connection, 'Browser.tabNavigated', this._onTabNavigated.bind(this)),
];
}
/**
* @return {!Promise<string>}
*/
async userAgent() {
const info = await this._connection.send('Browser.getInfo');
return info.userAgent;
}
/**
* @return {!Promise<string>}
*/
async version() {
const info = await this._connection.send('Browser.getInfo');
return info.version;
}
/**
* @return {?Puppeteer.ChildProcess}
*/
process() {
return this._process;
}
/**
* @param {function(!Target):boolean} predicate
* @param {{timeout?: number}=} options
* @return {!Promise<!Target>}
*/
async waitForTarget(predicate, options = {}) {
const {
timeout = 30000
} = options;
const existingTarget = this.targets().find(predicate);
if (existingTarget)
return existingTarget;
let resolve;
const targetPromise = new Promise(x => resolve = x);
this.on(Browser.Events.TargetCreated, check);
this.on('targetchanged', check);
try {
if (!timeout)
return await targetPromise;
return await helper.waitWithTimeout(targetPromise, 'target', timeout);
} finally {
this.removeListener(Browser.Events.TargetCreated, check);
this.removeListener('targetchanged', check);
}
/**
* @param {!Target} target
*/
function check(target) {
if (predicate(target))
resolve(target);
}
}
async newPage() {
const {pageId} = await this._connection.send('Browser.newPage');
const target = this._pageTargets.get(pageId);
return await target.page();
}
async pages() {
const pageTargets = Array.from(this._pageTargets.values());
return await Promise.all(pageTargets.map(target => target.page()));
}
targets() {
return Array.from(this._pageTargets.values());
}
_onTabOpened({pageId, url}) {
const target = new Target(this._connection, this, pageId, url);
this._pageTargets.set(pageId, target);
this.emit(Browser.Events.TargetCreated, target);
}
_onTabClosed({pageId}) {
const target = this._pageTargets.get(pageId);
this._pageTargets.delete(pageId);
this.emit(Browser.Events.TargetDestroyed, target);
}
_onTabNavigated({pageId, url}) {
const target = this._pageTargets.get(pageId);
target._url = url;
this.emit(Browser.Events.TargetChanged, target);
}
async close() {
helper.removeEventListeners(this._eventListeners);
await this._closeCallback();
}
}
/** @enum {string} */
Browser.Events = {
TargetCreated: 'targetcreated',
TargetChanged: 'targetchanged',
TargetDestroyed: 'targetdestroyed'
}
class Target {
/**
*
* @param {*} connection
* @param {!Browser} browser
* @param {string} pageId
* @param {string} url
*/
constructor(connection, browser, pageId, url) {
this._browser = browser;
this._connection = connection;
this._pageId = pageId;
/** @type {?Promise<!Page>} */
this._pagePromise = null;
this._url = url;
}
/**
* @return {"page"|"background_page"|"service_worker"|"other"|"browser"}
*/
type() {
return 'page';
}
url() {
return this._url;
}
async page() {
if (!this._pagePromise)
this._pagePromise = Page.create(this._connection, this, this._pageId, this._browser._defaultViewport);
return this._pagePromise;
}
browser() {
return this._browser;
}
}
module.exports = {Browser, Target};

View File

@ -0,0 +1,342 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const os = require('os');
const fs = require('fs');
const path = require('path');
const extract = require('extract-zip');
const util = require('util');
const URL = require('url');
const {helper, assert} = require('./helper');
const removeRecursive = require('rimraf');
// @ts-ignore
const ProxyAgent = require('https-proxy-agent');
// @ts-ignore
const getProxyForUrl = require('proxy-from-env').getProxyForUrl;
const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';
const downloadURLs = {
chromium: {
linux: '%s/chromium-browser-snapshots/Linux_x64/%s/%s.zip',
mac: '%s/chromium-browser-snapshots/Mac/%s/%s.zip',
win32: '%s/chromium-browser-snapshots/Win/%s/%s.zip',
win64: '%s/chromium-browser-snapshots/Win_x64/%s/%s.zip',
},
firefox: {
linux: '%s/juggler-builds/%s/%s.zip',
mac: '%s/juggler-builds/%s/%s.zip',
win32: '%s/juggler-builds/%s/%s.zip',
win64: '%s/juggler-builds/%s/%s.zip',
},
};
/**
* @param {string} product
* @param {string} platform
* @param {string} revision
* @return {string}
*/
function archiveName(product, platform, revision) {
if (product === 'chromium') {
if (platform === 'linux')
return 'chrome-linux';
if (platform === 'mac')
return 'chrome-mac';
if (platform === 'win32' || platform === 'win64') {
// Windows archive name changed at r591479.
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
}
} else if (product === 'firefox') {
if (platform === 'linux')
return 'firefox-linux';
if (platform === 'mac')
return 'firefox-mac';
if (platform === 'win32' || platform === 'win64')
return 'firefox-' + platform;
}
return null;
}
/**
* @param {string} product
* @param {string} platform
* @param {string} host
* @param {string} revision
* @return {string}
*/
function downloadURL(product, platform, host, revision) {
return util.format(downloadURLs[product][platform], host, revision, archiveName(product, platform, revision));
}
const readdirAsync = helper.promisify(fs.readdir.bind(fs));
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
const chmodAsync = helper.promisify(fs.chmod.bind(fs));
function existsAsync(filePath) {
let fulfill = null;
const promise = new Promise(x => fulfill = x);
fs.access(filePath, err => fulfill(!err));
return promise;
}
class BrowserFetcher {
/**
* @param {string} projectRoot
* @param {!BrowserFetcher.Options=} options
*/
constructor(projectRoot, options = {}) {
this._product = (options.product || 'chromium').toLowerCase();
assert(this._product === 'chromium' || this._product === 'firefox', `Unkown product: "${options.product}"`);
this._downloadsFolder = options.path || path.join(projectRoot, '.local-browser');
this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
this._platform = options.platform || '';
if (!this._platform) {
const platform = os.platform();
if (platform === 'darwin')
this._platform = 'mac';
else if (platform === 'linux')
this._platform = 'linux';
else if (platform === 'win32')
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
assert(this._platform, 'Unsupported platform: ' + os.platform());
}
assert(downloadURLs[this._product][this._platform], 'Unsupported platform: ' + this._platform);
}
/**
* @return {string}
*/
platform() {
return this._platform;
}
/**
* @param {string} revision
* @return {!Promise<boolean>}
*/
canDownload(revision) {
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
let resolve;
const promise = new Promise(x => resolve = x);
const request = httpRequest(url, 'HEAD', response => {
resolve(response.statusCode === 200);
});
request.on('error', error => {
console.error(error);
resolve(false);
});
return promise;
}
/**
* @param {string} revision
* @param {?function(number, number)} progressCallback
* @return {!Promise<!BrowserFetcher.RevisionInfo>}
*/
async download(revision, progressCallback) {
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
const zipPath = path.join(this._downloadsFolder, `download-${this._product}-${this._platform}-${revision}.zip`);
const folderPath = this._getFolderPath(revision);
if (await existsAsync(folderPath))
return this.revisionInfo(revision);
if (!(await existsAsync(this._downloadsFolder)))
await mkdirAsync(this._downloadsFolder);
try {
await downloadFile(url, zipPath, progressCallback);
await extractZip(zipPath, folderPath);
} finally {
if (await existsAsync(zipPath))
await unlinkAsync(zipPath);
}
const revisionInfo = this.revisionInfo(revision);
if (revisionInfo)
await chmodAsync(revisionInfo.executablePath, 0o755);
return revisionInfo;
}
/**
* @return {!Promise<!Array<string>>}
*/
async localRevisions() {
if (!await existsAsync(this._downloadsFolder))
return [];
const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
}
/**
* @param {string} revision
*/
async remove(revision) {
const folderPath = this._getFolderPath(revision);
assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
await new Promise(fulfill => removeRecursive(folderPath, fulfill));
}
/**
* @param {string} revision
* @return {!BrowserFetcher.RevisionInfo}
*/
revisionInfo(revision) {
const folderPath = this._getFolderPath(revision);
let executablePath = '';
if (this._product === 'chromium') {
if (this._platform === 'mac')
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
else if (this._platform === 'linux')
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome');
else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome.exe');
else
throw new Error('Unsupported platform: ' + this._platform);
} else if (this._product === 'firefox') {
if (this._platform === 'mac')
executablePath = path.join(folderPath, 'firefox', 'Nightly.app', 'Contents', 'MacOS', 'Firefox');
else if (this._platform === 'linux')
executablePath = path.join(folderPath, 'firefox', 'firefox');
else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
else
throw new Error('Unsupported platform: ' + this._platform);
}
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
const local = fs.existsSync(folderPath);
return {revision, executablePath, folderPath, local, url};
}
/**
* @param {string} revision
* @return {string}
*/
_getFolderPath(revision) {
return path.join(this._downloadsFolder, this._product + '-' + this._platform + '-' + revision);
}
}
module.exports = BrowserFetcher;
/**
* @param {string} folderPath
* @return {?{platform: string, revision: string}}
*/
function parseFolderPath(folderPath) {
const name = path.basename(folderPath);
const splits = name.split('-');
if (splits.length !== 3)
return null;
const [product, platform, revision] = splits;
if (!downloadURLs[product][platform])
return null;
return {platform, revision};
}
/**
* @param {string} url
* @param {string} destinationPath
* @param {?function(number, number)} progressCallback
* @return {!Promise}
*/
function downloadFile(url, destinationPath, progressCallback) {
let fulfill, reject;
let downloadedBytes = 0;
let totalBytes = 0;
const promise = new Promise((x, y) => { fulfill = x; reject = y; });
const request = httpRequest(url, 'GET', response => {
if (response.statusCode !== 200) {
const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
// consume response data to free up memory
response.resume();
reject(error);
return;
}
const file = fs.createWriteStream(destinationPath);
file.on('finish', () => fulfill());
file.on('error', error => reject(error));
response.pipe(file);
totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10);
if (progressCallback)
response.on('data', onData);
});
request.on('error', error => reject(error));
return promise;
function onData(chunk) {
downloadedBytes += chunk.length;
progressCallback(downloadedBytes, totalBytes);
}
}
/**
* @param {string} zipPath
* @param {string} folderPath
* @return {!Promise<?Error>}
*/
function extractZip(zipPath, folderPath) {
return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => {
if (err)
reject(err);
else
fulfill();
}));
}
function httpRequest(url, method, response) {
/** @type {Object} */
const options = URL.parse(url);
options.method = method;
const proxyURL = getProxyForUrl(url);
if (proxyURL) {
/** @type {Object} */
const parsedProxyURL = URL.parse(proxyURL);
parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
options.agent = new ProxyAgent(parsedProxyURL);
options.rejectUnauthorized = false;
}
const requestCallback = res => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
httpRequest(res.headers.location, method, response);
else
response(res);
};
const request = options.protocol === 'https:' ?
require('https').request(options, requestCallback) :
require('http').request(options, requestCallback);
request.end();
return request;
}
/**
* @typedef {Object} BrowserFetcher.Options
* @property {string=} platform
* @property {string=} path
* @property {string=} host
*/
/**
* @typedef {Object} BrowserFetcher.RevisionInfo
* @property {string} folderPath
* @property {string} executablePath
* @property {string} url
* @property {boolean} local
* @property {string} revision
*/

View File

@ -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.
*/
const debugProtocol = require('debug')('hdfox:protocol');
const EventEmitter = require('events');
/**
* @internal
*/
class Connection extends EventEmitter {
/**
* @param {!Puppeteer.ConnectionTransport} transport
* @param {number=} delay
*/
constructor(transport, delay = 0) {
super();
this._lastId = 0;
/** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
this._callbacks = new Map();
this._delay = delay;
this._transport = transport;
this._transport.onmessage = this._onMessage.bind(this);
this._transport.onclose = this._onClose.bind(this);
this._closed = false;
}
/**
* @param {string} method
* @param {!Object=} params
* @return {!Promise<?Object>}
*/
send(method, params = {}) {
const id = ++this._lastId;
const message = JSON.stringify({id, method, params});
debugProtocol('SEND ► ' + message);
this._transport.send(message);
return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
});
}
/**
* @param {string} message
*/
async _onMessage(message) {
if (this._delay)
await new Promise(f => setTimeout(f, this._delay));
debugProtocol('◀ RECV ' + message);
const object = JSON.parse(message);
if (object.id) {
const callback = this._callbacks.get(object.id);
// Callbacks could be all rejected if someone has called `.dispose()`.
if (callback) {
this._callbacks.delete(object.id);
if (object.error)
callback.reject(createProtocolError(callback.error, callback.method, object));
else
callback.resolve(object.result);
}
} else {
this.emit(object.method, object.params);
}
}
_onClose() {
if (this._closed)
return;
this._closed = true;
this._transport.onmessage = null;
this._transport.onclose = null;
for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear();
this.emit(Connection.Events.Disconnected);
}
dispose() {
this._onClose();
this._transport.close();
}
}
Connection.Events = {
Disconnected: Symbol('Connection.Events.Disconnected'),
};
/**
* @param {!Error} error
* @param {string} method
* @param {{error: {message: string, data: any}}} object
* @return {!Error}
*/
function createProtocolError(error, method, object) {
let message = `Protocol error (${method}): ${object.error.message}`;
if ('data' in object.error)
message += ` ${object.error.data}`;
return rewriteError(error, message);
}
/**
* @param {!Error} error
* @param {string} message
* @return {!Error}
*/
function rewriteError(error, message) {
error.message = message;
return error;
}
module.exports = {Connection};

View File

@ -0,0 +1,57 @@
const {helper, assert, debugError} = require('./helper');
class Dialog {
constructor(client, payload) {
this._client = client;
this._dialogId = payload.dialogId;
this._type = payload.type;
this._message = payload.message;
this._handled = false;
this._defaultValue = payload.defaultValue || '';
}
/**
* @return {string}
*/
type() {
return this._type;
}
/**
* @return {string}
*/
message() {
return this._message;
}
/**
* @return {string}
*/
defaultValue() {
return this._defaultValue;
}
/**
* @param {string=} promptText
*/
async accept(promptText) {
assert(!this._handled, 'Cannot accept dialog which is already handled!');
this._handled = true;
await this._client.send('Page.handleDialog', {
dialogId: this._dialogId,
accept: true,
promptText: promptText
}).catch(debugError);
}
async dismiss() {
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
this._handled = true;
await this._client.send('Page.handleDialog', {
dialogId: this._dialogId,
accept: false
}).catch(debugError);
}
}
module.exports = {Dialog};

View File

@ -0,0 +1,126 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {Socket} = require('net');
/**
* @implements {!Puppeteer.ConnectionTransport}
* @internal
*/
class FirefoxTransport {
/**
* @param {number} port
* @return {!Promise<!FirefoxTransport>}
*/
static async create(port) {
const socket = new Socket();
try {
await new Promise((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
socket.connect({
port,
host: 'localhost'
});
});
} catch (e) {
socket.destroy();
throw e;
}
return new FirefoxTransport(socket);
}
/**
* @param {!Socket} socket
*/
constructor(socket) {
this._socket = socket;
this._socket.once('close', had_error => {
if (this.onclose)
this.onclose.call(null);
});
this._dispatchQueue = new DispatchQueue(this);
let buffer = Buffer.from('');
socket.on('data', async data => {
buffer = Buffer.concat([buffer, data]);
while (true) {
const bufferString = buffer.toString();
const seperatorIndex = bufferString.indexOf(':');
if (seperatorIndex === -1)
return;
const length = parseInt(bufferString.substring(0, seperatorIndex), 10);
if (buffer.length < length + seperatorIndex)
return;
const message = buffer.slice(seperatorIndex + 1, seperatorIndex + 1 + length).toString();
buffer = buffer.slice(seperatorIndex + 1 + length);
this._dispatchQueue.enqueue(message);
}
});
// Silently ignore all errors - we don't know what to do with them.
this._socket.on('error', () => {});
this.onmessage = null;
this.onclose = null;
}
/**
* @param {string} message
*/
send(message) {
this._socket.write(Buffer.byteLength(message) + ':' + message);
}
close() {
this._socket.destroy();
}
}
// We want to dispatch all "message" events in separate tasks
// to make sure all message-related promises are resolved first
// before dispatching next message.
//
// We cannot just use setTimeout() in Node.js here like we would
// do in Browser - see https://github.com/nodejs/node/issues/23773
// Thus implement a dispatch queue that enforces new tasks manually.
/**
* @internal
*/
class DispatchQueue {
constructor(transport) {
this._transport = transport;
this._timeoutId = null;
this._queue = [];
this._dispatch = this._dispatch.bind(this);
}
enqueue(message) {
this._queue.push(message);
if (!this._timeoutId)
this._timeoutId = setTimeout(this._dispatch, 0);
}
_dispatch() {
const message = this._queue.shift();
if (this._queue.length)
this._timeoutId = setTimeout(this._dispatch, 0)
else
this._timeoutId = null;
if (this._transport.onmessage)
this._transport.onmessage.call(null, message);
}
}
module.exports = FirefoxTransport;

View File

@ -0,0 +1,295 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const keyDefinitions = require('./USKeyboardLayout');
const os = require('os');
/**
* @typedef {Object} KeyDescription
* @property {number} keyCode
* @property {string} key
* @property {string} text
* @property {string} code
* @property {number} location
*/
class Keyboard {
constructor(client) {
this._client = client;
this._modifiers = 0;
this._pressedKeys = new Set();
}
/**
* @param {string} key
*/
async down(key) {
const description = this._keyDescriptionForString(key);
const repeat = this._pressedKeys.has(description.code);
this._pressedKeys.add(description.code);
this._modifiers |= this._modifierBit(description.key);
await this._client.send('Page.dispatchKeyEvent', {
type: 'keydown',
keyCode: description.keyCode,
code: description.code,
key: description.key,
repeat,
location: description.location
});
}
/**
* @param {string} key
* @return {number}
*/
_modifierBit(key) {
if (key === 'Alt')
return 1;
if (key === 'Control')
return 2;
if (key === 'Shift')
return 4;
if (key === 'Meta')
return 8;
return 0;
}
/**
* @param {string} keyString
* @return {KeyDescription}
*/
_keyDescriptionForString(keyString) {
const shift = this._modifiers & 8;
const description = {
key: '',
keyCode: 0,
code: '',
text: '',
location: 0
};
const definition = keyDefinitions[keyString];
if (!definition)
throw new Error(`Unknown key: "${keyString}"`);
if (definition.key)
description.key = definition.key;
if (shift && definition.shiftKey)
description.key = definition.shiftKey;
if (definition.keyCode)
description.keyCode = definition.keyCode;
if (shift && definition.shiftKeyCode)
description.keyCode = definition.shiftKeyCode;
if (definition.code)
description.code = definition.code;
if (definition.location)
description.location = definition.location;
if (description.key.length === 1)
description.text = description.key;
if (definition.text)
description.text = definition.text;
if (shift && definition.shiftText)
description.text = definition.shiftText;
// if any modifiers besides shift are pressed, no text should be sent
if (this._modifiers & ~8)
description.text = '';
// Firefox calls the 'Meta' key 'OS' on everything but mac
if (os.platform() !== 'darwin' && description.key === 'Meta')
description.key = 'OS';
if (description.code === 'MetaLeft')
description.code = 'OSLeft';
if (description.code === 'MetaRight')
description.code = 'OSRight';
return description;
}
/**
* @param {string} key
*/
async up(key) {
const description = this._keyDescriptionForString(key);
this._modifiers &= ~this._modifierBit(description.key);
this._pressedKeys.delete(description.code);
await this._client.send('Page.dispatchKeyEvent', {
type: 'keyup',
key: description.key,
keyCode: description.keyCode,
code: description.code,
location: description.location,
repeat: false
});
}
/**
* @param {string} char
*/
async sendCharacter(char) {
await this._client.send('Page.insertText', {
text: char
});
}
/**
* @param {string} text
* @param {!{delay?: number}=} options
*/
async type(text, options = {}) {
const {delay = null} = options;
for (const char of text) {
if (keyDefinitions[char])
await this.press(char, {delay});
else
await this.sendCharacter(char);
if (delay !== null)
await new Promise(f => setTimeout(f, delay));
}
}
/**
* @param {string} key
* @param {!{delay?: number}=} options
*/
async press(key, options = {}) {
const {delay = null} = options;
await this.down(key);
if (delay !== null)
await new Promise(f => setTimeout(f, options.delay));
await this.up(key);
}
}
class Mouse {
/**
* @param {!Keyboard} keyboard
*/
constructor(client, keyboard) {
this._client = client;
this._keyboard = keyboard;
this._x = 0;
this._y = 0;
this._buttons = 0;
}
/**
* @param {number} x
* @param {number} y
* @param {{steps?: number}=} options
*/
async move(x, y, options = {}) {
const {steps = 1} = options;
const fromX = this._x, fromY = this._y;
this._x = x;
this._y = y;
for (let i = 1; i <= steps; i++) {
await this._client.send('Page.dispatchMouseEvent', {
type: 'mousemove',
button: 0,
x: fromX + (this._x - fromX) * (i / steps),
y: fromY + (this._y - fromY) * (i / steps),
modifiers: this._keyboard._modifiers,
buttons: this._buttons,
});
}
}
/**
* @param {number} x
* @param {number} y
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
*/
async click(x, y, options = {}) {
const {delay = null} = options;
this.move(x, y);
this.down(options);
if (delay !== null)
await new Promise(f => setTimeout(f, delay));
await this.up(options);
}
/**
* @param {!{button?: string, clickCount?: number}=} options
*/
async down(options = {}) {
const {
button = "left",
clickCount = 1
} = options;
if (button === 'left')
this._buttons |= 1;
if (button === 'right')
this._buttons |= 2;
if (button === 'middle')
this._buttons |= 4;
await this._client.send('Page.dispatchMouseEvent', {
type: 'mousedown',
button: this._buttonNameToButton(button),
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
clickCount,
buttons: this._buttons,
});
}
/**
* @param {string} buttonName
* @return {number}
*/
_buttonNameToButton(buttonName) {
if (buttonName === 'left')
return 0;
if (buttonName === 'middle')
return 1;
if (buttonName === 'right')
return 2;
}
/**
* @param {!{button?: string, clickCount?: number}=} options
*/
async up(options = {}) {
const {
button = "left",
clickCount = 1
} = options;
if (button === 'left')
this._buttons &= ~1;
if (button === 'right')
this._buttons &= ~2;
if (button === 'middle')
this._buttons &= ~4;
await this._client.send('Page.dispatchMouseEvent', {
type: 'mouseup',
button: this._buttonNameToButton(button),
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
clickCount: clickCount,
buttons: this._buttons,
});
}
}
module.exports = { Keyboard, Mouse };

View File

@ -0,0 +1,206 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const os = require('os');
const path = require('path');
const removeFolder = require('rimraf');
const childProcess = require('child_process');
const {Connection} = require('./Connection');
const {Browser} = require('./Browser');
const readline = require('readline');
const fs = require('fs');
const util = require('util');
const {helper} = require('./helper');
const {TimeoutError} = require('../Errors')
const FirefoxTransport = require('./FirefoxTransport');
const mkdtempAsync = util.promisify(fs.mkdtemp);
const removeFolderAsync = util.promisify(removeFolder);
const FIREFOX_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_firefox_profile-');
/**
* @internal
*/
class Launcher {
/**
* @param {Object} options
* @return {!Promise<!Browser>}
*/
async launch(options = {}) {
const {
args = [],
dumpio = false,
executablePath = null,
handleSIGHUP = true,
handleSIGINT = true,
handleSIGTERM = true,
ignoreHTTPSErrors = false,
headless = true,
defaultViewport = {width: 800, height: 600},
slowMo = 0,
} = options;
if (!executablePath)
throw new Error('Firefox launching is only supported with local version of firefox!');
const firefoxArguments = args.slice();
firefoxArguments.push('-no-remote');
firefoxArguments.push('-juggler', '0');
firefoxArguments.push('-foreground');
if (headless)
firefoxArguments.push('-headless');
let temporaryProfileDir = null;
if (!firefoxArguments.some(arg => arg.startsWith('-profile') || arg.startsWith('--profile'))) {
temporaryProfileDir = await mkdtempAsync(FIREFOX_PROFILE_PATH);
firefoxArguments.push(`-profile`, temporaryProfileDir);
}
if (firefoxArguments.every(arg => arg.startsWith('--') || arg.startsWith('-')))
firefoxArguments.push('about:blank');
const stdio = ['pipe', 'pipe', 'pipe'];
const firefoxProcess = childProcess.spawn(
executablePath,
firefoxArguments,
{
// On non-windows platforms, `detached: false` makes child process a leader of a new
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
detached: process.platform !== 'win32',
stdio
}
);
if (dumpio) {
firefoxProcess.stderr.pipe(process.stderr);
firefoxProcess.stdout.pipe(process.stdout);
}
let firefoxClosed = false;
const waitForFirefoxToClose = new Promise((fulfill, reject) => {
firefoxProcess.once('exit', () => {
firefoxClosed = true;
// Cleanup as processes exit.
if (temporaryProfileDir) {
removeFolderAsync(temporaryProfileDir)
.then(() => fulfill())
.catch(err => console.error(err));
} else {
fulfill();
}
});
});
const listeners = [ helper.addEventListener(process, 'exit', killFirefox) ];
if (handleSIGINT)
listeners.push(helper.addEventListener(process, 'SIGINT', () => { killFirefox(); process.exit(130); }));
if (handleSIGTERM)
listeners.push(helper.addEventListener(process, 'SIGTERM', killFirefox));
if (handleSIGHUP)
listeners.push(helper.addEventListener(process, 'SIGHUP', killFirefox));
/** @type {?Connection} */
let connection = null;
try {
const port = await waitForWSEndpoint(firefoxProcess, 30000);
const transport = await FirefoxTransport.create(parseInt(port, 10));
connection = new Connection(transport, slowMo);
const browser = new Browser(connection, defaultViewport, firefoxProcess, killFirefox);
if (ignoreHTTPSErrors)
await connection.send('Browser.setIgnoreHTTPSErrors', {enabled: true});
if (!browser.targets().length)
await new Promise(x => browser.once('targetcreated', x));
return browser;
} catch (e) {
killFirefox();
throw e;
}
// This method has to be sync to be used as 'exit' event handler.
function killFirefox() {
helper.removeEventListeners(listeners);
if (firefoxProcess.pid && !firefoxProcess.killed && !firefoxClosed) {
// Force kill chrome.
try {
if (process.platform === 'win32')
childProcess.execSync(`taskkill /pid ${firefoxProcess.pid} /T /F`);
else
process.kill(-firefoxProcess.pid, 'SIGKILL');
} catch (e) {
// the process might have already stopped
}
}
// Attempt to remove temporary profile directory to avoid littering.
try {
removeFolder.sync(temporaryProfileDir);
} catch (e) { }
}
}
}
/**
* @param {!Puppeteer.ChildProcess} firefoxProcess
* @param {number} timeout
* @return {!Promise<string>}
*/
function waitForWSEndpoint(firefoxProcess, timeout) {
return new Promise((resolve, reject) => {
const rl = readline.createInterface({ input: firefoxProcess.stdout });
let stderr = '';
const listeners = [
helper.addEventListener(rl, 'line', onLine),
helper.addEventListener(rl, 'close', () => onClose()),
helper.addEventListener(firefoxProcess, 'exit', () => onClose()),
helper.addEventListener(firefoxProcess, 'error', error => onClose(error))
];
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
/**
* @param {!Error=} error
*/
function onClose(error) {
cleanup();
reject(new Error([
'Failed to launch Firefox!' + (error ? ' ' + error.message : ''),
stderr,
'',
].join('\n')));
}
function onTimeout() {
cleanup();
reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`));
}
/**
* @param {string} line
*/
function onLine(line) {
stderr += line + '\n';
const match = line.match(/^Juggler listening on (\d+)$/);
if (!match)
return;
cleanup();
resolve(match[1]);
}
function cleanup() {
if (timeoutId)
clearTimeout(timeoutId);
helper.removeEventListeners(listeners);
}
});
}
module.exports = {Launcher};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,281 @@
/**
* 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.
*/
/**
* @typedef {Object} KeyDefinition
* @property {number=} keyCode
* @property {number=} shiftKeyCode
* @property {string=} key
* @property {string=} shiftKey
* @property {string=} code
* @property {string=} text
* @property {string=} shiftText
* @property {number=} location
*/
/**
* @type {Object<string, KeyDefinition>}
*/
module.exports = {
'0': {'keyCode': 48, 'key': '0', 'code': 'Digit0'},
'1': {'keyCode': 49, 'key': '1', 'code': 'Digit1'},
'2': {'keyCode': 50, 'key': '2', 'code': 'Digit2'},
'3': {'keyCode': 51, 'key': '3', 'code': 'Digit3'},
'4': {'keyCode': 52, 'key': '4', 'code': 'Digit4'},
'5': {'keyCode': 53, 'key': '5', 'code': 'Digit5'},
'6': {'keyCode': 54, 'key': '6', 'code': 'Digit6'},
'7': {'keyCode': 55, 'key': '7', 'code': 'Digit7'},
'8': {'keyCode': 56, 'key': '8', 'code': 'Digit8'},
'9': {'keyCode': 57, 'key': '9', 'code': 'Digit9'},
'Power': {'key': 'Power', 'code': 'Power'},
'Eject': {'key': 'Eject', 'code': 'Eject'},
'Abort': {'keyCode': 3, 'code': 'Abort', 'key': 'Cancel'},
'Help': {'keyCode': 6, 'code': 'Help', 'key': 'Help'},
'Backspace': {'keyCode': 8, 'code': 'Backspace', 'key': 'Backspace'},
'Tab': {'keyCode': 9, 'code': 'Tab', 'key': 'Tab'},
'Numpad5': {'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3},
'NumpadEnter': {'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3},
'Enter': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'\r': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'\n': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'ShiftLeft': {'keyCode': 16, 'code': 'ShiftLeft', 'key': 'Shift', 'location': 1},
'ShiftRight': {'keyCode': 16, 'code': 'ShiftRight', 'key': 'Shift', 'location': 2},
'ControlLeft': {'keyCode': 17, 'code': 'ControlLeft', 'key': 'Control', 'location': 1},
'ControlRight': {'keyCode': 17, 'code': 'ControlRight', 'key': 'Control', 'location': 2},
'AltLeft': {'keyCode': 18, 'code': 'AltLeft', 'key': 'Alt', 'location': 1},
'AltRight': {'keyCode': 18, 'code': 'AltRight', 'key': 'Alt', 'location': 2},
'Pause': {'keyCode': 19, 'code': 'Pause', 'key': 'Pause'},
'CapsLock': {'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock'},
'Escape': {'keyCode': 27, 'code': 'Escape', 'key': 'Escape'},
'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'},
'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'},
'Space': {'keyCode': 32, 'code': 'Space', 'key': ' '},
'Numpad9': {'keyCode': 33, 'shiftKeyCode': 105, 'key': 'PageUp', 'code': 'Numpad9', 'shiftKey': '9', 'location': 3},
'PageUp': {'keyCode': 33, 'code': 'PageUp', 'key': 'PageUp'},
'Numpad3': {'keyCode': 34, 'shiftKeyCode': 99, 'key': 'PageDown', 'code': 'Numpad3', 'shiftKey': '3', 'location': 3},
'PageDown': {'keyCode': 34, 'code': 'PageDown', 'key': 'PageDown'},
'End': {'keyCode': 35, 'code': 'End', 'key': 'End'},
'Numpad1': {'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3},
'Home': {'keyCode': 36, 'code': 'Home', 'key': 'Home'},
'Numpad7': {'keyCode': 36, 'shiftKeyCode': 103, 'key': 'Home', 'code': 'Numpad7', 'shiftKey': '7', 'location': 3},
'ArrowLeft': {'keyCode': 37, 'code': 'ArrowLeft', 'key': 'ArrowLeft'},
'Numpad4': {'keyCode': 37, 'shiftKeyCode': 100, 'key': 'ArrowLeft', 'code': 'Numpad4', 'shiftKey': '4', 'location': 3},
'Numpad8': {'keyCode': 38, 'shiftKeyCode': 104, 'key': 'ArrowUp', 'code': 'Numpad8', 'shiftKey': '8', 'location': 3},
'ArrowUp': {'keyCode': 38, 'code': 'ArrowUp', 'key': 'ArrowUp'},
'ArrowRight': {'keyCode': 39, 'code': 'ArrowRight', 'key': 'ArrowRight'},
'Numpad6': {'keyCode': 39, 'shiftKeyCode': 102, 'key': 'ArrowRight', 'code': 'Numpad6', 'shiftKey': '6', 'location': 3},
'Numpad2': {'keyCode': 40, 'shiftKeyCode': 98, 'key': 'ArrowDown', 'code': 'Numpad2', 'shiftKey': '2', 'location': 3},
'ArrowDown': {'keyCode': 40, 'code': 'ArrowDown', 'key': 'ArrowDown'},
'Select': {'keyCode': 41, 'code': 'Select', 'key': 'Select'},
'Open': {'keyCode': 43, 'code': 'Open', 'key': 'Execute'},
'PrintScreen': {'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen'},
'Insert': {'keyCode': 45, 'code': 'Insert', 'key': 'Insert'},
'Numpad0': {'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3},
'Delete': {'keyCode': 46, 'code': 'Delete', 'key': 'Delete'},
'NumpadDecimal': {'keyCode': 46, 'shiftKeyCode': 110, 'code': 'NumpadDecimal', 'key': '\u0000', 'shiftKey': '.', 'location': 3},
'Digit0': {'keyCode': 48, 'code': 'Digit0', 'shiftKey': ')', 'key': '0'},
'Digit1': {'keyCode': 49, 'code': 'Digit1', 'shiftKey': '!', 'key': '1'},
'Digit2': {'keyCode': 50, 'code': 'Digit2', 'shiftKey': '@', 'key': '2'},
'Digit3': {'keyCode': 51, 'code': 'Digit3', 'shiftKey': '#', 'key': '3'},
'Digit4': {'keyCode': 52, 'code': 'Digit4', 'shiftKey': '$', 'key': '4'},
'Digit5': {'keyCode': 53, 'code': 'Digit5', 'shiftKey': '%', 'key': '5'},
'Digit6': {'keyCode': 54, 'code': 'Digit6', 'shiftKey': '^', 'key': '6'},
'Digit7': {'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7'},
'Digit8': {'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8'},
'Digit9': {'keyCode': 57, 'code': 'Digit9', 'shiftKey': '\(', 'key': '9'},
'KeyA': {'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a'},
'KeyB': {'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b'},
'KeyC': {'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c'},
'KeyD': {'keyCode': 68, 'code': 'KeyD', 'shiftKey': 'D', 'key': 'd'},
'KeyE': {'keyCode': 69, 'code': 'KeyE', 'shiftKey': 'E', 'key': 'e'},
'KeyF': {'keyCode': 70, 'code': 'KeyF', 'shiftKey': 'F', 'key': 'f'},
'KeyG': {'keyCode': 71, 'code': 'KeyG', 'shiftKey': 'G', 'key': 'g'},
'KeyH': {'keyCode': 72, 'code': 'KeyH', 'shiftKey': 'H', 'key': 'h'},
'KeyI': {'keyCode': 73, 'code': 'KeyI', 'shiftKey': 'I', 'key': 'i'},
'KeyJ': {'keyCode': 74, 'code': 'KeyJ', 'shiftKey': 'J', 'key': 'j'},
'KeyK': {'keyCode': 75, 'code': 'KeyK', 'shiftKey': 'K', 'key': 'k'},
'KeyL': {'keyCode': 76, 'code': 'KeyL', 'shiftKey': 'L', 'key': 'l'},
'KeyM': {'keyCode': 77, 'code': 'KeyM', 'shiftKey': 'M', 'key': 'm'},
'KeyN': {'keyCode': 78, 'code': 'KeyN', 'shiftKey': 'N', 'key': 'n'},
'KeyO': {'keyCode': 79, 'code': 'KeyO', 'shiftKey': 'O', 'key': 'o'},
'KeyP': {'keyCode': 80, 'code': 'KeyP', 'shiftKey': 'P', 'key': 'p'},
'KeyQ': {'keyCode': 81, 'code': 'KeyQ', 'shiftKey': 'Q', 'key': 'q'},
'KeyR': {'keyCode': 82, 'code': 'KeyR', 'shiftKey': 'R', 'key': 'r'},
'KeyS': {'keyCode': 83, 'code': 'KeyS', 'shiftKey': 'S', 'key': 's'},
'KeyT': {'keyCode': 84, 'code': 'KeyT', 'shiftKey': 'T', 'key': 't'},
'KeyU': {'keyCode': 85, 'code': 'KeyU', 'shiftKey': 'U', 'key': 'u'},
'KeyV': {'keyCode': 86, 'code': 'KeyV', 'shiftKey': 'V', 'key': 'v'},
'KeyW': {'keyCode': 87, 'code': 'KeyW', 'shiftKey': 'W', 'key': 'w'},
'KeyX': {'keyCode': 88, 'code': 'KeyX', 'shiftKey': 'X', 'key': 'x'},
'KeyY': {'keyCode': 89, 'code': 'KeyY', 'shiftKey': 'Y', 'key': 'y'},
'KeyZ': {'keyCode': 90, 'code': 'KeyZ', 'shiftKey': 'Z', 'key': 'z'},
'MetaLeft': {'keyCode': 91, 'code': 'MetaLeft', 'key': 'Meta', 'location': 1},
'MetaRight': {'keyCode': 92, 'code': 'MetaRight', 'key': 'Meta', 'location': 2},
'ContextMenu': {'keyCode': 93, 'code': 'ContextMenu', 'key': 'ContextMenu'},
'NumpadMultiply': {'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3},
'NumpadAdd': {'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3},
'NumpadSubtract': {'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3},
'NumpadDivide': {'keyCode': 111, 'code': 'NumpadDivide', 'key': '/', 'location': 3},
'F1': {'keyCode': 112, 'code': 'F1', 'key': 'F1'},
'F2': {'keyCode': 113, 'code': 'F2', 'key': 'F2'},
'F3': {'keyCode': 114, 'code': 'F3', 'key': 'F3'},
'F4': {'keyCode': 115, 'code': 'F4', 'key': 'F4'},
'F5': {'keyCode': 116, 'code': 'F5', 'key': 'F5'},
'F6': {'keyCode': 117, 'code': 'F6', 'key': 'F6'},
'F7': {'keyCode': 118, 'code': 'F7', 'key': 'F7'},
'F8': {'keyCode': 119, 'code': 'F8', 'key': 'F8'},
'F9': {'keyCode': 120, 'code': 'F9', 'key': 'F9'},
'F10': {'keyCode': 121, 'code': 'F10', 'key': 'F10'},
'F11': {'keyCode': 122, 'code': 'F11', 'key': 'F11'},
'F12': {'keyCode': 123, 'code': 'F12', 'key': 'F12'},
'F13': {'keyCode': 124, 'code': 'F13', 'key': 'F13'},
'F14': {'keyCode': 125, 'code': 'F14', 'key': 'F14'},
'F15': {'keyCode': 126, 'code': 'F15', 'key': 'F15'},
'F16': {'keyCode': 127, 'code': 'F16', 'key': 'F16'},
'F17': {'keyCode': 128, 'code': 'F17', 'key': 'F17'},
'F18': {'keyCode': 129, 'code': 'F18', 'key': 'F18'},
'F19': {'keyCode': 130, 'code': 'F19', 'key': 'F19'},
'F20': {'keyCode': 131, 'code': 'F20', 'key': 'F20'},
'F21': {'keyCode': 132, 'code': 'F21', 'key': 'F21'},
'F22': {'keyCode': 133, 'code': 'F22', 'key': 'F22'},
'F23': {'keyCode': 134, 'code': 'F23', 'key': 'F23'},
'F24': {'keyCode': 135, 'code': 'F24', 'key': 'F24'},
'NumLock': {'keyCode': 144, 'code': 'NumLock', 'key': 'NumLock'},
'ScrollLock': {'keyCode': 145, 'code': 'ScrollLock', 'key': 'ScrollLock'},
'AudioVolumeMute': {'keyCode': 173, 'code': 'AudioVolumeMute', 'key': 'AudioVolumeMute'},
'AudioVolumeDown': {'keyCode': 174, 'code': 'AudioVolumeDown', 'key': 'AudioVolumeDown'},
'AudioVolumeUp': {'keyCode': 175, 'code': 'AudioVolumeUp', 'key': 'AudioVolumeUp'},
'MediaTrackNext': {'keyCode': 176, 'code': 'MediaTrackNext', 'key': 'MediaTrackNext'},
'MediaTrackPrevious': {'keyCode': 177, 'code': 'MediaTrackPrevious', 'key': 'MediaTrackPrevious'},
'MediaStop': {'keyCode': 178, 'code': 'MediaStop', 'key': 'MediaStop'},
'MediaPlayPause': {'keyCode': 179, 'code': 'MediaPlayPause', 'key': 'MediaPlayPause'},
'Semicolon': {'keyCode': 186, 'code': 'Semicolon', 'shiftKey': ':', 'key': ';'},
'Equal': {'keyCode': 187, 'code': 'Equal', 'shiftKey': '+', 'key': '='},
'NumpadEqual': {'keyCode': 187, 'code': 'NumpadEqual', 'key': '=', 'location': 3},
'Comma': {'keyCode': 188, 'code': 'Comma', 'shiftKey': '\<', 'key': ','},
'Minus': {'keyCode': 189, 'code': 'Minus', 'shiftKey': '_', 'key': '-'},
'Period': {'keyCode': 190, 'code': 'Period', 'shiftKey': '>', 'key': '.'},
'Slash': {'keyCode': 191, 'code': 'Slash', 'shiftKey': '?', 'key': '/'},
'Backquote': {'keyCode': 192, 'code': 'Backquote', 'shiftKey': '~', 'key': '`'},
'BracketLeft': {'keyCode': 219, 'code': 'BracketLeft', 'shiftKey': '{', 'key': '['},
'Backslash': {'keyCode': 220, 'code': 'Backslash', 'shiftKey': '|', 'key': '\\'},
'BracketRight': {'keyCode': 221, 'code': 'BracketRight', 'shiftKey': '}', 'key': ']'},
'Quote': {'keyCode': 222, 'code': 'Quote', 'shiftKey': '"', 'key': '\''},
'AltGraph': {'keyCode': 225, 'code': 'AltGraph', 'key': 'AltGraph'},
'Props': {'keyCode': 247, 'code': 'Props', 'key': 'CrSel'},
'Cancel': {'keyCode': 3, 'key': 'Cancel', 'code': 'Abort'},
'Clear': {'keyCode': 12, 'key': 'Clear', 'code': 'Numpad5', 'location': 3},
'Shift': {'keyCode': 16, 'key': 'Shift', 'code': 'ShiftLeft', 'location': 1},
'Control': {'keyCode': 17, 'key': 'Control', 'code': 'ControlLeft', 'location': 1},
'Alt': {'keyCode': 18, 'key': 'Alt', 'code': 'AltLeft', 'location': 1},
'Accept': {'keyCode': 30, 'key': 'Accept'},
'ModeChange': {'keyCode': 31, 'key': 'ModeChange'},
' ': {'keyCode': 32, 'key': ' ', 'code': 'Space'},
'Print': {'keyCode': 42, 'key': 'Print'},
'Execute': {'keyCode': 43, 'key': 'Execute', 'code': 'Open'},
'\u0000': {'keyCode': 46, 'key': '\u0000', 'code': 'NumpadDecimal', 'location': 3},
'a': {'keyCode': 65, 'key': 'a', 'code': 'KeyA'},
'b': {'keyCode': 66, 'key': 'b', 'code': 'KeyB'},
'c': {'keyCode': 67, 'key': 'c', 'code': 'KeyC'},
'd': {'keyCode': 68, 'key': 'd', 'code': 'KeyD'},
'e': {'keyCode': 69, 'key': 'e', 'code': 'KeyE'},
'f': {'keyCode': 70, 'key': 'f', 'code': 'KeyF'},
'g': {'keyCode': 71, 'key': 'g', 'code': 'KeyG'},
'h': {'keyCode': 72, 'key': 'h', 'code': 'KeyH'},
'i': {'keyCode': 73, 'key': 'i', 'code': 'KeyI'},
'j': {'keyCode': 74, 'key': 'j', 'code': 'KeyJ'},
'k': {'keyCode': 75, 'key': 'k', 'code': 'KeyK'},
'l': {'keyCode': 76, 'key': 'l', 'code': 'KeyL'},
'm': {'keyCode': 77, 'key': 'm', 'code': 'KeyM'},
'n': {'keyCode': 78, 'key': 'n', 'code': 'KeyN'},
'o': {'keyCode': 79, 'key': 'o', 'code': 'KeyO'},
'p': {'keyCode': 80, 'key': 'p', 'code': 'KeyP'},
'q': {'keyCode': 81, 'key': 'q', 'code': 'KeyQ'},
'r': {'keyCode': 82, 'key': 'r', 'code': 'KeyR'},
's': {'keyCode': 83, 'key': 's', 'code': 'KeyS'},
't': {'keyCode': 84, 'key': 't', 'code': 'KeyT'},
'u': {'keyCode': 85, 'key': 'u', 'code': 'KeyU'},
'v': {'keyCode': 86, 'key': 'v', 'code': 'KeyV'},
'w': {'keyCode': 87, 'key': 'w', 'code': 'KeyW'},
'x': {'keyCode': 88, 'key': 'x', 'code': 'KeyX'},
'y': {'keyCode': 89, 'key': 'y', 'code': 'KeyY'},
'z': {'keyCode': 90, 'key': 'z', 'code': 'KeyZ'},
'Meta': {'keyCode': 91, 'key': 'Meta', 'code': 'MetaLeft', 'location': 1},
'*': {'keyCode': 106, 'key': '*', 'code': 'NumpadMultiply', 'location': 3},
'+': {'keyCode': 107, 'key': '+', 'code': 'NumpadAdd', 'location': 3},
'-': {'keyCode': 109, 'key': '-', 'code': 'NumpadSubtract', 'location': 3},
'/': {'keyCode': 111, 'key': '/', 'code': 'NumpadDivide', 'location': 3},
';': {'keyCode': 186, 'key': ';', 'code': 'Semicolon'},
'=': {'keyCode': 187, 'key': '=', 'code': 'Equal'},
',': {'keyCode': 188, 'key': ',', 'code': 'Comma'},
'.': {'keyCode': 190, 'key': '.', 'code': 'Period'},
'`': {'keyCode': 192, 'key': '`', 'code': 'Backquote'},
'[': {'keyCode': 219, 'key': '[', 'code': 'BracketLeft'},
'\\': {'keyCode': 220, 'key': '\\', 'code': 'Backslash'},
']': {'keyCode': 221, 'key': ']', 'code': 'BracketRight'},
'\'': {'keyCode': 222, 'key': '\'', 'code': 'Quote'},
'Attn': {'keyCode': 246, 'key': 'Attn'},
'CrSel': {'keyCode': 247, 'key': 'CrSel', 'code': 'Props'},
'ExSel': {'keyCode': 248, 'key': 'ExSel'},
'EraseEof': {'keyCode': 249, 'key': 'EraseEof'},
'Play': {'keyCode': 250, 'key': 'Play'},
'ZoomOut': {'keyCode': 251, 'key': 'ZoomOut'},
')': {'keyCode': 48, 'key': ')', 'code': 'Digit0'},
'!': {'keyCode': 49, 'key': '!', 'code': 'Digit1'},
'@': {'keyCode': 50, 'key': '@', 'code': 'Digit2'},
'#': {'keyCode': 51, 'key': '#', 'code': 'Digit3'},
'$': {'keyCode': 52, 'key': '$', 'code': 'Digit4'},
'%': {'keyCode': 53, 'key': '%', 'code': 'Digit5'},
'^': {'keyCode': 54, 'key': '^', 'code': 'Digit6'},
'&': {'keyCode': 55, 'key': '&', 'code': 'Digit7'},
'(': {'keyCode': 57, 'key': '\(', 'code': 'Digit9'},
'A': {'keyCode': 65, 'key': 'A', 'code': 'KeyA'},
'B': {'keyCode': 66, 'key': 'B', 'code': 'KeyB'},
'C': {'keyCode': 67, 'key': 'C', 'code': 'KeyC'},
'D': {'keyCode': 68, 'key': 'D', 'code': 'KeyD'},
'E': {'keyCode': 69, 'key': 'E', 'code': 'KeyE'},
'F': {'keyCode': 70, 'key': 'F', 'code': 'KeyF'},
'G': {'keyCode': 71, 'key': 'G', 'code': 'KeyG'},
'H': {'keyCode': 72, 'key': 'H', 'code': 'KeyH'},
'I': {'keyCode': 73, 'key': 'I', 'code': 'KeyI'},
'J': {'keyCode': 74, 'key': 'J', 'code': 'KeyJ'},
'K': {'keyCode': 75, 'key': 'K', 'code': 'KeyK'},
'L': {'keyCode': 76, 'key': 'L', 'code': 'KeyL'},
'M': {'keyCode': 77, 'key': 'M', 'code': 'KeyM'},
'N': {'keyCode': 78, 'key': 'N', 'code': 'KeyN'},
'O': {'keyCode': 79, 'key': 'O', 'code': 'KeyO'},
'P': {'keyCode': 80, 'key': 'P', 'code': 'KeyP'},
'Q': {'keyCode': 81, 'key': 'Q', 'code': 'KeyQ'},
'R': {'keyCode': 82, 'key': 'R', 'code': 'KeyR'},
'S': {'keyCode': 83, 'key': 'S', 'code': 'KeyS'},
'T': {'keyCode': 84, 'key': 'T', 'code': 'KeyT'},
'U': {'keyCode': 85, 'key': 'U', 'code': 'KeyU'},
'V': {'keyCode': 86, 'key': 'V', 'code': 'KeyV'},
'W': {'keyCode': 87, 'key': 'W', 'code': 'KeyW'},
'X': {'keyCode': 88, 'key': 'X', 'code': 'KeyX'},
'Y': {'keyCode': 89, 'key': 'Y', 'code': 'KeyY'},
'Z': {'keyCode': 90, 'key': 'Z', 'code': 'KeyZ'},
':': {'keyCode': 186, 'key': ':', 'code': 'Semicolon'},
'<': {'keyCode': 188, 'key': '\<', 'code': 'Comma'},
'_': {'keyCode': 189, 'key': '_', 'code': 'Minus'},
'>': {'keyCode': 190, 'key': '>', 'code': 'Period'},
'?': {'keyCode': 191, 'key': '?', 'code': 'Slash'},
'~': {'keyCode': 192, 'key': '~', 'code': 'Backquote'},
'{': {'keyCode': 219, 'key': '{', 'code': 'BracketLeft'},
'|': {'keyCode': 220, 'key': '|', 'code': 'Backslash'},
'}': {'keyCode': 221, 'key': '}', 'code': 'BracketRight'},
'"': {'keyCode': 222, 'key': '"', 'code': 'Quote'}
};

View File

@ -0,0 +1,28 @@
import { Connection as RealConnection } from './Connection';
import { Target as RealTarget } from './Browser';
import * as child_process from 'child_process';
declare global {
module Puppeteer {
export interface ConnectionTransport {
send(string);
close();
onmessage?: (message: string) => void,
onclose?: () => void,
}
export interface ChildProcess extends child_process.ChildProcess { }
export type Viewport = {
width: number;
height: number;
deviceScaleFactor?: number;
isMobile?: boolean;
isLandscape?: boolean;
hasTouch?: boolean;
}
export class Connection extends RealConnection { }
export class Target extends RealTarget { }
}
}

View File

@ -0,0 +1,128 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {TimeoutError} = require('../Errors');
/**
* @internal
*/
class Helper {
/**
* @param {Function|string} fun
* @param {!Array<*>} args
* @return {string}
*/
static evaluationString(fun, ...args) {
if (Helper.isString(fun)) {
if (args.length !== 0)
throw new Error('Cannot evaluate a string with arguments');
return /** @type {string} */ (fun);
}
return `(${fun})(${args.map(serializeArgument).join(',')})`;
/**
* @param {*} arg
* @return {string}
*/
function serializeArgument(arg) {
if (Object.is(arg, undefined))
return 'undefined';
return JSON.stringify(arg);
}
}
static promisify(nodeFunction) {
function promisified(...args) {
return new Promise((resolve, reject) => {
function callback(err, ...result) {
if (err)
return reject(err);
if (result.length === 1)
return resolve(result[0]);
return resolve(result);
}
nodeFunction.call(null, ...args, callback);
});
}
return promisified;
}
/**
* @param {!Object} obj
* @return {boolean}
*/
static isNumber(obj) {
return typeof obj === 'number' || obj instanceof Number;
}
/**
* @param {!Object} obj
* @return {boolean}
*/
static isString(obj) {
return typeof obj === 'string' || obj instanceof String;
}
/**
* @param {!NodeJS.EventEmitter} emitter
* @param {(string|symbol)} eventName
* @param {function(?)} handler
* @return {{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}}
*/
static addEventListener(emitter, eventName, handler) {
emitter.on(eventName, handler);
return { emitter, eventName, handler };
}
/**
* @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}>} listeners
*/
static removeEventListeners(listeners) {
for (const listener of listeners)
listener.emitter.removeListener(listener.eventName, listener.handler);
listeners.splice(0, listeners.length);
}
/**
* @template T
* @param {!Promise<T>} promise
* @param {string} taskName
* @param {number} timeout
* @return {!Promise<T>}
*/
static async waitWithTimeout(promise, taskName, timeout) {
let reject;
const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`);
const timeoutPromise = new Promise((resolve, x) => reject = x);
const timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeoutTimer);
}
}
}
function assert(condition, errorText) {
if (!condition)
throw new Error(errorText);
}
module.exports = {
helper: Helper,
debugError: require('debug')(`puppeteer:error`),
assert,
};

View File

@ -0,0 +1,3 @@
// Any comment. You must start the file with a single-line comment!
pref("general.config.filename", "puppeteer.cfg");
pref("general.config.obscure_value", 0);

View File

@ -0,0 +1,199 @@
// Any comment. You must start the file with a comment!
// Make sure Shield doesn't hit the network.
// pref("app.normandy.api_url", "");
pref("app.normandy.enabled", false);
// Disable updater
pref("app.update.enabled", false);
// make absolutely sure it is really off
pref("app.update.auto", false);
pref("app.update.mode", 0);
pref("app.update.service.enabled", false);
// Dislabe newtabpage
pref("browser.startup.homepage", 'about:blank');
pref("browser.newtabpage.enabled", false);
// Disable topstories
pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
// Increase the APZ content response timeout in tests to 1 minute.
// This is to accommodate the fact that test environments tends to be
// slower than production environments (with the b2g emulator being
// the slowest of them all), resulting in the production timeout value
// sometimes being exceeded and causing false-positive test failures.
//
// (bug 1176798, bug 1177018, bug 1210465)
pref("apz.content_response_timeout", 60000);
// Indicate that the download panel has been shown once so that
// whichever download test runs first doesn't show the popup
// inconsistently.
pref("browser.download.panel.shown", true);
// Background thumbnails in particular cause grief, and disabling
// thumbnails in general cannot hurt
pref("browser.pagethumbnails.capturing_disabled", true);
// Disable safebrowsing components.
pref("browser.safebrowsing.blockedURIs.enabled", false);
pref("browser.safebrowsing.downloads.enabled", false);
pref("browser.safebrowsing.passwords.enabled", false);
pref("browser.safebrowsing.malware.enabled", false);
pref("browser.safebrowsing.phishing.enabled", false);
// Disable updates to search engines.
pref("browser.search.update", false);
// Do not restore the last open set of tabs if the browser has crashed
pref("browser.sessionstore.resume_from_crash", false);
// Don't check for the default web browser during startup.
pref("browser.shell.checkDefaultBrowser", false);
// Do not redirect user when a milstone upgrade of Firefox is detected
pref("browser.startup.homepage_override.mstone", "ignore");
// Disable browser animations (tabs, fullscreen, sliding alerts)
pref("toolkit.cosmeticAnimations.enabled", false);
// Do not close the window when the last tab gets closed
pref("browser.tabs.closeWindowWithLastTab", false);
// Do not allow background tabs to be zombified on Android, otherwise for
// tests that open additional tabs, the test harness tab itself might get
// unloaded
pref("browser.tabs.disableBackgroundZombification", false);
// Do not warn when closing all open tabs
pref("browser.tabs.warnOnClose", false);
// Do not warn when closing all other open tabs
pref("browser.tabs.warnOnCloseOtherTabs", false);
// Do not warn when multiple tabs will be opened
pref("browser.tabs.warnOnOpen", false);
// Disable first run splash page on Windows 10
pref("browser.usedOnWindows10.introURL", "");
// Disable the UI tour.
//
// Should be set in profile.
pref("browser.uitour.enabled", false);
// Turn off search suggestions in the location bar so as not to trigger
// network connections.
pref("browser.urlbar.suggest.searches", false);
// Do not warn on quitting Firefox
pref("browser.warnOnQuit", false);
// Do not show datareporting policy notifications which can
// interfere with tests
pref(
"datareporting.healthreport.documentServerURI",
"http://%(server)s/dummy/healthreport/",
);
pref("datareporting.healthreport.logging.consoleEnabled", false);
pref("datareporting.healthreport.service.enabled", false);
pref("datareporting.healthreport.service.firstRun", false);
pref("datareporting.healthreport.uploadEnabled", false);
pref("datareporting.policy.dataSubmissionEnabled", false);
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
// Automatically unload beforeunload alerts
pref("dom.disable_beforeunload", true);
// Disable popup-blocker
pref("dom.disable_open_during_load", false);
// Disable the ProcessHangMonitor
pref("dom.ipc.reportProcessHangs", false);
// Disable slow script dialogues
pref("dom.max_chrome_script_run_time", 0);
pref("dom.max_script_run_time", 0);
// Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
pref("extensions.autoDisableScopes", 0);
pref("extensions.enabledScopes", 5);
// Disable metadata caching for installed add-ons by default
pref("extensions.getAddons.cache.enabled", false);
// Disable installing any distribution extensions or add-ons.
pref("extensions.installDistroAddons", false);
// Turn off extension updates so they do not bother tests
pref("extensions.update.enabled", false);
pref("extensions.update.notifyUser", false);
// Make sure opening about:addons will not hit the network
pref(
"extensions.webservice.discoverURL",
"http://%(server)s/dummy/discoveryURL",
);
// Allow the application to have focus even it runs in the background
pref("focusmanager.testmode", true);
// Disable useragent updates
pref("general.useragent.updates.enabled", false);
// Always use network provider for geolocation tests so we bypass the
// macOS dialog raised by the corelocation provider
pref("geo.provider.testing", true);
// Do not scan Wifi
pref("geo.wifi.scan", false);
// Show chrome errors and warnings in the error console
pref("javascript.options.showInConsole", true);
// Do not prompt with long usernames or passwords in URLs
pref("network.http.phishy-userpass-length", 255);
// Do not prompt for temporary redirects
pref("network.http.prompt-temp-redirect", false);
// Disable speculative connections so they are not reported as leaking
// when they are hanging around
pref("network.http.speculative-parallel-limit", 0);
// Do not automatically switch between offline and online
pref("network.manage-offline-status", false);
// Make sure SNTP requests do not hit the network
pref("network.sntp.pools", "%(server)s");
// Local documents have access to all other local documents,
// including directory listings
pref("security.fileuri.strict_origin_policy", false);
// Tests do not wait for the notification button security delay
pref("security.notification_enable_delay", 0);
// Ensure blocklist updates do not hit the network
pref("services.settings.server", "http://%(server)s/dummy/blocklist/");
// Do not automatically fill sign-in forms with known usernames and
// passwords
pref("signon.autofillForms", false);
// Disable password capture, so that tests that include forms are not
// influenced by the presence of the persistent doorhanger notification
pref("signon.rememberSignons", false);
// Disable first-run welcome page
pref("startup.homepage_welcome_url", "about:blank");
pref("startup.homepage_welcome_url.additional", "");
// Prevent starting into safe mode after application crashes
pref("toolkit.startup.max_resumed_crashes", -1);
lockPref("toolkit.crashreporter.enabled", false);
// Disable crash reporter.
Components.classes["@mozilla.org/toolkit/crash-reporter;1"].getService(Components.interfaces.nsICrashReporter).submitReports = false;

View File

@ -0,0 +1,53 @@
{
"name": "puppeteer-firefox",
"version": "0.4.0",
"description": "Puppeteer API for Firefox",
"main": "index.js",
"repository": "github:GoogleChrome/puppeteer",
"engines": {
"node": ">=8.9.4"
},
"puppeteer": {
"firefox_revision": "e5fdeac984d4f966caafcdbc9b14da7a7f73fbed"
},
"scripts": {
"install": "node install.js",
"unit": "node test/test.js",
"funit": "cross-env DUMPIO=1 PRODUCT=firefox node test/test.js",
"cunit": "cross-env PRODUCT=chromium node test/test.js",
"tsc": "tsc -p ."
},
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0",
"extract-zip": "^1.6.6",
"https-proxy-agent": "^2.2.1",
"mime": "^2.0.3",
"progress": "^2.0.1",
"proxy-from-env": "^1.0.0",
"rimraf": "^2.6.1"
},
"devDependencies": {
"puppeteer": "^1.11.0",
"@pptr/testrunner": "^0.5.0",
"@pptr/testserver": "^0.5.0",
"@types/debug": "0.0.31",
"@types/extract-zip": "^1.6.2",
"@types/mime": "^2.0.0",
"@types/node": "^8.10.34",
"@types/rimraf": "^2.0.2",
"@types/ws": "^6.0.1",
"commonmark": "^0.28.1",
"cross-env": "^5.0.5",
"eslint": "^5.9.0",
"esprima": "^4.0.0",
"jpeg-js": "^0.3.4",
"minimist": "^1.2.0",
"ncp": "^2.0.0",
"pixelmatch": "^4.0.2",
"pngjs": "^3.3.3",
"text-diff": "^1.0.1",
"typescript": "3.1.6"
}
}

View File

@ -0,0 +1 @@
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Detect Touch Test</title>
<script src='modernizr.js'></script>
</head>
<body style="font-size:30vmin">
<script>
document.body.textContent = Modernizr.touchevents ? 'YES' : 'NO';
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

View File

@ -0,0 +1,15 @@
<script>
a();
function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error('Fancy error!');
}
</script>

View File

@ -0,0 +1,2 @@
import num from './es6module.js';
window.__es6injected = num;

View File

@ -0,0 +1 @@
export default 42;

View File

@ -0,0 +1,2 @@
import num from './es6/es6module.js';
window.__es6injected = num;

View File

@ -0,0 +1,8 @@
<link rel='stylesheet' href='./style.css'>
<script src='./script.js' type='text/javascript'></script>
<style>
div {
line-height: 18px;
}
</style>
<div>Hi, I'm frame</div>

View File

@ -0,0 +1,8 @@
<frameset>
<frameset>
<frame src='./frame.html'></frame>
<frame src='about:blank'></frame>
</frameset>
<frame src='/empty.html'></frame>
<frame></frame>
</frameset>

View File

@ -0,0 +1,25 @@
<style>
body {
display: flex;
}
body iframe {
flex-grow: 1;
flex-shrink: 1;
}
::-webkit-scrollbar{
display: none;
}
</style>
<script>
async function attachFrame(frameId, url) {
var frame = document.createElement('iframe');
frame.src = url;
frame.id = frameId;
document.body.appendChild(frame);
await new Promise(x => frame.onload = x);
return 'kazakh';
}
</script>
<iframe src='./two-frames.html' name='2frames'></iframe>
<iframe src='./frame.html' name='aframe'></iframe>

View File

@ -0,0 +1 @@
<iframe src='./frame.html'></iframe>

View File

@ -0,0 +1 @@
console.log('Cheers!');

View File

@ -0,0 +1,3 @@
div {
color: blue;
}

View File

@ -0,0 +1,13 @@
<style>
body {
display: flex;
flex-direction: column;
}
body iframe {
flex-grow: 1;
flex-shrink: 1;
}
</style>
<iframe src='./frame.html' name='uno'></iframe>
<iframe src='./frame.html' name='dos'></iframe>

View File

@ -0,0 +1,3 @@
<script>
var globalVar = 123;
</script>

View File

@ -0,0 +1,52 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
function generatePalette(amount) {
var result = [];
var hueStep = 360 / amount;
for (var i = 0; i < amount; ++i)
result.push('hsl(' + (hueStep * i) + ', 100%, 90%)');
return result;
}
var palette = generatePalette(100);
for (var i = 0; i < 200; ++i) {
var box = document.createElement('div');
box.classList.add('box');
box.style.setProperty('background-color', palette[i % palette.length]);
var x = i;
do {
var digit = x % 10;
x = (x / 10)|0;
var img = document.createElement('img');
img.src = `./digits/${digit}.png`;
box.insertBefore(img, box.firstChild);
} while (x);
document.body.appendChild(box);
}
});
</script>
<style>
body {
margin: 0;
padding: 0;
}
.box {
font-family: arial;
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
width: 50px;
height: 50px;
box-sizing: border-box;
border: 1px solid darkgray;
}
::-webkit-scrollbar {
display: none;
}
</style>

View File

@ -0,0 +1,2 @@
window.__injected = 42;
window.__injectedError = new Error('hi');

View File

@ -0,0 +1,3 @@
body {
background-color: red;
}

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Button test</title>
</head>
<body>
<script src="mouse-helper.js"></script>
<button onclick="clicked();">Click target</button>
<script>
window.result = 'Was not clicked';
function clicked() {
result = 'Clicked';
}
</script>
</body>
</html>

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<title>Selection Test</title>
</head>
<body>
<label for="agree">Remember Me</label>
<input id="agree" type="checkbox">
<script>
window.result = {
check: null,
events: [],
};
let checkbox = document.querySelector('input');
const events = [
'change',
'click',
'dblclick',
'input',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup',
];
for (let event of events) {
checkbox.addEventListener(event, () => {
if (['change', 'click', 'dblclick', 'input'].includes(event) === true) {
result.check = checkbox.checked;
}
result.events.push(event);
}, false);
}
</script>
</body>
</html>

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>Keyboard test</title>
</head>
<body>
<textarea></textarea>
<script>
window.result = "";
let textarea = document.querySelector('textarea');
textarea.focus();
textarea.addEventListener('keydown', event => {
log('Keydown:', event.key, event.code, event.which, modifiers(event));
});
textarea.addEventListener('keypress', event => {
log('Keypress:', event.key, event.code, event.which, event.charCode, modifiers(event));
});
textarea.addEventListener('keyup', event => {
log('Keyup:', event.key, event.code, event.which, modifiers(event));
});
function modifiers(event) {
let m = [];
if (event.altKey)
m.push('Alt')
if (event.ctrlKey)
m.push('Control');
if (event.metaKey)
m.push('Meta')
if (event.shiftKey)
m.push('Shift')
return '[' + m.join(' ') + ']';
}
function log(...args) {
console.log.apply(console, args);
result += args.join(' ') + '\n';
}
function getResult() {
let temp = result.trim();
result = "";
return temp;
}
</script>
</body>
</html>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Scrollable test</title>
</head>
<body>
<script src='mouse-helper.js'></script>
<script>
for (let i = 0; i < 100; i++) {
let button = document.createElement('button');
button.textContent = i + ': not clicked';
button.id = 'button-' + i;
button.onclick = () => button.textContent = 'clicked';
button.oncontextmenu = event => {
event.preventDefault();
button.textContent = 'context menu';
}
document.body.appendChild(button);
document.body.appendChild(document.createElement('br'));
}
</script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More