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.
@ -6,3 +6,4 @@ utils/testrunner/examples/
|
|||||||
node6/*
|
node6/*
|
||||||
node6-test/*
|
node6-test/*
|
||||||
node6-testrunner/*
|
node6-testrunner/*
|
||||||
|
experimental/
|
||||||
|
8
experimental/README.md
Normal 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
|
38
experimental/juggler/.cirrus.yml
Normal 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
@ -0,0 +1 @@
|
|||||||
|
firefox/
|
27
experimental/juggler/Dockerfile
Normal 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
|
1
experimental/juggler/FIREFOX_SHA
Normal file
@ -0,0 +1 @@
|
|||||||
|
663997bb1dd09a5d93135b1707feb59024eb9db4
|
77
experimental/juggler/README.md
Normal 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`
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
18
experimental/juggler/scripts/fetch_firefox.sh
Executable 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
|
9
experimental/juggler/scripts/install_gcloud.sh
Executable 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
|
13
experimental/juggler/scripts/upload_linux.sh
Executable 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)/
|
13
experimental/juggler/scripts/upload_mac.sh
Executable 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)/
|
149
experimental/juggler/src/BrowserHandler.jsm
Normal 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;
|
72
experimental/juggler/src/ChromeSession.js
Normal 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;
|
||||||
|
|
46
experimental/juggler/src/Helper.js
Normal 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;
|
||||||
|
|
78
experimental/juggler/src/InsecureSweepingOverride.js
Normal 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;
|
337
experimental/juggler/src/PageHandler.jsm
Normal 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;
|
371
experimental/juggler/src/Protocol.js
Normal 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'];
|
63
experimental/juggler/src/components/juggler.js
Normal 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]);
|
||||||
|
|
3
experimental/juggler/src/components/juggler.manifest
Normal 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
|
9
experimental/juggler/src/components/moz.build
Normal 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",
|
||||||
|
]
|
||||||
|
|
53
experimental/juggler/src/content/ContentSession.js
Normal 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;
|
||||||
|
|
287
experimental/juggler/src/content/FrameTree.js
Normal 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;
|
||||||
|
|
460
experimental/juggler/src/content/PageAgent.js
Normal 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;
|
||||||
|
|
275
experimental/juggler/src/content/RuntimeAgent.js
Normal 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;
|
58
experimental/juggler/src/content/ScrollbarManager.js
Normal 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;
|
||||||
|
|
47
experimental/juggler/src/content/floating-scrollbars.css
Normal 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;
|
||||||
|
}
|
13
experimental/juggler/src/content/hidden-scrollbars.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
27
experimental/juggler/src/content/main.js
Normal 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();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
25
experimental/juggler/src/jar.mn
Normal 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)
|
||||||
|
|
15
experimental/juggler/src/moz.build
Normal 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")
|
||||||
|
|
407
experimental/juggler/src/server/packets.js
Normal 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;
|
||||||
|
},
|
||||||
|
});
|
97
experimental/juggler/src/server/server.js
Normal 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;
|
247
experimental/juggler/src/server/stream-utils.js
Normal 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,
|
||||||
|
};
|
523
experimental/juggler/src/server/transport.js
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
17
experimental/puppeteer-firefox/.ci/node6/Dockerfile.linux
Normal 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
|
17
experimental/puppeteer-firefox/.ci/node8/Dockerfile.linux
Normal 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
|
11
experimental/puppeteer-firefox/.ci/node8/Dockerfile.windows
Normal 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'
|
31
experimental/puppeteer-firefox/.cirrus.yml
Normal 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
|
10
experimental/puppeteer-firefox/.gitignore
vendored
Normal 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
|
37
experimental/puppeteer-firefox/.npmignore
Normal 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
|
||||||
|
|
1
experimental/puppeteer-firefox/Errors.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./lib/Errors');
|
202
experimental/puppeteer-firefox/LICENSE
Normal 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.
|
187
experimental/puppeteer-firefox/README.md
Normal 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.
|
7
experimental/puppeteer-firefox/completeness.sh
Executable 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
|
||||||
|
|
27
experimental/puppeteer-firefox/examples/screenshot.js
Normal 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();
|
||||||
|
})();
|
55
experimental/puppeteer-firefox/examples/search.js
Normal 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();
|
||||||
|
})();
|
56
experimental/puppeteer-firefox/index.js
Normal 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();
|
153
experimental/puppeteer-firefox/install.js
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
29
experimental/puppeteer-firefox/lib/Errors.js
Normal 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,
|
||||||
|
};
|
20
experimental/puppeteer-firefox/lib/common.js
Normal 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};
|
171
experimental/puppeteer-firefox/lib/firefox/Browser.js
Normal 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};
|
342
experimental/puppeteer-firefox/lib/firefox/BrowserFetcher.js
Normal 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
|
||||||
|
*/
|
123
experimental/puppeteer-firefox/lib/firefox/Connection.js
Normal 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};
|
57
experimental/puppeteer-firefox/lib/firefox/Dialog.js
Normal 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};
|
126
experimental/puppeteer-firefox/lib/firefox/FirefoxTransport.js
Normal 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;
|
295
experimental/puppeteer-firefox/lib/firefox/Input.js
Normal 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 };
|
206
experimental/puppeteer-firefox/lib/firefox/Launcher.js
Normal 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};
|
1821
experimental/puppeteer-firefox/lib/firefox/Page.js
Normal file
281
experimental/puppeteer-firefox/lib/firefox/USKeyboardLayout.js
Normal 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'}
|
||||||
|
};
|
28
experimental/puppeteer-firefox/lib/firefox/externs.d.ts
vendored
Normal 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 { }
|
||||||
|
}
|
||||||
|
}
|
128
experimental/puppeteer-firefox/lib/firefox/helper.js
Normal 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,
|
||||||
|
};
|
@ -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);
|
199
experimental/puppeteer-firefox/misc/puppeteer.cfg
Normal 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;
|
53
experimental/puppeteer-firefox/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
1
experimental/puppeteer-firefox/test/assets/csp.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
|
12
experimental/puppeteer-firefox/test/assets/detect-touch.html
Normal 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>
|
BIN
experimental/puppeteer-firefox/test/assets/digits/0.png
Normal file
After Width: | Height: | Size: 434 B |
BIN
experimental/puppeteer-firefox/test/assets/digits/1.png
Normal file
After Width: | Height: | Size: 346 B |
BIN
experimental/puppeteer-firefox/test/assets/digits/2.png
Normal file
After Width: | Height: | Size: 413 B |
BIN
experimental/puppeteer-firefox/test/assets/digits/3.png
Normal file
After Width: | Height: | Size: 434 B |
BIN
experimental/puppeteer-firefox/test/assets/digits/4.png
Normal file
After Width: | Height: | Size: 403 B |
BIN
experimental/puppeteer-firefox/test/assets/digits/5.png
Normal file
After Width: | Height: | Size: 422 B |
BIN
experimental/puppeteer-firefox/test/assets/digits/6.png
Normal file
After Width: | Height: | Size: 445 B |
BIN
experimental/puppeteer-firefox/test/assets/digits/7.png
Normal file
After Width: | Height: | Size: 387 B |
BIN
experimental/puppeteer-firefox/test/assets/digits/8.png
Normal file
After Width: | Height: | Size: 447 B |
BIN
experimental/puppeteer-firefox/test/assets/digits/9.png
Normal file
After Width: | Height: | Size: 437 B |
15
experimental/puppeteer-firefox/test/assets/error.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
a();
|
||||||
|
|
||||||
|
function a() {
|
||||||
|
b();
|
||||||
|
}
|
||||||
|
|
||||||
|
function b() {
|
||||||
|
c();
|
||||||
|
}
|
||||||
|
|
||||||
|
function c() {
|
||||||
|
throw new Error('Fancy error!');
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,2 @@
|
|||||||
|
import num from './es6module.js';
|
||||||
|
window.__es6injected = num;
|
@ -0,0 +1 @@
|
|||||||
|
export default 42;
|
@ -0,0 +1,2 @@
|
|||||||
|
import num from './es6/es6module.js';
|
||||||
|
window.__es6injected = num;
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -0,0 +1 @@
|
|||||||
|
<iframe src='./frame.html'></iframe>
|
@ -0,0 +1 @@
|
|||||||
|
console.log('Cheers!');
|
@ -0,0 +1,3 @@
|
|||||||
|
div {
|
||||||
|
color: blue;
|
||||||
|
}
|
@ -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>
|
@ -0,0 +1,3 @@
|
|||||||
|
<script>
|
||||||
|
var globalVar = 123;
|
||||||
|
</script>
|
52
experimental/puppeteer-firefox/test/assets/grid.html
Normal 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>
|
@ -0,0 +1,2 @@
|
|||||||
|
window.__injected = 42;
|
||||||
|
window.__injectedError = new Error('hi');
|
@ -0,0 +1,3 @@
|
|||||||
|
body {
|
||||||
|
background-color: red;
|
||||||
|
}
|
16
experimental/puppeteer-firefox/test/assets/input/button.html
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|