mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-15 06:02:57 +03:00
39357c3f6c
Different GIT setups export different length of shortsha. With this, always export full SHA.
5092 lines
171 KiB
Diff
5092 lines
171 KiB
Diff
diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn
|
|
index 711ce5c668c08ecf324027c5392c6f1ebab8a5ec..7b65d87e3af89d884655a59b585002e48e0a275c 100644
|
|
--- a/browser/installer/allowed-dupes.mn
|
|
+++ b/browser/installer/allowed-dupes.mn
|
|
@@ -139,6 +139,11 @@ browser/chrome/browser/res/payments/formautofill/autofillEditForms.js
|
|
# Bug 1451050 - Remote settings empty dumps (will be populated with data eventually)
|
|
browser/defaults/settings/pinning/pins.json
|
|
browser/defaults/settings/main/example.json
|
|
+# 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
|
|
#ifdef MOZ_EME_WIN32_ARTIFACT
|
|
gmp-clearkey/0.1/manifest.json
|
|
i686/gmp-clearkey/0.1/manifest.json
|
|
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
|
|
index 92ae9f6052731a1b96b3b6f11f309267fc886283..a0ae741c78ae514f4721c619ea74ff2c6db447d6 100644
|
|
--- a/browser/installer/package-manifest.in
|
|
+++ b/browser/installer/package-manifest.in
|
|
@@ -208,6 +208,11 @@
|
|
@RESPATH@/components/marionette.js
|
|
#endif
|
|
|
|
+@RESPATH@/chrome/juggler@JAREXT@
|
|
+@RESPATH@/chrome/juggler.manifest
|
|
+@RESPATH@/components/juggler.manifest
|
|
+@RESPATH@/components/juggler.js
|
|
+
|
|
#if defined(ENABLE_TESTS) && defined(MOZ_DEBUG)
|
|
@RESPATH@/components/TestInterfaceJS.js
|
|
@RESPATH@/components/TestInterfaceJS.manifest
|
|
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
|
|
index cb9022a5f143405722f3bd51423625ec32aba04c..4402116154858278e20fd0a04f73e19354c34f99 100644
|
|
--- a/docshell/base/nsDocShell.cpp
|
|
+++ b/docshell/base/nsDocShell.cpp
|
|
@@ -97,6 +97,7 @@
|
|
#include "nsIDocShellTreeItem.h"
|
|
#include "nsIDocShellTreeOwner.h"
|
|
#include "mozilla/dom/Document.h"
|
|
+#include "mozilla/dom/Element.h"
|
|
#include "nsIDocumentLoaderFactory.h"
|
|
#include "nsIDOMWindow.h"
|
|
#include "nsIEditingSession.h"
|
|
@@ -360,6 +361,8 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext,
|
|
mUseStrictSecurityChecks(false),
|
|
mObserveErrorPages(true),
|
|
mCSSErrorReportingEnabled(false),
|
|
+ mFileInputInterceptionEnabled(false),
|
|
+ mBypassCSPEnabled(false),
|
|
mAllowAuth(mItemType == typeContent),
|
|
mAllowKeywordFixup(false),
|
|
mIsOffScreenBrowser(false),
|
|
@@ -1235,6 +1238,7 @@ bool nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest,
|
|
isSubFrame = mLSHE->GetIsSubFrame();
|
|
}
|
|
|
|
+ FireOnFrameLocationChange(this, aRequest, aURI, aLocationFlags);
|
|
if (!isSubFrame && !isRoot) {
|
|
/*
|
|
* We don't want to send OnLocationChange notifications when
|
|
@@ -3503,6 +3507,54 @@ nsDocShell::GetContentBlockingLog(Promise** aPromise) {
|
|
return NS_OK;
|
|
}
|
|
|
|
+nsDocShell* nsDocShell::GetRootDocShell() {
|
|
+ nsCOMPtr<nsIDocShellTreeItem> rootAsItem;
|
|
+ GetInProcessSameTypeRootTreeItem(getter_AddRefs(rootAsItem));
|
|
+ nsCOMPtr<nsIDocShell> rootShell = do_QueryInterface(rootAsItem);
|
|
+ return nsDocShell::Cast(rootShell);
|
|
+}
|
|
+
|
|
+NS_IMETHODIMP
|
|
+nsDocShell::GetBypassCSPEnabled(bool* aEnabled) {
|
|
+ MOZ_ASSERT(aEnabled);
|
|
+ *aEnabled = mBypassCSPEnabled;
|
|
+ return NS_OK;
|
|
+}
|
|
+
|
|
+NS_IMETHODIMP
|
|
+nsDocShell::SetBypassCSPEnabled(bool aEnabled) {
|
|
+ mBypassCSPEnabled = aEnabled;
|
|
+ return NS_OK;
|
|
+}
|
|
+
|
|
+bool nsDocShell::IsBypassCSPEnabled() {
|
|
+ return GetRootDocShell()->mBypassCSPEnabled;
|
|
+}
|
|
+
|
|
+NS_IMETHODIMP
|
|
+nsDocShell::GetFileInputInterceptionEnabled(bool* aEnabled) {
|
|
+ MOZ_ASSERT(aEnabled);
|
|
+ *aEnabled = mFileInputInterceptionEnabled;
|
|
+ return NS_OK;
|
|
+}
|
|
+
|
|
+NS_IMETHODIMP
|
|
+nsDocShell::SetFileInputInterceptionEnabled(bool aEnabled) {
|
|
+ mFileInputInterceptionEnabled = aEnabled;
|
|
+ return NS_OK;
|
|
+}
|
|
+
|
|
+bool nsDocShell::IsFileInputInterceptionEnabled() {
|
|
+ return GetRootDocShell()->mFileInputInterceptionEnabled;
|
|
+}
|
|
+
|
|
+void nsDocShell::FilePickerShown(mozilla::dom::Element* element) {
|
|
+ nsCOMPtr<nsIObserverService> observerService =
|
|
+ mozilla::services::GetObserverService();
|
|
+ observerService->NotifyObservers(
|
|
+ ToSupports(element), "juggler-file-picker-shown", nullptr);
|
|
+}
|
|
+
|
|
NS_IMETHODIMP
|
|
nsDocShell::GetIsNavigating(bool* aOut) {
|
|
*aOut = mIsNavigating;
|
|
diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h
|
|
index e69bc03ddaf2081df1bc84eb21bb2f0244d18bed..e154ecf5f5e4ecf5fd07cfab8812cb21c3af14d6 100644
|
|
--- a/docshell/base/nsDocShell.h
|
|
+++ b/docshell/base/nsDocShell.h
|
|
@@ -18,6 +18,7 @@
|
|
#include "mozilla/WeakPtr.h"
|
|
|
|
#include "mozilla/dom/BrowsingContext.h"
|
|
+#include "mozilla/dom/Element.h"
|
|
#include "mozilla/dom/ProfileTimelineMarkerBinding.h"
|
|
#include "mozilla/gfx/Matrix.h"
|
|
#include "mozilla/dom/ChildSHistory.h"
|
|
@@ -471,6 +472,11 @@ class nsDocShell final : public nsDocLoader,
|
|
mSkipBrowsingContextDetachOnDestroy = true;
|
|
}
|
|
|
|
+ bool IsFileInputInterceptionEnabled();
|
|
+ void FilePickerShown(mozilla::dom::Element* element);
|
|
+
|
|
+ bool IsBypassCSPEnabled();
|
|
+
|
|
// Create a content viewer within this nsDocShell for the given
|
|
// `WindowGlobalChild` actor.
|
|
nsresult CreateContentViewerForActor(
|
|
@@ -1026,6 +1032,8 @@ class nsDocShell final : public nsDocLoader,
|
|
|
|
bool CSSErrorReportingEnabled() const { return mCSSErrorReportingEnabled; }
|
|
|
|
+ nsDocShell* GetRootDocShell();
|
|
+
|
|
// Handles retrieval of subframe session history for nsDocShell::LoadURI. If a
|
|
// load is requested in a subframe of the current DocShell, the subframe
|
|
// loadType may need to reflect the loadType of the parent document, or in
|
|
@@ -1284,6 +1292,8 @@ class nsDocShell final : public nsDocLoader,
|
|
bool mUseStrictSecurityChecks : 1;
|
|
bool mObserveErrorPages : 1;
|
|
bool mCSSErrorReportingEnabled : 1;
|
|
+ bool mFileInputInterceptionEnabled: 1;
|
|
+ bool mBypassCSPEnabled : 1;
|
|
bool mAllowAuth : 1;
|
|
bool mAllowKeywordFixup : 1;
|
|
bool mIsOffScreenBrowser : 1;
|
|
diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl
|
|
index 40ca833abc529595f1b90121a1d153c94746b578..bfe4c9ae54e2399a81a632ab16f3ce7e40e9deb8 100644
|
|
--- a/docshell/base/nsIDocShell.idl
|
|
+++ b/docshell/base/nsIDocShell.idl
|
|
@@ -1153,4 +1153,9 @@ interface nsIDocShell : nsIDocShellTreeItem
|
|
* nsIWebNavigation.loadURI
|
|
*/
|
|
[infallible] readonly attribute boolean isNavigating;
|
|
+
|
|
+ attribute boolean fileInputInterceptionEnabled;
|
|
+
|
|
+ attribute boolean bypassCSPEnabled;
|
|
+
|
|
};
|
|
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
|
|
index 290fa4baac5b7e3dbfe0415d6755d838f1282afa..72c18a8b4f43a0300742cff0047ebbc17e6168e1 100644
|
|
--- a/dom/base/Document.cpp
|
|
+++ b/dom/base/Document.cpp
|
|
@@ -3078,6 +3078,9 @@ void Document::SendToConsole(nsCOMArray<nsISecurityConsoleMessage>& aMessages) {
|
|
}
|
|
|
|
void Document::ApplySettingsFromCSP(bool aSpeculative) {
|
|
+ if (mDocumentContainer && mDocumentContainer->IsBypassCSPEnabled())
|
|
+ return;
|
|
+
|
|
nsresult rv = NS_OK;
|
|
if (!aSpeculative) {
|
|
// 1) apply settings from regular CSP
|
|
@@ -3127,6 +3130,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) {
|
|
return NS_OK;
|
|
}
|
|
|
|
+ nsCOMPtr<nsIDocShell> shell(mDocumentContainer);
|
|
+ if (shell && nsDocShell::Cast(shell)->IsBypassCSPEnabled()) {
|
|
+ return NS_OK;
|
|
+ }
|
|
+
|
|
// If this is a data document - no need to set CSP.
|
|
if (mLoadedAsData) {
|
|
return NS_OK;
|
|
diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp
|
|
index c0483590b22a7ad763f008f4a4dccc86a9572c5d..a4d31b2b18e474d84239985988d68ec195852d44 100644
|
|
--- a/dom/html/HTMLInputElement.cpp
|
|
+++ b/dom/html/HTMLInputElement.cpp
|
|
@@ -45,6 +45,7 @@
|
|
#include "nsMappedAttributes.h"
|
|
#include "nsIFormControl.h"
|
|
#include "mozilla/dom/Document.h"
|
|
+#include "nsDocShell.h"
|
|
#include "nsIFormControlFrame.h"
|
|
#include "nsITextControlFrame.h"
|
|
#include "nsIFrame.h"
|
|
@@ -733,6 +734,12 @@ nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
+ nsDocShell* docShell = static_cast<nsDocShell*>(win->GetDocShell());
|
|
+ if (docShell && docShell->IsFileInputInterceptionEnabled()) {
|
|
+ docShell->FilePickerShown(this);
|
|
+ return NS_OK;
|
|
+ }
|
|
+
|
|
if (IsPopupBlocked()) {
|
|
return NS_OK;
|
|
}
|
|
diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp
|
|
index 2b236218126427a454b85bfbaabc3f0759e92130..a5dca3071fdbe677fcb7d3c540f3ebe90c4a1f9d 100644
|
|
--- a/dom/ipc/BrowserChild.cpp
|
|
+++ b/dom/ipc/BrowserChild.cpp
|
|
@@ -3650,6 +3650,13 @@ NS_IMETHODIMP BrowserChild::OnStateChange(nsIWebProgress* aWebProgress,
|
|
return NS_OK;
|
|
}
|
|
|
|
+NS_IMETHODIMP BrowserChild::OnFrameLocationChange(nsIWebProgress *aWebProgress,
|
|
+ nsIRequest *aRequest,
|
|
+ nsIURI *aLocation,
|
|
+ uint32_t aFlags) {
|
|
+ return NS_OK;
|
|
+}
|
|
+
|
|
NS_IMETHODIMP BrowserChild::OnProgressChange(nsIWebProgress* aWebProgress,
|
|
nsIRequest* aRequest,
|
|
int32_t aCurSelfProgress,
|
|
diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp
|
|
index fb7692aa03375598e917cf81fd6faa6934f7bf84..66805103f6ff6c089e8f674cde534ea8dece62df 100644
|
|
--- a/dom/security/nsCSPUtils.cpp
|
|
+++ b/dom/security/nsCSPUtils.cpp
|
|
@@ -122,6 +122,11 @@ void CSP_ApplyMetaCSPToDoc(mozilla::dom::Document& aDoc,
|
|
return;
|
|
}
|
|
|
|
+ if (aDoc.GetDocShell() &&
|
|
+ nsDocShell::Cast(aDoc.GetDocShell())->IsBypassCSPEnabled()) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
nsAutoString policyStr(
|
|
nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(
|
|
aPolicyStr));
|
|
diff --git a/extensions/permissions/nsPermissionManager.cpp b/extensions/permissions/nsPermissionManager.cpp
|
|
index 9f40b6123b2d3dcf2f57a34654baedb3b07b918b..45ea8152d5453c74dd60d4a8d5ad60c6b9c6c3d4 100644
|
|
--- a/extensions/permissions/nsPermissionManager.cpp
|
|
+++ b/extensions/permissions/nsPermissionManager.cpp
|
|
@@ -167,7 +167,7 @@ void MaybeStripOAs(OriginAttributes& aOriginAttributes) {
|
|
}
|
|
|
|
if (flags != 0) {
|
|
- aOriginAttributes.StripAttributes(flags);
|
|
+ // aOriginAttributes.StripAttributes(flags);
|
|
}
|
|
}
|
|
|
|
@@ -199,6 +199,8 @@ nsresult GetOriginFromPrincipal(nsIPrincipal* aPrincipal, nsACString& aOrigin) {
|
|
|
|
OriginAppendOASuffix(attrs, aOrigin);
|
|
|
|
+ // Disable userContext for permissions.
|
|
+ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
|
|
return NS_OK;
|
|
}
|
|
|
|
@@ -317,7 +319,7 @@ already_AddRefed<nsIPrincipal> GetNextSubDomainPrincipal(
|
|
|
|
if (!StaticPrefs::permissions_isolateBy_userContext()) {
|
|
// Disable userContext for permissions.
|
|
- attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
|
|
+ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
|
|
}
|
|
|
|
nsCOMPtr<nsIPrincipal> principal =
|
|
diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp
|
|
index 0d62c2cd2b672bf6c155a66aca8879db0dbb5f11..9a5d68c9daa85bd665a6c566fc3783300c60f447 100644
|
|
--- a/parser/html/nsHtml5TreeOpExecutor.cpp
|
|
+++ b/parser/html/nsHtml5TreeOpExecutor.cpp
|
|
@@ -1066,9 +1066,12 @@ void nsHtml5TreeOpExecutor::AddSpeculationCSP(const nsAString& aCSP) {
|
|
if (!StaticPrefs::security_csp_enable()) {
|
|
return;
|
|
}
|
|
-
|
|
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
|
|
|
|
+ if (mDocShell && static_cast<nsDocShell*>(mDocShell.get())->IsBypassCSPEnabled()) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
nsresult rv = NS_OK;
|
|
nsCOMPtr<nsIContentSecurityPolicy> preloadCsp = mDocument->GetPreloadCsp();
|
|
if (!preloadCsp) {
|
|
diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp
|
|
index 31737688948a1c70fc16c806b70b052a666fc0a6..255e5ae967b43668fedf3fdedde39e21f249c11c 100644
|
|
--- a/security/manager/ssl/nsCertOverrideService.cpp
|
|
+++ b/security/manager/ssl/nsCertOverrideService.cpp
|
|
@@ -611,7 +611,7 @@ nsCertOverrideService::IsCertUsedForOverrides(nsIX509Cert* aCert,
|
|
NS_IMETHODIMP
|
|
nsCertOverrideService::
|
|
SetDisableAllSecurityChecksAndLetAttackersInterceptMyData(bool aDisable) {
|
|
- if (!(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") ||
|
|
+ if (false /* juggler hacks */ && !(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") ||
|
|
PR_GetEnv("MOZ_MARIONETTE"))) {
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
diff --git a/testing/juggler/BrowserContextManager.js b/testing/juggler/BrowserContextManager.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..febd84e315529a60db5967a1f4efed553a46851d
|
|
--- /dev/null
|
|
+++ b/testing/juggler/BrowserContextManager.js
|
|
@@ -0,0 +1,173 @@
|
|
+"use strict";
|
|
+
|
|
+const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
|
+
|
|
+const IDENTITY_NAME = 'JUGGLER ';
|
|
+const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100;
|
|
+
|
|
+const ALL_PERMISSIONS = [
|
|
+ 'geo',
|
|
+ 'microphone',
|
|
+ 'camera',
|
|
+ 'desktop-notifications',
|
|
+];
|
|
+
|
|
+class BrowserContextManager {
|
|
+ static instance() {
|
|
+ return BrowserContextManager._instance || null;
|
|
+ }
|
|
+
|
|
+ static initialize() {
|
|
+ if (BrowserContextManager._instance)
|
|
+ return;
|
|
+ BrowserContextManager._instance = new BrowserContextManager();
|
|
+ }
|
|
+
|
|
+ constructor() {
|
|
+ this._id = 0;
|
|
+ this._browserContextIdToUserContextId = new Map();
|
|
+ this._userContextIdToBrowserContextId = new Map();
|
|
+ this._principalsForBrowserContextId = new Map();
|
|
+
|
|
+ // Cleanup containers from previous runs (if any)
|
|
+ for (const identity of ContextualIdentityService.getPublicIdentities()) {
|
|
+ if (identity.name && identity.name.startsWith(IDENTITY_NAME)) {
|
|
+ ContextualIdentityService.remove(identity.userContextId);
|
|
+ ContextualIdentityService.closeContainerTabs(identity.userContextId);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ grantPermissions(browserContextId, origin, permissions) {
|
|
+ const attrs = browserContextId ? {userContextId: this.userContextId(browserContextId)} : {};
|
|
+ const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(origin), attrs);
|
|
+ if (!this._principalsForBrowserContextId.has(browserContextId))
|
|
+ this._principalsForBrowserContextId.set(browserContextId, []);
|
|
+ this._principalsForBrowserContextId.get(browserContextId).push(principal);
|
|
+ for (const permission of ALL_PERMISSIONS) {
|
|
+ const action = permissions.includes(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION;
|
|
+ Services.perms.addFromPrincipal(principal, permission, action);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ resetPermissions(browserContextId) {
|
|
+ if (!this._principalsForBrowserContextId.has(browserContextId))
|
|
+ return;
|
|
+ const principals = this._principalsForBrowserContextId.get(browserContextId);
|
|
+ for (const principal of principals) {
|
|
+ for (const permission of ALL_PERMISSIONS)
|
|
+ Services.perms.removeFromPrincipal(principal, permission);
|
|
+ }
|
|
+ this._principalsForBrowserContextId.delete(browserContextId);
|
|
+ }
|
|
+
|
|
+ createBrowserContext() {
|
|
+ const browserContextId = (++this._id) + '';
|
|
+ const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId);
|
|
+ this._browserContextIdToUserContextId.set(browserContextId, identity.userContextId);
|
|
+ this._userContextIdToBrowserContextId.set(identity.userContextId, browserContextId);
|
|
+ return browserContextId;
|
|
+ }
|
|
+
|
|
+ browserContextId(userContextId) {
|
|
+ return this._userContextIdToBrowserContextId.get(userContextId);
|
|
+ }
|
|
+
|
|
+ userContextId(browserContextId) {
|
|
+ return this._browserContextIdToUserContextId.get(browserContextId);
|
|
+ }
|
|
+
|
|
+ removeBrowserContext(browserContextId) {
|
|
+ const userContextId = this._browserContextIdToUserContextId.get(browserContextId);
|
|
+ ContextualIdentityService.remove(userContextId);
|
|
+ ContextualIdentityService.closeContainerTabs(userContextId);
|
|
+ this._browserContextIdToUserContextId.delete(browserContextId);
|
|
+ this._userContextIdToBrowserContextId.delete(userContextId);
|
|
+ }
|
|
+
|
|
+ getBrowserContexts() {
|
|
+ return Array.from(this._browserContextIdToUserContextId.keys());
|
|
+ }
|
|
+
|
|
+ setCookies(browserContextId, cookies) {
|
|
+ const protocolToSameSite = {
|
|
+ [undefined]: Ci.nsICookie.SAMESITE_NONE,
|
|
+ 'Lax': Ci.nsICookie.SAMESITE_LAX,
|
|
+ 'Strict': Ci.nsICookie.SAMESITE_STRICT,
|
|
+ };
|
|
+ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : undefined;
|
|
+ for (const cookie of cookies) {
|
|
+ const uri = cookie.url ? NetUtil.newURI(cookie.url) : null;
|
|
+ let domain = cookie.domain;
|
|
+ if (!domain) {
|
|
+ if (!uri)
|
|
+ throw new Error('At least one of the url and domain needs to be specified');
|
|
+ domain = uri.host;
|
|
+ }
|
|
+ let path = cookie.path;
|
|
+ if (!path)
|
|
+ path = uri ? dirPath(uri.filePath) : '/';
|
|
+ let secure = false;
|
|
+ if (cookie.secure !== undefined)
|
|
+ secure = cookie.secure;
|
|
+ else if (uri && uri.scheme === 'https')
|
|
+ secure = true;
|
|
+ Services.cookies.add(
|
|
+ domain,
|
|
+ path,
|
|
+ cookie.name,
|
|
+ cookie.value,
|
|
+ secure,
|
|
+ cookie.httpOnly || false,
|
|
+ cookie.expires === undefined || cookie.expires === -1 /* isSession */,
|
|
+ cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires,
|
|
+ { userContextId } /* originAttributes */,
|
|
+ protocolToSameSite[cookie.sameSite],
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+
|
|
+ clearCookies(browserContextId) {
|
|
+ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : undefined;
|
|
+ Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId }));
|
|
+ }
|
|
+
|
|
+ getCookies(browserContextId) {
|
|
+ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : 0;
|
|
+ const result = [];
|
|
+ const sameSiteToProtocol = {
|
|
+ [Ci.nsICookie.SAMESITE_NONE]: 'None',
|
|
+ [Ci.nsICookie.SAMESITE_LAX]: 'Lax',
|
|
+ [Ci.nsICookie.SAMESITE_STRICT]: 'Strict',
|
|
+ };
|
|
+ for (let cookie of Services.cookies.enumerator) {
|
|
+ if (cookie.originAttributes.userContextId !== userContextId)
|
|
+ continue;
|
|
+ if (cookie.host === 'addons.mozilla.org')
|
|
+ continue;
|
|
+ result.push({
|
|
+ name: cookie.name,
|
|
+ value: cookie.value,
|
|
+ domain: cookie.host,
|
|
+ path: cookie.path,
|
|
+ expires: cookie.isSession ? -1 : cookie.expiry,
|
|
+ size: cookie.name.length + cookie.value.length,
|
|
+ httpOnly: cookie.isHttpOnly,
|
|
+ secure: cookie.isSecure,
|
|
+ session: cookie.isSession,
|
|
+ sameSite: sameSiteToProtocol[cookie.sameSite],
|
|
+ });
|
|
+ }
|
|
+ return result;
|
|
+ }
|
|
+}
|
|
+
|
|
+function dirPath(path) {
|
|
+ return path.substring(0, path.lastIndexOf('/') + 1);
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['BrowserContextManager'];
|
|
+this.BrowserContextManager = BrowserContextManager;
|
|
+
|
|
diff --git a/testing/juggler/Helper.js b/testing/juggler/Helper.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..673e93b0278a3502d94006696cea7e6e8e820deb
|
|
--- /dev/null
|
|
+++ b/testing/juggler/Helper.js
|
|
@@ -0,0 +1,101 @@
|
|
+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();
|
|
+ }
|
|
+
|
|
+ getNetworkErrorStatusText(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 = [ "Helper" ];
|
|
+this.Helper = Helper;
|
|
+
|
|
diff --git a/testing/juggler/NetworkObserver.js b/testing/juggler/NetworkObserver.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca30a73e723
|
|
--- /dev/null
|
|
+++ b/testing/juggler/NetworkObserver.js
|
|
@@ -0,0 +1,450 @@
|
|
+"use strict";
|
|
+
|
|
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
|
+const {CommonUtils} = ChromeUtils.import("resource://services-common/utils.js");
|
|
+
|
|
+
|
|
+const Cc = Components.classes;
|
|
+const Ci = Components.interfaces;
|
|
+const Cu = Components.utils;
|
|
+const Cr = Components.results;
|
|
+const Cm = Components.manager;
|
|
+const CC = Components.Constructor;
|
|
+const helper = new Helper();
|
|
+
|
|
+const BinaryInputStream = CC('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream');
|
|
+const BinaryOutputStream = CC('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream');
|
|
+const StorageStream = CC('@mozilla.org/storagestream;1', 'nsIStorageStream', 'init');
|
|
+
|
|
+// Cap response storage with 100Mb per tracked tab.
|
|
+const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024;
|
|
+
|
|
+/**
|
|
+ * This is a nsIChannelEventSink implementation that monitors channel redirects.
|
|
+ */
|
|
+const SINK_CLASS_DESCRIPTION = "Juggler NetworkMonitor Channel Event Sink";
|
|
+const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}");
|
|
+const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
|
|
+const SINK_CATEGORY_NAME = "net-channel-event-sinks";
|
|
+
|
|
+class NetworkObserver {
|
|
+ static instance() {
|
|
+ return NetworkObserver._instance || null;
|
|
+ }
|
|
+
|
|
+ static initialize() {
|
|
+ if (NetworkObserver._instance)
|
|
+ return;
|
|
+ NetworkObserver._instance = new NetworkObserver();
|
|
+ }
|
|
+
|
|
+ constructor() {
|
|
+ EventEmitter.decorate(this);
|
|
+ this._browserSessionCount = new Map();
|
|
+ this._activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor);
|
|
+ this._activityDistributor.addObserver(this);
|
|
+
|
|
+ this._redirectMap = new Map();
|
|
+ this._channelSink = {
|
|
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
|
|
+ asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => {
|
|
+ this._onRedirect(oldChannel, newChannel);
|
|
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
|
|
+ },
|
|
+ };
|
|
+ this._channelSinkFactory = {
|
|
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIFactory]),
|
|
+ createInstance: (aOuter, aIID) => this._channelSink.QueryInterface(aIID),
|
|
+ };
|
|
+ // Register self as ChannelEventSink to track redirects.
|
|
+ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
|
|
+ registrar.registerFactory(SINK_CLASS_ID, SINK_CLASS_DESCRIPTION, SINK_CONTRACT_ID, this._channelSinkFactory);
|
|
+ Services.catMan.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, SINK_CONTRACT_ID, false, true);
|
|
+
|
|
+ // Request interception state.
|
|
+ this._browserSuspendedChannels = new Map();
|
|
+ this._extraHTTPHeaders = new Map();
|
|
+ this._browserResponseStorages = new Map();
|
|
+
|
|
+ this._eventListeners = [
|
|
+ helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'),
|
|
+ helper.addObserver(this._onResponse.bind(this, false /* fromCache */), 'http-on-examine-response'),
|
|
+ helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-cached-response'),
|
|
+ helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-merged-response'),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ setExtraHTTPHeaders(browser, headers) {
|
|
+ if (!headers)
|
|
+ this._extraHTTPHeaders.delete(browser);
|
|
+ else
|
|
+ this._extraHTTPHeaders.set(browser, headers);
|
|
+ }
|
|
+
|
|
+ enableRequestInterception(browser) {
|
|
+ if (!this._browserSuspendedChannels.has(browser))
|
|
+ this._browserSuspendedChannels.set(browser, new Map());
|
|
+ }
|
|
+
|
|
+ disableRequestInterception(browser) {
|
|
+ const suspendedChannels = this._browserSuspendedChannels.get(browser);
|
|
+ if (!suspendedChannels)
|
|
+ return;
|
|
+ this._browserSuspendedChannels.delete(browser);
|
|
+ for (const channel of suspendedChannels.values())
|
|
+ channel.resume();
|
|
+ }
|
|
+
|
|
+ resumeSuspendedRequest(browser, requestId, headers) {
|
|
+ const suspendedChannels = this._browserSuspendedChannels.get(browser);
|
|
+ if (!suspendedChannels)
|
|
+ throw new Error(`Request interception is not enabled`);
|
|
+ const httpChannel = suspendedChannels.get(requestId);
|
|
+ if (!httpChannel)
|
|
+ throw new Error(`Cannot find request "${requestId}"`);
|
|
+ if (headers) {
|
|
+ // 1. Clear all previous headers.
|
|
+ for (const header of requestHeaders(httpChannel))
|
|
+ httpChannel.setRequestHeader(header.name, '', false /* merge */);
|
|
+ // 2. Set new headers.
|
|
+ for (const header of headers)
|
|
+ httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
|
|
+ }
|
|
+ suspendedChannels.delete(requestId);
|
|
+ httpChannel.resume();
|
|
+ }
|
|
+
|
|
+ getResponseBody(browser, requestId) {
|
|
+ const responseStorage = this._browserResponseStorages.get(browser);
|
|
+ if (!responseStorage)
|
|
+ throw new Error('Responses are not tracked for the given browser');
|
|
+ return responseStorage.getBase64EncodedResponse(requestId);
|
|
+ }
|
|
+
|
|
+ abortSuspendedRequest(browser, aRequestId) {
|
|
+ const suspendedChannels = this._browserSuspendedChannels.get(browser);
|
|
+ if (!suspendedChannels)
|
|
+ throw new Error(`Request interception is not enabled`);
|
|
+ const httpChannel = suspendedChannels.get(aRequestId);
|
|
+ if (!httpChannel)
|
|
+ throw new Error(`Cannot find request "${aRequestId}"`);
|
|
+ suspendedChannels.delete(aRequestId);
|
|
+ httpChannel.cancel(Cr.NS_ERROR_FAILURE);
|
|
+ httpChannel.resume();
|
|
+ this.emit('requestfailed', httpChannel, {
|
|
+ requestId: requestId(httpChannel),
|
|
+ errorCode: helper.getNetworkErrorStatusText(httpChannel.status),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onRedirect(oldChannel, newChannel) {
|
|
+ if (!(oldChannel instanceof Ci.nsIHttpChannel))
|
|
+ return;
|
|
+ const httpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ const loadContext = getLoadContext(httpChannel);
|
|
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement))
|
|
+ return;
|
|
+ this._redirectMap.set(newChannel, oldChannel);
|
|
+ }
|
|
+
|
|
+ observeActivity(channel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) {
|
|
+ if (activityType !== Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION)
|
|
+ return;
|
|
+ if (!(channel instanceof Ci.nsIHttpChannel))
|
|
+ return;
|
|
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ const loadContext = getLoadContext(httpChannel);
|
|
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement))
|
|
+ return;
|
|
+ if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
|
|
+ return;
|
|
+ this.emit('requestfinished', httpChannel, {
|
|
+ requestId: requestId(httpChannel),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onRequest(channel, topic) {
|
|
+ if (!(channel instanceof Ci.nsIHttpChannel))
|
|
+ return;
|
|
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ const loadContext = getLoadContext(httpChannel);
|
|
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement))
|
|
+ return;
|
|
+ const extraHeaders = this._extraHTTPHeaders.get(loadContext.topFrameElement);
|
|
+ if (extraHeaders) {
|
|
+ for (const header of extraHeaders)
|
|
+ httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
|
|
+ }
|
|
+ const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
|
|
+ const suspendedChannels = this._browserSuspendedChannels.get(loadContext.topFrameElement);
|
|
+ if (suspendedChannels) {
|
|
+ httpChannel.suspend();
|
|
+ suspendedChannels.set(requestId(httpChannel), httpChannel);
|
|
+ }
|
|
+ const oldChannel = this._redirectMap.get(httpChannel);
|
|
+ this._redirectMap.delete(httpChannel);
|
|
+
|
|
+ // Install response body hooks.
|
|
+ new ResponseBodyListener(this, loadContext.topFrameElement, httpChannel);
|
|
+
|
|
+ this.emit('request', httpChannel, {
|
|
+ url: httpChannel.URI.spec,
|
|
+ suspended: suspendedChannels ? true : undefined,
|
|
+ requestId: requestId(httpChannel),
|
|
+ redirectedFrom: oldChannel ? requestId(oldChannel) : undefined,
|
|
+ postData: readRequestPostData(httpChannel),
|
|
+ headers: requestHeaders(httpChannel),
|
|
+ method: httpChannel.requestMethod,
|
|
+ navigationId: httpChannel.isMainDocumentChannel ? requestId(httpChannel) : undefined,
|
|
+ cause: causeTypeToString(causeType),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onResponse(fromCache, httpChannel, topic) {
|
|
+ const loadContext = getLoadContext(httpChannel);
|
|
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement))
|
|
+ return;
|
|
+ httpChannel.QueryInterface(Ci.nsIHttpChannelInternal);
|
|
+ const headers = [];
|
|
+ httpChannel.visitResponseHeaders({
|
|
+ visitHeader: (name, value) => headers.push({name, value}),
|
|
+ });
|
|
+
|
|
+ let remoteIPAddress = undefined;
|
|
+ let remotePort = undefined;
|
|
+ try {
|
|
+ remoteIPAddress = httpChannel.remoteAddress;
|
|
+ remotePort = httpChannel.remotePort;
|
|
+ } catch (e) {
|
|
+ // remoteAddress is not defined for cached requests.
|
|
+ }
|
|
+ this.emit('response', httpChannel, {
|
|
+ requestId: requestId(httpChannel),
|
|
+ securityDetails: getSecurityDetails(httpChannel),
|
|
+ fromCache,
|
|
+ headers,
|
|
+ remoteIPAddress,
|
|
+ remotePort,
|
|
+ status: httpChannel.responseStatus,
|
|
+ statusText: httpChannel.responseStatusText,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onResponseFinished(browser, httpChannel, body) {
|
|
+ const responseStorage = this._browserResponseStorages.get(browser);
|
|
+ if (!responseStorage)
|
|
+ return;
|
|
+ responseStorage.addResponseBody(httpChannel, body);
|
|
+ this.emit('requestfinished', httpChannel, {
|
|
+ requestId: requestId(httpChannel),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ startTrackingBrowserNetwork(browser) {
|
|
+ const value = this._browserSessionCount.get(browser) || 0;
|
|
+ this._browserSessionCount.set(browser, value + 1);
|
|
+ if (value === 0)
|
|
+ this._browserResponseStorages.set(browser, new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10));
|
|
+ return () => this.stopTrackingBrowserNetwork(browser);
|
|
+ }
|
|
+
|
|
+ stopTrackingBrowserNetwork(browser) {
|
|
+ const value = this._browserSessionCount.get(browser);
|
|
+ if (value) {
|
|
+ this._browserSessionCount.set(browser, value - 1);
|
|
+ } else {
|
|
+ this._browserSessionCount.delete(browser);
|
|
+ this._browserResponseStorages.delete(browser);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._activityDistributor.removeObserver(this);
|
|
+ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
|
|
+ registrar.unregisterFactory(SINK_CLASS_ID, this._channelSinkFactory);
|
|
+ Services.catMan.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, false);
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+}
|
|
+
|
|
+const protocolVersionNames = {
|
|
+ [Ci.nsITransportSecurityInfo.TLS_VERSION_1]: 'TLS 1',
|
|
+ [Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: 'TLS 1.1',
|
|
+ [Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: 'TLS 1.2',
|
|
+ [Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: 'TLS 1.3',
|
|
+};
|
|
+
|
|
+function getSecurityDetails(httpChannel) {
|
|
+ const securityInfo = httpChannel.securityInfo;
|
|
+ if (!securityInfo)
|
|
+ return null;
|
|
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
|
|
+ if (!securityInfo.serverCert)
|
|
+ return null;
|
|
+ return {
|
|
+ protocol: protocolVersionNames[securityInfo.protocolVersion] || '<unknown>',
|
|
+ subjectName: securityInfo.serverCert.commonName,
|
|
+ issuer: securityInfo.serverCert.issuerCommonName,
|
|
+ // Convert to seconds.
|
|
+ validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000,
|
|
+ validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000,
|
|
+ };
|
|
+}
|
|
+
|
|
+function readRequestPostData(httpChannel) {
|
|
+ if (!(httpChannel instanceof Ci.nsIUploadChannel))
|
|
+ return undefined;
|
|
+ const iStream = httpChannel.uploadStream;
|
|
+ if (!iStream)
|
|
+ return undefined;
|
|
+ const isSeekableStream = iStream instanceof Ci.nsISeekableStream;
|
|
+
|
|
+ let prevOffset;
|
|
+ if (isSeekableStream) {
|
|
+ prevOffset = iStream.tell();
|
|
+ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
|
+ }
|
|
+
|
|
+ // Read data from the stream.
|
|
+ let text = undefined;
|
|
+ try {
|
|
+ text = NetUtil.readInputStreamToString(iStream, iStream.available());
|
|
+ const converter = Cc['@mozilla.org/intl/scriptableunicodeconverter']
|
|
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
|
|
+ converter.charset = 'UTF-8';
|
|
+ text = converter.ConvertToUnicode(text);
|
|
+ } catch (err) {
|
|
+ text = undefined;
|
|
+ }
|
|
+
|
|
+ // Seek locks the file, so seek to the beginning only if necko hasn't
|
|
+ // read it yet, since necko doesn't seek to 0 before reading (at lest
|
|
+ // not till 459384 is fixed).
|
|
+ if (isSeekableStream && prevOffset == 0)
|
|
+ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
|
+ return text;
|
|
+}
|
|
+
|
|
+function getLoadContext(httpChannel) {
|
|
+ let loadContext = null;
|
|
+ try {
|
|
+ if (httpChannel.notificationCallbacks)
|
|
+ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
|
|
+ } catch (e) {}
|
|
+ try {
|
|
+ if (!loadContext && httpChannel.loadGroup)
|
|
+ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
|
|
+ } catch (e) { }
|
|
+ return loadContext;
|
|
+}
|
|
+
|
|
+function requestId(httpChannel) {
|
|
+ return httpChannel.channelId + '';
|
|
+}
|
|
+
|
|
+function requestHeaders(httpChannel) {
|
|
+ const headers = [];
|
|
+ httpChannel.visitRequestHeaders({
|
|
+ visitHeader: (name, value) => headers.push({name, value}),
|
|
+ });
|
|
+ return headers;
|
|
+}
|
|
+
|
|
+function causeTypeToString(causeType) {
|
|
+ for (let key in Ci.nsIContentPolicy) {
|
|
+ if (Ci.nsIContentPolicy[key] === causeType)
|
|
+ return key;
|
|
+ }
|
|
+ return 'TYPE_OTHER';
|
|
+}
|
|
+
|
|
+class ResponseStorage {
|
|
+ constructor(maxTotalSize, maxResponseSize) {
|
|
+ this._totalSize = 0;
|
|
+ this._maxResponseSize = maxResponseSize;
|
|
+ this._maxTotalSize = maxTotalSize;
|
|
+ this._responses = new Map();
|
|
+ }
|
|
+
|
|
+ addResponseBody(httpChannel, body) {
|
|
+ if (body.length > this._maxResponseSize) {
|
|
+ this._responses.set(requestId, {
|
|
+ evicted: true,
|
|
+ body: '',
|
|
+ });
|
|
+ return;
|
|
+ }
|
|
+ let encodings = [];
|
|
+ if ((httpChannel instanceof Ci.nsIEncodedChannel) && httpChannel.contentEncodings && !httpChannel.applyConversion) {
|
|
+ const encodingHeader = httpChannel.getResponseHeader("Content-Encoding");
|
|
+ encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
|
|
+ }
|
|
+ this._responses.set(requestId(httpChannel), {body, encodings});
|
|
+ this._totalSize += body.length;
|
|
+ if (this._totalSize > this._maxTotalSize) {
|
|
+ for (let [requestId, response] of this._responses) {
|
|
+ this._totalSize -= response.body.length;
|
|
+ response.body = '';
|
|
+ response.evicted = true;
|
|
+ if (this._totalSize < this._maxTotalSize)
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ getBase64EncodedResponse(requestId) {
|
|
+ const response = this._responses.get(requestId);
|
|
+ if (!response)
|
|
+ throw new Error(`Request "${requestId}" is not found`);
|
|
+ if (response.evicted)
|
|
+ return {base64body: '', evicted: true};
|
|
+ let result = response.body;
|
|
+ if (response.encodings && response.encodings.length) {
|
|
+ for (const encoding of response.encodings)
|
|
+ result = CommonUtils.convertString(result, encoding, 'uncompressed');
|
|
+ }
|
|
+ return {base64body: btoa(result)};
|
|
+ }
|
|
+}
|
|
+
|
|
+class ResponseBodyListener {
|
|
+ constructor(networkObserver, browser, httpChannel) {
|
|
+ this._networkObserver = networkObserver;
|
|
+ this._browser = browser;
|
|
+ this._httpChannel = httpChannel;
|
|
+ this._chunks = [];
|
|
+ this.QueryInterface = ChromeUtils.generateQI([Ci.nsIStreamListener]);
|
|
+ httpChannel.QueryInterface(Ci.nsITraceableChannel);
|
|
+ this.originalListener = httpChannel.setNewListener(this);
|
|
+ }
|
|
+
|
|
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
|
|
+ const iStream = new BinaryInputStream(aInputStream);
|
|
+ const sStream = new StorageStream(8192, aCount, null);
|
|
+ const oStream = new BinaryOutputStream(sStream.getOutputStream(0));
|
|
+
|
|
+ // Copy received data as they come.
|
|
+ const data = iStream.readBytes(aCount);
|
|
+ this._chunks.push(data);
|
|
+
|
|
+ oStream.writeBytes(data, aCount);
|
|
+ this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount);
|
|
+ }
|
|
+
|
|
+ onStartRequest(aRequest) {
|
|
+ this.originalListener.onStartRequest(aRequest);
|
|
+ }
|
|
+
|
|
+ onStopRequest(aRequest, aStatusCode) {
|
|
+ this.originalListener.onStopRequest(aRequest, aStatusCode);
|
|
+ const body = this._chunks.join('');
|
|
+ delete this._chunks;
|
|
+ this._networkObserver._onResponseFinished(this._browser, this._httpChannel, body);
|
|
+ }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['NetworkObserver'];
|
|
+this.NetworkObserver = NetworkObserver;
|
|
diff --git a/testing/juggler/TargetRegistry.js b/testing/juggler/TargetRegistry.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..da5e4ee371d03bd0c6524cef694b12b735f57350
|
|
--- /dev/null
|
|
+++ b/testing/juggler/TargetRegistry.js
|
|
@@ -0,0 +1,187 @@
|
|
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+
|
|
+const Cc = Components.classes;
|
|
+const Ci = Components.interfaces;
|
|
+const Cu = Components.utils;
|
|
+
|
|
+const helper = new Helper();
|
|
+
|
|
+class TargetRegistry {
|
|
+ static instance() {
|
|
+ return TargetRegistry._instance || null;
|
|
+ }
|
|
+
|
|
+ static initialize(mainWindow, contextManager) {
|
|
+ if (TargetRegistry._instance)
|
|
+ return;
|
|
+ TargetRegistry._instance = new TargetRegistry(mainWindow, contextManager);
|
|
+ }
|
|
+
|
|
+ constructor(mainWindow, contextManager) {
|
|
+ EventEmitter.decorate(this);
|
|
+
|
|
+ this._mainWindow = mainWindow;
|
|
+ this._contextManager = contextManager;
|
|
+ this._targets = new Map();
|
|
+
|
|
+ this._browserTarget = new BrowserTarget();
|
|
+ this._targets.set(this._browserTarget.id(), this._browserTarget);
|
|
+ this._tabToTarget = new Map();
|
|
+
|
|
+ for (const tab of this._mainWindow.gBrowser.tabs)
|
|
+ this._ensureTargetForTab(tab);
|
|
+ this._mainWindow.gBrowser.tabContainer.addEventListener('TabOpen', event => {
|
|
+ this._ensureTargetForTab(event.target);
|
|
+ });
|
|
+ this._mainWindow.gBrowser.tabContainer.addEventListener('TabClose', event => {
|
|
+ const tab = event.target;
|
|
+ const target = this._tabToTarget.get(tab);
|
|
+ if (!target)
|
|
+ return;
|
|
+ this._targets.delete(target.id());
|
|
+ this._tabToTarget.delete(tab);
|
|
+ target.dispose();
|
|
+ this.emit(TargetRegistry.Events.TargetDestroyed, target.info());
|
|
+ });
|
|
+ }
|
|
+
|
|
+ async newPage({browserContextId}) {
|
|
+ const tab = this._mainWindow.gBrowser.addTab('about:blank', {
|
|
+ userContextId: this._contextManager.userContextId(browserContextId),
|
|
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
+ });
|
|
+ this._mainWindow.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 target = this._ensureTargetForTab(tab);
|
|
+ return target.id();
|
|
+ }
|
|
+
|
|
+ async closePage(targetId, runBeforeUnload = false) {
|
|
+ const tab = this.tabForTarget(targetId);
|
|
+ await this._mainWindow.gBrowser.removeTab(tab, {
|
|
+ skipPermitUnload: !runBeforeUnload,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ targetInfos() {
|
|
+ return Array.from(this._targets.values()).map(target => target.info());
|
|
+ }
|
|
+
|
|
+ targetInfo(targetId) {
|
|
+ const target = this._targets.get(targetId);
|
|
+ return target ? target.info() : null;
|
|
+ }
|
|
+
|
|
+ browserTargetInfo() {
|
|
+ return this._browserTarget.info();
|
|
+ }
|
|
+
|
|
+ tabForTarget(targetId) {
|
|
+ const target = this._targets.get(targetId);
|
|
+ if (!target)
|
|
+ throw new Error(`Target "${targetId}" does not exist!`);
|
|
+ if (!(target instanceof PageTarget))
|
|
+ throw new Error(`Target "${targetId}" is not a page!`);
|
|
+ return target._tab;
|
|
+ }
|
|
+
|
|
+ _ensureTargetForTab(tab) {
|
|
+ if (this._tabToTarget.has(tab))
|
|
+ return this._tabToTarget.get(tab);
|
|
+ const openerTarget = tab.openerTab ? this._ensureTargetForTab(tab.openerTab) : null;
|
|
+ const target = new PageTarget(this, tab, this._contextManager.browserContextId(tab.userContextId), openerTarget);
|
|
+
|
|
+ this._targets.set(target.id(), target);
|
|
+ this._tabToTarget.set(tab, target);
|
|
+ this.emit(TargetRegistry.Events.TargetCreated, target.info());
|
|
+ }
|
|
+}
|
|
+
|
|
+let lastTabTargetId = 0;
|
|
+
|
|
+class PageTarget {
|
|
+ constructor(registry, tab, browserContextId, opener) {
|
|
+ this._targetId = 'target-page-' + (++lastTabTargetId);
|
|
+ this._registry = registry;
|
|
+ this._tab = tab;
|
|
+ this._browserContextId = browserContextId;
|
|
+ this._openerId = opener ? opener.id() : undefined;
|
|
+ this._url = tab.linkedBrowser.currentURI.spec;
|
|
+
|
|
+ // First navigation always happens to about:blank - do not report it.
|
|
+ this._skipNextNavigation = true;
|
|
+
|
|
+ const navigationListener = {
|
|
+ QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]),
|
|
+ onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation),
|
|
+ };
|
|
+ this._eventListeners = [
|
|
+ helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ id() {
|
|
+ return this._targetId;
|
|
+ }
|
|
+
|
|
+ info() {
|
|
+ return {
|
|
+ targetId: this.id(),
|
|
+ type: 'page',
|
|
+ url: this._url,
|
|
+ browserContextId: this._browserContextId,
|
|
+ openerId: this._openerId,
|
|
+ };
|
|
+ }
|
|
+
|
|
+ _onNavigated(aLocation) {
|
|
+ if (this._skipNextNavigation) {
|
|
+ this._skipNextNavigation = false;
|
|
+ return;
|
|
+ }
|
|
+ this._url = aLocation.spec;
|
|
+ this._registry.emit(TargetRegistry.Events.TargetChanged, this.info());
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+}
|
|
+
|
|
+class BrowserTarget {
|
|
+ id() {
|
|
+ return 'target-browser';
|
|
+ }
|
|
+
|
|
+ info() {
|
|
+ return {
|
|
+ targetId: this.id(),
|
|
+ type: 'browser',
|
|
+ url: '',
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+TargetRegistry.Events = {
|
|
+ TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'),
|
|
+ TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'),
|
|
+ TargetChanged: Symbol('TargetRegistry.Events.TargetChanged'),
|
|
+};
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['TargetRegistry'];
|
|
+this.TargetRegistry = TargetRegistry;
|
|
diff --git a/testing/juggler/components/juggler.js b/testing/juggler/components/juggler.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..9654aeeb257d2741e728c45c1a81d9c3d2c654af
|
|
--- /dev/null
|
|
+++ b/testing/juggler/components/juggler.js
|
|
@@ -0,0 +1,112 @@
|
|
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js");
|
|
+const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js");
|
|
+const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js");
|
|
+const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.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 = -1;
|
|
+};
|
|
+
|
|
+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: async function(subject, topic) {
|
|
+ Services.obs.removeObserver(this, 'sessionstore-windows-restored');
|
|
+
|
|
+ const win = await waitForBrowserWindow();
|
|
+ BrowserContextManager.initialize();
|
|
+ NetworkObserver.initialize();
|
|
+ TargetRegistry.initialize(win, BrowserContextManager.instance());
|
|
+
|
|
+ const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
|
|
+ const WebSocketServer = require('devtools/server/socket/websocket-server');
|
|
+ this._server = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket);
|
|
+ this._server.initSpecialConnection(this._port, Ci.nsIServerSocket.KeepWhenOffline | Ci.nsIServerSocket.LoopbackOnly, 4);
|
|
+ this._server.asyncListen({
|
|
+ onSocketAccepted: async(socket, transport) => {
|
|
+ const input = transport.openInputStream(0, 0, 0);
|
|
+ const output = transport.openOutputStream(0, 0, 0);
|
|
+ const webSocket = await WebSocketServer.accept(transport, input, output);
|
|
+ new Dispatcher(webSocket);
|
|
+ }
|
|
+ });
|
|
+
|
|
+ Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */);
|
|
+ dump(`Juggler listening on ws://127.0.0.1:${this._server.port}\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]);
|
|
+
|
|
+/**
|
|
+ * @return {!Promise<Ci.nsIDOMChromeWindow>}
|
|
+ */
|
|
+async function waitForBrowserWindow() {
|
|
+ const windowsIt = Services.wm.getEnumerator('navigator:browser');
|
|
+ if (windowsIt.hasMoreElements())
|
|
+ return waitForWindowLoaded(windowsIt.getNext());
|
|
+
|
|
+ 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);
|
|
+ });
|
|
+ });
|
|
+ }
|
|
+}
|
|
diff --git a/testing/juggler/components/juggler.manifest b/testing/juggler/components/juggler.manifest
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..50f8930207563e0d6b8a7878fc602dbca54d77fc
|
|
--- /dev/null
|
|
+++ b/testing/juggler/components/juggler.manifest
|
|
@@ -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
|
|
diff --git a/testing/juggler/components/moz.build b/testing/juggler/components/moz.build
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..268fbc361d8053182bb6c27f626e853dd7aeb254
|
|
--- /dev/null
|
|
+++ b/testing/juggler/components/moz.build
|
|
@@ -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",
|
|
+]
|
|
+
|
|
diff --git a/testing/juggler/content/ContentSession.js b/testing/juggler/content/ContentSession.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..f68780d529e753e7456c3182b051ad790dcd0e16
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/ContentSession.js
|
|
@@ -0,0 +1,63 @@
|
|
+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
|
|
+ * @param {!ScrollbarManager} scrollbarManager
|
|
+ * @param {!NetworkMonitor} networkMonitor
|
|
+ */
|
|
+ constructor(sessionId, messageManager, frameTree, scrollbarManager, networkMonitor) {
|
|
+ this._sessionId = sessionId;
|
|
+ this._messageManager = messageManager;
|
|
+ const runtimeAgent = new RuntimeAgent(this);
|
|
+ const pageAgent = new PageAgent(this, runtimeAgent, frameTree, scrollbarManager, networkMonitor);
|
|
+ this._agents = {
|
|
+ Page: pageAgent,
|
|
+ Runtime: runtimeAgent,
|
|
+ };
|
|
+ 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 [domainName, methodName] = msg.data.methodName.split('.');
|
|
+ const agent = this._agents[domainName];
|
|
+ if (!agent)
|
|
+ throw new Error(`unknown domain: ${domainName}`);
|
|
+ const handler = agent[methodName];
|
|
+ if (!handler)
|
|
+ throw new Error(`unknown method: ${domainName}.${methodName}`);
|
|
+ const result = await handler.call(agent, 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);
|
|
+ for (const agent of Object.values(this._agents))
|
|
+ agent.dispose();
|
|
+ }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['ContentSession'];
|
|
+this.ContentSession = ContentSession;
|
|
+
|
|
diff --git a/testing/juggler/content/FrameTree.js b/testing/juggler/content/FrameTree.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..640782151e30b2a34b3c0c57e88a4053b5382a88
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/FrameTree.js
|
|
@@ -0,0 +1,240 @@
|
|
+"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 = this._channelId(channel);
|
|
+ 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, helper.getNetworkErrorStatusText(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);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ _channelId(channel) {
|
|
+ if (channel instanceof Ci.nsIHttpChannel) {
|
|
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ return String(httpChannel.channelId);
|
|
+ }
|
|
+ return helper.generateId();
|
|
+ }
|
|
+
|
|
+ _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;
|
|
+ }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['FrameTree'];
|
|
+this.FrameTree = FrameTree;
|
|
+
|
|
diff --git a/testing/juggler/content/NetworkMonitor.js b/testing/juggler/content/NetworkMonitor.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..2508cce41565023b7fee9c7b85afe8ecebd26e7d
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/NetworkMonitor.js
|
|
@@ -0,0 +1,62 @@
|
|
+"use strict";
|
|
+const Ci = Components.interfaces;
|
|
+const Cr = Components.results;
|
|
+const Cu = Components.utils;
|
|
+
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+
|
|
+const helper = new Helper();
|
|
+
|
|
+class NetworkMonitor {
|
|
+ constructor(rootDocShell, frameTree) {
|
|
+ this._frameTree = frameTree;
|
|
+ this._requestDetails = new Map();
|
|
+
|
|
+ this._eventListeners = [
|
|
+ helper.addObserver(this._onRequest.bind(this), 'http-on-opening-request'),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ _onRequest(channel) {
|
|
+ if (!(channel instanceof Ci.nsIHttpChannel))
|
|
+ return;
|
|
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ const loadContext = getLoadContext(httpChannel);
|
|
+ if (!loadContext)
|
|
+ return;
|
|
+ const window = loadContext.associatedWindow;
|
|
+ const frame = this._frameTree.frameForDocShell(window.docShell)
|
|
+ if (!frame)
|
|
+ return;
|
|
+ this._requestDetails.set(httpChannel.channelId, {
|
|
+ frameId: frame.id(),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ requestDetails(channelId) {
|
|
+ return this._requestDetails.get(channelId) || null;
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._requestDetails.clear();
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+}
|
|
+
|
|
+function getLoadContext(httpChannel) {
|
|
+ let loadContext = null;
|
|
+ try {
|
|
+ if (httpChannel.notificationCallbacks)
|
|
+ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
|
|
+ } catch (e) {}
|
|
+ try {
|
|
+ if (!loadContext && httpChannel.loadGroup)
|
|
+ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
|
|
+ } catch (e) { }
|
|
+ return loadContext;
|
|
+}
|
|
+
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['NetworkMonitor'];
|
|
+this.NetworkMonitor = NetworkMonitor;
|
|
+
|
|
diff --git a/testing/juggler/content/PageAgent.js b/testing/juggler/content/PageAgent.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..266fad046ba2fcc950b250fed49be61d10ee6776
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/PageAgent.js
|
|
@@ -0,0 +1,661 @@
|
|
+"use strict";
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+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, networkMonitor) {
|
|
+ this._session = session;
|
|
+ this._runtime = runtimeAgent;
|
|
+ this._frameTree = frameTree;
|
|
+ this._networkMonitor = networkMonitor;
|
|
+ this._scrollbarManager = scrollbarManager;
|
|
+
|
|
+ this._frameToExecutionContext = new Map();
|
|
+ this._scriptsToEvaluateOnNewDocument = new Map();
|
|
+ this._bindingsToAdd = new Set();
|
|
+
|
|
+ this._eventListeners = [];
|
|
+ this._enabled = false;
|
|
+
|
|
+ const docShell = frameTree.mainFrame().docShell();
|
|
+ this._docShell = 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();
|
|
+ }
|
|
+ });
|
|
+ });
|
|
+ }
|
|
+
|
|
+ requestDetails({channelId}) {
|
|
+ return this._networkMonitor.requestDetails(channelId);
|
|
+ }
|
|
+
|
|
+ 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);
|
|
+ }
|
|
+
|
|
+ async setEmulatedMedia({type, colorScheme}) {
|
|
+ const docShell = this._frameTree.mainFrame().docShell();
|
|
+ const cv = docShell.contentViewer;
|
|
+ if (type === '')
|
|
+ cv.stopEmulatingMedium();
|
|
+ else if (type)
|
|
+ cv.emulateMedium(type);
|
|
+ switch (colorScheme) {
|
|
+ case 'light': cv.emulatePrefersColorScheme(cv.PREFERS_COLOR_SCHEME_LIGHT); break;
|
|
+ case 'dark': cv.emulatePrefersColorScheme(cv.PREFERS_COLOR_SCHEME_DARK); break;
|
|
+ case 'no-preference': cv.emulatePrefersColorScheme(cv.PREFERS_COLOR_SCHEME_NO_PREFERENCE); break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ async setUserAgent({userAgent}) {
|
|
+ const docShell = this._frameTree.mainFrame().docShell();
|
|
+ docShell.customUserAgent = userAgent;
|
|
+ }
|
|
+
|
|
+ async setBypassCSP({enabled}) {
|
|
+ const docShell = this._frameTree.mainFrame().docShell();
|
|
+ docShell.bypassCSPEnabled = enabled;
|
|
+ }
|
|
+
|
|
+ addScriptToEvaluateOnNewDocument({script}) {
|
|
+ const scriptId = helper.generateId();
|
|
+ this._scriptsToEvaluateOnNewDocument.set(scriptId, script);
|
|
+ return {scriptId};
|
|
+ }
|
|
+
|
|
+ removeScriptToEvaluateOnNewDocument({scriptId}) {
|
|
+ this._scriptsToEvaluateOnNewDocument.delete(scriptId);
|
|
+ }
|
|
+
|
|
+ setCacheDisabled({cacheDisabled}) {
|
|
+ const enable = Ci.nsIRequest.LOAD_NORMAL;
|
|
+ const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
|
|
+ Ci.nsIRequest.INHIBIT_CACHING;
|
|
+
|
|
+ const docShell = this._frameTree.mainFrame().docShell();
|
|
+ docShell.defaultLoadFlags = cacheDisabled ? disable : enable;
|
|
+ }
|
|
+
|
|
+ setJavascriptEnabled({enabled}) {
|
|
+ const docShell = this._frameTree.mainFrame().docShell();
|
|
+ docShell.allowJavascript = enabled;
|
|
+ }
|
|
+
|
|
+ enable() {
|
|
+ if (this._enabled)
|
|
+ return;
|
|
+
|
|
+ this._enabled = true;
|
|
+ // 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);
|
|
+ }
|
|
+
|
|
+ this._eventListeners = [
|
|
+ helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'),
|
|
+ helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'),
|
|
+ helper.addEventListener(this._session.mm(), 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)),
|
|
+ helper.addEventListener(this._session.mm(), 'pageshow', this._onLoad.bind(this)),
|
|
+ helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'),
|
|
+ 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)),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ setInterceptFileChooserDialog({enabled}) {
|
|
+ this._docShell.fileInputInterceptionEnabled = !!enabled;
|
|
+ }
|
|
+
|
|
+ _filePickerShown(inputElement) {
|
|
+ if (inputElement.ownerGlobal.docShell !== this._docShell)
|
|
+ return;
|
|
+ const result = this._runtime.rawElementToRemoteObject(inputElement);
|
|
+ this._session.emitEvent('Page.fileChooserOpened', {
|
|
+ executionContextId: result.executionContextId,
|
|
+ element: result.element
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _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
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onDocumentOpenLoad(document) {
|
|
+ const docShell = document.ownerGlobal.docShell;
|
|
+ const frame = this._frameTree.frameForDocShell(docShell);
|
|
+ if (!frame)
|
|
+ return;
|
|
+ this._session.emitEvent('Page.eventFired', {
|
|
+ frameId: frame.id(),
|
|
+ name: 'load'
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _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) {
|
|
+ this._session.emitEvent('Page.navigationCommitted', {
|
|
+ frameId: frame.id(),
|
|
+ navigationId: frame.lastCommittedNavigationId(),
|
|
+ url: frame.url(),
|
|
+ name: frame.name(),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onDOMWindowCreated(window) {
|
|
+ const docShell = window.docShell;
|
|
+ const frame = this._frameTree.frameForDocShell(docShell);
|
|
+ if (!frame)
|
|
+ return;
|
|
+
|
|
+ if (this._frameToExecutionContext.has(frame)) {
|
|
+ this._runtime.destroyExecutionContext(this._frameToExecutionContext.get(frame));
|
|
+ this._frameToExecutionContext.delete(frame);
|
|
+ }
|
|
+ const executionContext = this._ensureExecutionContext(frame);
|
|
+
|
|
+ if (!this._scriptsToEvaluateOnNewDocument.size && !this._bindingsToAdd.size)
|
|
+ return;
|
|
+ for (const bindingName of this._bindingsToAdd.values())
|
|
+ this._exposeFunction(frame, bindingName);
|
|
+ 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,
|
|
+ });
|
|
+ this._ensureExecutionContext(frame);
|
|
+ }
|
|
+
|
|
+ _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(), {
|
|
+ frameId: frame.id(),
|
|
+ });
|
|
+ this._frameToExecutionContext.set(frame, executionContext);
|
|
+ }
|
|
+ return executionContext;
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+
|
|
+ async navigate({frameId, url, referer}) {
|
|
+ try {
|
|
+ const uri = NetUtil.newURI(url);
|
|
+ } catch (e) {
|
|
+ throw new Error(`Invalid url: "${url}"`);
|
|
+ }
|
|
+ let referrerURI = null;
|
|
+ let referrerInfo = null;
|
|
+ if (referer) {
|
|
+ try {
|
|
+ referrerURI = NetUtil.newURI(referer);
|
|
+ const ReferrerInfo = Components.Constructor(
|
|
+ '@mozilla.org/referrer-info;1',
|
|
+ 'nsIReferrerInfo',
|
|
+ 'init'
|
|
+ );
|
|
+ referrerInfo = new ReferrerInfo(Ci.nsIHttpChannel.REFERRER_POLICY_UNSET, true, referrerURI);
|
|
+ } catch (e) {
|
|
+ throw new Error(`Invalid referer: "${referer}"`);
|
|
+ }
|
|
+ }
|
|
+ const frame = this._frameTree.frame(frameId);
|
|
+ const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation);
|
|
+ docShell.loadURI(url, {
|
|
+ flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
|
|
+ referrerInfo,
|
|
+ postData: null,
|
|
+ headers: null,
|
|
+ });
|
|
+ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
|
|
+ }
|
|
+
|
|
+ async reload({frameId, url}) {
|
|
+ const frame = this._frameTree.frame(frameId);
|
|
+ const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation);
|
|
+ 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()};
|
|
+ }
|
|
+
|
|
+ addBinding({name}) {
|
|
+ if (this._bindingsToAdd.has(name))
|
|
+ throw new Error(`Binding with name ${name} already exists`);
|
|
+ this._bindingsToAdd.add(name);
|
|
+ for (const frame of this._frameTree.frames())
|
|
+ this._exposeFunction(frame, name);
|
|
+ }
|
|
+
|
|
+ _exposeFunction(frame, name) {
|
|
+ Cu.exportFunction((...args) => {
|
|
+ const executionContext = this._ensureExecutionContext(frame);
|
|
+ this._session.emitEvent('Page.bindingCalled', {
|
|
+ executionContextId: executionContext.id(),
|
|
+ name,
|
|
+ payload: args[0]
|
|
+ });
|
|
+ }, frame.domWindow(), {
|
|
+ defineAs: name,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ async setFileInputFiles({objectId, frameId, files}) {
|
|
+ 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)
|
|
+ throw new Error('Object is not input!');
|
|
+ const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
|
|
+ unsafeObject.mozSetFileArray(nsFiles);
|
|
+ }
|
|
+
|
|
+ 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};
|
|
+ }
|
|
+
|
|
+ contentFrame({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.contentWindow)
|
|
+ return null;
|
|
+ const contentFrame = this._frameTree.frameForDocShell(unsafeObject.contentWindow.docShell);
|
|
+ return {frameId: contentFrame.id()};
|
|
+ }
|
|
+
|
|
+ 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 screenshot({mimeType, fullPage, 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();
|
|
+ if (key === 'Meta' && Services.appinfo.OS !== 'Darwin')
|
|
+ key = 'OS';
|
|
+ else if (key === 'OS' && Services.appinfo.OS === 'Darwin')
|
|
+ key = 'Meta';
|
|
+ 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 dispatchTouchEvent({type, touchPoints, modifiers}) {
|
|
+ const frame = this._frameTree.mainFrame();
|
|
+ const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent(
|
|
+ type.toLowerCase(),
|
|
+ touchPoints.map((point, id) => id),
|
|
+ touchPoints.map(point => point.x),
|
|
+ touchPoints.map(point => point.y),
|
|
+ touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX),
|
|
+ touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY),
|
|
+ touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle),
|
|
+ touchPoints.map(point => point.force === undefined ? 1.0 : point.force),
|
|
+ touchPoints.length,
|
|
+ modifiers);
|
|
+ return {defaultPrevented};
|
|
+ }
|
|
+
|
|
+ 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);
|
|
+ }
|
|
+
|
|
+ async getFullAXTree() {
|
|
+ const service = Cc["@mozilla.org/accessibilityService;1"]
|
|
+ .getService(Ci.nsIAccessibilityService);
|
|
+ const document = this._frameTree.mainFrame().domWindow().document;
|
|
+ const docAcc = service.getAccessibleFor(document);
|
|
+
|
|
+ async function waitForQuiet() {
|
|
+ let state = {};
|
|
+ docAcc.getState(state, {});
|
|
+ if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0)
|
|
+ return;
|
|
+ let resolve, reject;
|
|
+ const promise = new Promise((x, y) => {resolve = x, reject = y});
|
|
+ let eventObserver = {
|
|
+ observe(subject, topic) {
|
|
+ if (topic !== "accessible-event") {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // If event type does not match expected type, skip the event.
|
|
+ let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
|
|
+ if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // If event's accessible does not match expected accessible,
|
|
+ // skip the event.
|
|
+ if (event.accessible !== docAcc) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ Services.obs.removeObserver(this, "accessible-event");
|
|
+ resolve();
|
|
+ },
|
|
+ };
|
|
+ Services.obs.addObserver(eventObserver, "accessible-event");
|
|
+ return promise;
|
|
+ }
|
|
+ function buildNode(accElement) {
|
|
+ let a = {}, b = {};
|
|
+ accElement.getState(a, b);
|
|
+ const tree = {
|
|
+ role: service.getStringRole(accElement.role),
|
|
+ name: accElement.name || '',
|
|
+ };
|
|
+ for (const userStringProperty of [
|
|
+ 'value',
|
|
+ 'description'
|
|
+ ]) {
|
|
+ tree[userStringProperty] = accElement[userStringProperty] || undefined;
|
|
+ }
|
|
+
|
|
+ const states = {};
|
|
+ for (const name of service.getStringStates(a.value, b.value))
|
|
+ states[name] = true;
|
|
+ for (const name of ['selected',
|
|
+ 'focused',
|
|
+ 'pressed',
|
|
+ 'focusable',
|
|
+ 'haspopup',
|
|
+ 'required',
|
|
+ 'invalid',
|
|
+ 'modal',
|
|
+ 'editable',
|
|
+ 'busy',
|
|
+ 'checked',
|
|
+ 'multiselectable']) {
|
|
+ if (states[name])
|
|
+ tree[name] = true;
|
|
+ }
|
|
+
|
|
+ if (states['multi line'])
|
|
+ tree['multiline'] = true;
|
|
+ if (states['editable'] && states['readonly'])
|
|
+ tree['readonly'] = true;
|
|
+ if (states['checked'])
|
|
+ tree['checked'] = true;
|
|
+ if (states['mixed'])
|
|
+ tree['checked'] = 'mixed';
|
|
+ if (states['expanded'])
|
|
+ tree['expanded'] = true;
|
|
+ else if (states['collapsed'])
|
|
+ tree['expanded'] = false
|
|
+ if (!states['enabled'])
|
|
+ tree['disabled'] = true;
|
|
+
|
|
+ const attributes = {};
|
|
+ if (accElement.attributes) {
|
|
+ for (const { key, value } of accElement.attributes.enumerate()) {
|
|
+ attributes[key] = value;
|
|
+ }
|
|
+ }
|
|
+ for (const numericalProperty of ['level']) {
|
|
+ if (numericalProperty in attributes)
|
|
+ tree[numericalProperty] = parseFloat(attributes[numericalProperty]);
|
|
+ }
|
|
+ for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts']) {
|
|
+ if (stringProperty in attributes)
|
|
+ tree[stringProperty] = attributes[stringProperty];
|
|
+ }
|
|
+ const children = [];
|
|
+
|
|
+ for (let child = accElement.firstChild; child; child = child.nextSibling) {
|
|
+ children.push(buildNode(child));
|
|
+ }
|
|
+ if (children.length)
|
|
+ tree.children = children;
|
|
+ return tree;
|
|
+ }
|
|
+ await waitForQuiet();
|
|
+ return {
|
|
+ tree: buildNode(docAcc)
|
|
+ };
|
|
+ }
|
|
+}
|
|
+
|
|
+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;
|
|
+
|
|
diff --git a/testing/juggler/content/RuntimeAgent.js b/testing/juggler/content/RuntimeAgent.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..a8f017a071334c73aa96160c96018e6f5ac65d1f
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/RuntimeAgent.js
|
|
@@ -0,0 +1,468 @@
|
|
+"use strict";
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+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();
|
|
+
|
|
+const consoleLevelToProtocolType = {
|
|
+ '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 disallowedMessageCategories = new Set([
|
|
+ 'XPConnect JavaScript',
|
|
+ 'component javascript',
|
|
+ 'chrome javascript',
|
|
+ 'chrome registration',
|
|
+ 'XBL',
|
|
+ 'XBL Prototype Handler',
|
|
+ 'XBL Content Sink',
|
|
+ 'xbl javascript',
|
|
+]);
|
|
+
|
|
+class RuntimeAgent {
|
|
+ constructor(session) {
|
|
+ this._debugger = new Debugger();
|
|
+ this._pendingPromises = new Map();
|
|
+ this._session = session;
|
|
+ this._executionContexts = new Map();
|
|
+ this._windowToExecutionContext = new Map();
|
|
+ this._consoleServiceListener = {
|
|
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]),
|
|
+
|
|
+ observe: message => {
|
|
+ if (!(message instanceof Ci.nsIScriptError) || !message.outerWindowID ||
|
|
+ !message.category || disallowedMessageCategories.has(message.category)) {
|
|
+ return;
|
|
+ }
|
|
+ const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID);
|
|
+ const executionContext = this._windowToExecutionContext.get(errorWindow);
|
|
+ if (!executionContext)
|
|
+ return;
|
|
+ const typeNames = {
|
|
+ [Ci.nsIConsoleMessage.debug]: 'debug',
|
|
+ [Ci.nsIConsoleMessage.info]: 'info',
|
|
+ [Ci.nsIConsoleMessage.warn]: 'warn',
|
|
+ [Ci.nsIConsoleMessage.error]: 'error',
|
|
+ };
|
|
+ this._session.emitEvent('Runtime.console', {
|
|
+ args: [{
|
|
+ value: message.message,
|
|
+ }],
|
|
+ type: typeNames[message.logLevel],
|
|
+ executionContextId: executionContext.id(),
|
|
+ location: {
|
|
+ lineNumber: message.lineNumber,
|
|
+ columnNumber: message.columnNumber,
|
|
+ url: message.sourceName,
|
|
+ },
|
|
+ });
|
|
+ },
|
|
+ };
|
|
+
|
|
+ this._eventListeners = [];
|
|
+ this._enabled = false;
|
|
+ }
|
|
+
|
|
+ rawElementToRemoteObject(node) {
|
|
+ const executionContext = Array.from(this._executionContexts.values()).find(context => node.ownerDocument == context._domWindow.document);
|
|
+ return {
|
|
+ executionContextId: executionContext.id(),
|
|
+ element: executionContext.rawValueToRemoteObject(node)
|
|
+ };
|
|
+ }
|
|
+
|
|
+ _consoleAPICalled({wrappedJSObject}, topic, data) {
|
|
+ const type = consoleLevelToProtocolType[wrappedJSObject.level];
|
|
+ if (!type)
|
|
+ return;
|
|
+ const executionContext = Array.from(this._executionContexts.values()).find(context => {
|
|
+ const domWindow = context._domWindow;
|
|
+ return domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID;
|
|
+ });
|
|
+ if (!executionContext)
|
|
+ return;
|
|
+ const args = wrappedJSObject.arguments.map(arg => executionContext.rawValueToRemoteObject(arg));
|
|
+ this._session.emitEvent('Runtime.console', {
|
|
+ args,
|
|
+ type,
|
|
+ executionContextId: executionContext.id(),
|
|
+ location: {
|
|
+ lineNumber: wrappedJSObject.lineNumber - 1,
|
|
+ columnNumber: wrappedJSObject.columnNumber - 1,
|
|
+ url: wrappedJSObject.filename,
|
|
+ },
|
|
+ });
|
|
+ }
|
|
+
|
|
+ enable() {
|
|
+ if (this._enabled)
|
|
+ return;
|
|
+ this._enabled = true;
|
|
+ for (const executionContext of this._executionContexts.values())
|
|
+ this._notifyExecutionContextCreated(executionContext);
|
|
+ Services.console.registerListener(this._consoleServiceListener);
|
|
+ this._eventListeners = [
|
|
+ () => Services.console.unregisterListener(this._consoleServiceListener),
|
|
+ helper.addObserver(this._consoleAPICalled.bind(this), "console-api-log-event"),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ _notifyExecutionContextCreated(executionContext) {
|
|
+ if (!this._enabled)
|
|
+ return;
|
|
+ this._session.emitEvent('Runtime.executionContextCreated', {
|
|
+ executionContextId: executionContext._id,
|
|
+ auxData: executionContext._auxData,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _notifyExecutionContextDestroyed(executionContext) {
|
|
+ if (!this._enabled)
|
|
+ return;
|
|
+ this._session.emitEvent('Runtime.executionContextDestroyed', {
|
|
+ executionContextId: executionContext._id,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+
|
|
+ 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, auxData) {
|
|
+ const context = new ExecutionContext(this, domWindow, this._debugger.addDebuggee(domWindow), auxData);
|
|
+ this._executionContexts.set(context._id, context);
|
|
+ this._windowToExecutionContext.set(domWindow, context);
|
|
+ this._notifyExecutionContextCreated(context);
|
|
+ return context;
|
|
+ }
|
|
+
|
|
+ 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);
|
|
+ this._executionContexts.delete(destroyedContext._id);
|
|
+ this._windowToExecutionContext.delete(destroyedContext._domWindow);
|
|
+ this._notifyExecutionContextDestroyed(destroyedContext);
|
|
+ }
|
|
+
|
|
+ async evaluate({executionContextId, expression, returnByValue}) {
|
|
+ const executionContext = this._executionContexts.get(executionContextId);
|
|
+ if (!executionContext)
|
|
+ throw new Error('Failed to find execution context with id = ' + executionContextId);
|
|
+ const exceptionDetails = {};
|
|
+ let result = await executionContext.evaluateScript(expression, exceptionDetails);
|
|
+ if (!result)
|
|
+ return {exceptionDetails};
|
|
+ let isNode = undefined;
|
|
+ if (returnByValue)
|
|
+ result = executionContext.ensureSerializedToValue(result);
|
|
+ return {result};
|
|
+ }
|
|
+
|
|
+ async callFunction({executionContextId, functionDeclaration, args, returnByValue}) {
|
|
+ const executionContext = this._executionContexts.get(executionContextId);
|
|
+ if (!executionContext)
|
|
+ throw new Error('Failed to find execution context with id = ' + executionContextId);
|
|
+ const exceptionDetails = {};
|
|
+ let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails);
|
|
+ if (!result)
|
|
+ return {exceptionDetails};
|
|
+ let isNode = undefined;
|
|
+ if (returnByValue)
|
|
+ result = executionContext.ensureSerializedToValue(result);
|
|
+ return {result};
|
|
+ }
|
|
+
|
|
+ async getObjectProperties({executionContextId, objectId}) {
|
|
+ const executionContext = this._executionContexts.get(executionContextId);
|
|
+ if (!executionContext)
|
|
+ throw new Error('Failed to find execution context with id = ' + executionContextId);
|
|
+ return {properties: executionContext.getObjectProperties(objectId)};
|
|
+ }
|
|
+
|
|
+ async disposeObject({executionContextId, objectId}) {
|
|
+ const executionContext = this._executionContexts.get(executionContextId);
|
|
+ if (!executionContext)
|
|
+ throw new Error('Failed to find execution context with id = ' + executionContextId);
|
|
+ return executionContext.disposeObject(objectId);
|
|
+ }
|
|
+}
|
|
+
|
|
+class ExecutionContext {
|
|
+ constructor(runtime, domWindow, global, auxData) {
|
|
+ this._runtime = runtime;
|
|
+ this._domWindow = domWindow;
|
|
+ this._global = global;
|
|
+ this._remoteObjects = new Map();
|
|
+ this._id = helper.generateId();
|
|
+ this._auxData = auxData;
|
|
+ }
|
|
+
|
|
+ id() {
|
|
+ return this._id;
|
|
+ }
|
|
+
|
|
+ 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;
|
|
diff --git a/testing/juggler/content/ScrollbarManager.js b/testing/juggler/content/ScrollbarManager.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..caee4df323d0a526ed7e38947c41c6430983568d
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/ScrollbarManager.js
|
|
@@ -0,0 +1,85 @@
|
|
+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(docShell) {
|
|
+ this._docShell = docShell;
|
|
+ this._customScrollbars = null;
|
|
+ this._contentViewerScrollBars = new Map();
|
|
+
|
|
+ if (isHeadless)
|
|
+ this._setCustomScrollbars(HIDDEN_SCROLLBARS);
|
|
+
|
|
+ const webProgress = this._docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
+ .getInterface(Ci.nsIWebProgress);
|
|
+
|
|
+ this.QueryInterface = ChromeUtils.generateQI(['nsIWebProgressListener', 'nsISupportsWeakReference']);
|
|
+ this._eventListeners = [
|
|
+ helper.addProgressListener(webProgress, this, Ci.nsIWebProgress.NOTIFY_ALL),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ onLocationChange(webProgress, request, URI, flags) {
|
|
+ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
|
|
+ return;
|
|
+ this._updateAllDocShells();
|
|
+ }
|
|
+
|
|
+ setFloatingScrollbars(enabled) {
|
|
+ if (this._customScrollbars === HIDDEN_SCROLLBARS)
|
|
+ return;
|
|
+ this._setCustomScrollbars(enabled ? FLOATING_SCROLLBARS : null);
|
|
+ }
|
|
+
|
|
+ _setCustomScrollbars(customScrollbars) {
|
|
+ if (this._customScrollbars === customScrollbars)
|
|
+ return;
|
|
+ this._customScrollbars = customScrollbars;
|
|
+ this._updateAllDocShells();
|
|
+ }
|
|
+
|
|
+ _updateAllDocShells() {
|
|
+ const allDocShells = [this._docShell];
|
|
+ for (let i = 0; i < this._docShell.childCount; i++)
|
|
+ allDocShells.push(this._docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell));
|
|
+ // At this point, a content viewer might not be loaded for certain docShells.
|
|
+ // Scrollbars will be updated in onLocationChange.
|
|
+ const contentViewers = allDocShells.map(docShell => docShell.contentViewer).filter(contentViewer => !!contentViewer);
|
|
+
|
|
+ // Update scrollbar stylesheets.
|
|
+ for (const contentViewer of contentViewers) {
|
|
+ const oldScrollbars = this._contentViewerScrollBars.get(contentViewer);
|
|
+ if (oldScrollbars === this._customScrollbars)
|
|
+ continue;
|
|
+ const winUtils = contentViewer.DOMDocument.defaultView.windowUtils;
|
|
+ if (oldScrollbars)
|
|
+ winUtils.removeSheet(oldScrollbars, winUtils.AGENT_SHEET);
|
|
+ if (this._customScrollbars)
|
|
+ winUtils.loadSheet(this._customScrollbars, winUtils.AGENT_SHEET);
|
|
+ }
|
|
+ // Update state for all *existing* docShells.
|
|
+ this._contentViewerScrollBars.clear();
|
|
+ for (const contentViewer of contentViewers)
|
|
+ this._contentViewerScrollBars.set(contentViewer, this._customScrollbars);
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._setCustomScrollbars(null);
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['ScrollbarManager'];
|
|
+this.ScrollbarManager = ScrollbarManager;
|
|
+
|
|
diff --git a/testing/juggler/content/floating-scrollbars.css b/testing/juggler/content/floating-scrollbars.css
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..7709bdd34c65062fc63684ef17fc792d3991d965
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/floating-scrollbars.css
|
|
@@ -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;
|
|
+}
|
|
diff --git a/testing/juggler/content/hidden-scrollbars.css b/testing/juggler/content/hidden-scrollbars.css
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..3a386425d3796d0a6786dea193b3402dfd2ac4f6
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/hidden-scrollbars.css
|
|
@@ -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;
|
|
+}
|
|
+
|
|
diff --git a/testing/juggler/content/main.js b/testing/juggler/content/main.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..8585092e04e7e763a0c115c28363e505e8eb91bd
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/main.js
|
|
@@ -0,0 +1,39 @@
|
|
+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 {NetworkMonitor} = ChromeUtils.import('chrome://juggler/content/content/NetworkMonitor.js');
|
|
+const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js');
|
|
+
|
|
+const sessions = new Map();
|
|
+const frameTree = new FrameTree(docShell);
|
|
+const networkMonitor = new NetworkMonitor(docShell, frameTree);
|
|
+const scrollbarManager = new ScrollbarManager(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, networkMonitor));
|
|
+ }),
|
|
+
|
|
+ helper.addMessageListener(this, 'juggler:dispose-content-session', msg => {
|
|
+ const sessionId = msg.data;
|
|
+ const session = sessions.get(sessionId);
|
|
+ if (!session)
|
|
+ return;
|
|
+ sessions.delete(sessionId);
|
|
+ session.dispose();
|
|
+ }),
|
|
+
|
|
+ helper.addEventListener(this, 'unload', msg => {
|
|
+ helper.removeListeners(gListeners);
|
|
+ for (const session of sessions.values())
|
|
+ session.dispose();
|
|
+ sessions.clear();
|
|
+ scrollbarManager.dispose();
|
|
+ networkMonitor.dispose();
|
|
+ frameTree.dispose();
|
|
+ }),
|
|
+];
|
|
+
|
|
diff --git a/testing/juggler/jar.mn b/testing/juggler/jar.mn
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..27f5a15fd7f14385bb1f080d5965d90e60423d1a
|
|
--- /dev/null
|
|
+++ b/testing/juggler/jar.mn
|
|
@@ -0,0 +1,29 @@
|
|
+# 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/Helper.js (Helper.js)
|
|
+ content/NetworkObserver.js (NetworkObserver.js)
|
|
+ content/BrowserContextManager.js (BrowserContextManager.js)
|
|
+ content/TargetRegistry.js (TargetRegistry.js)
|
|
+ content/protocol/PrimitiveTypes.js (protocol/PrimitiveTypes.js)
|
|
+ content/protocol/Protocol.js (protocol/Protocol.js)
|
|
+ content/protocol/Dispatcher.js (protocol/Dispatcher.js)
|
|
+ content/protocol/PageHandler.js (protocol/PageHandler.js)
|
|
+ content/protocol/RuntimeHandler.js (protocol/RuntimeHandler.js)
|
|
+ content/protocol/NetworkHandler.js (protocol/NetworkHandler.js)
|
|
+ content/protocol/BrowserHandler.js (protocol/BrowserHandler.js)
|
|
+ content/protocol/TargetHandler.js (protocol/TargetHandler.js)
|
|
+ content/protocol/AccessibilityHandler.js (protocol/AccessibilityHandler.js)
|
|
+ content/content/main.js (content/main.js)
|
|
+ content/content/ContentSession.js (content/ContentSession.js)
|
|
+ content/content/FrameTree.js (content/FrameTree.js)
|
|
+ content/content/NetworkMonitor.js (content/NetworkMonitor.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)
|
|
+
|
|
diff --git a/testing/juggler/moz.build b/testing/juggler/moz.build
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..1a0a3130bf9509829744fadc692a79754fddd351
|
|
--- /dev/null
|
|
+++ b/testing/juggler/moz.build
|
|
@@ -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")
|
|
+
|
|
diff --git a/testing/juggler/protocol/AccessibilityHandler.js b/testing/juggler/protocol/AccessibilityHandler.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..fc8a7397e50a3316760bd0b4fee74ef7fb97e1c5
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/AccessibilityHandler.js
|
|
@@ -0,0 +1,15 @@
|
|
+class AccessibilityHandler {
|
|
+ constructor(chromeSession, contentSession) {
|
|
+ this._chromeSession = chromeSession;
|
|
+ this._contentSession = contentSession;
|
|
+ }
|
|
+
|
|
+ async getFullAXTree() {
|
|
+ return await this._contentSession.send('Page.getFullAXTree');
|
|
+ }
|
|
+
|
|
+ dispose() { }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['AccessibilityHandler'];
|
|
+this.AccessibilityHandler = AccessibilityHandler;
|
|
diff --git a/testing/juggler/protocol/BrowserHandler.js b/testing/juggler/protocol/BrowserHandler.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..708059a95b3a01f3d9c7b7ef4714ee6f8ab26b94
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/BrowserHandler.js
|
|
@@ -0,0 +1,66 @@
|
|
+"use strict";
|
|
+
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+const { allowAllCerts } = ChromeUtils.import(
|
|
+ "chrome://marionette/content/cert.js"
|
|
+);
|
|
+const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js");
|
|
+
|
|
+class BrowserHandler {
|
|
+ /**
|
|
+ * @param {ChromeSession} session
|
|
+ */
|
|
+ constructor() {
|
|
+ this._sweepingOverride = null;
|
|
+ this._contextManager = BrowserContextManager.instance();
|
|
+ }
|
|
+
|
|
+ async close() {
|
|
+ Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
|
|
+ }
|
|
+
|
|
+ async setIgnoreHTTPSErrors({enabled}) {
|
|
+ if (!enabled) {
|
|
+ allowAllCerts.disable()
|
|
+ Services.prefs.setBoolPref('security.mixed_content.block_active_content', true);
|
|
+ } else {
|
|
+ allowAllCerts.enable()
|
|
+ Services.prefs.setBoolPref('security.mixed_content.block_active_content', false);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ grantPermissions({browserContextId, origin, permissions}) {
|
|
+ this._contextManager.grantPermissions(browserContextId, origin, permissions);
|
|
+ }
|
|
+
|
|
+ resetPermissions({browserContextId}) {
|
|
+ this._contextManager.resetPermissions(browserContextId);
|
|
+ }
|
|
+
|
|
+ setCookies({browserContextId, cookies}) {
|
|
+ this._contextManager.setCookies(browserContextId, cookies);
|
|
+ }
|
|
+
|
|
+ clearCookies({browserContextId}) {
|
|
+ this._contextManager.clearCookies(browserContextId);
|
|
+ }
|
|
+
|
|
+ getCookies({browserContextId}) {
|
|
+ return {cookies: this._contextManager.getCookies(browserContextId)};
|
|
+ }
|
|
+
|
|
+ async getInfo() {
|
|
+ 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};
|
|
+ }
|
|
+
|
|
+ dispose() { }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['BrowserHandler'];
|
|
+this.BrowserHandler = BrowserHandler;
|
|
diff --git a/testing/juggler/protocol/Dispatcher.js b/testing/juggler/protocol/Dispatcher.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..7b3a6fa4fe7a2b50a78ed446fbf5537504994798
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/Dispatcher.js
|
|
@@ -0,0 +1,255 @@
|
|
+const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
|
|
+const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js");
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const helper = new Helper();
|
|
+
|
|
+const PROTOCOL_HANDLERS = {
|
|
+ Page: ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js").PageHandler,
|
|
+ Network: ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js").NetworkHandler,
|
|
+ Browser: ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js").BrowserHandler,
|
|
+ Target: ChromeUtils.import("chrome://juggler/content/protocol/TargetHandler.js").TargetHandler,
|
|
+ Runtime: ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js").RuntimeHandler,
|
|
+ Accessibility: ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js").AccessibilityHandler,
|
|
+};
|
|
+
|
|
+class Dispatcher {
|
|
+ /**
|
|
+ * @param {Connection} connection
|
|
+ */
|
|
+ constructor(connection) {
|
|
+ this._connection = connection;
|
|
+ this._connection.onmessage = this._dispatch.bind(this);
|
|
+ this._connection.onclose = this._dispose.bind(this);
|
|
+
|
|
+ this._targetSessions = new Map();
|
|
+ this._sessions = new Map();
|
|
+ this._rootSession = new ChromeSession(this, undefined, null /* contentSession */, TargetRegistry.instance().browserTargetInfo());
|
|
+
|
|
+ this._eventListeners = [
|
|
+ helper.on(TargetRegistry.instance(), TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ async createSession(targetId) {
|
|
+ const targetInfo = TargetRegistry.instance().targetInfo(targetId);
|
|
+ if (!targetInfo)
|
|
+ throw new Error(`Target "${targetId}" is not found`);
|
|
+ let targetSessions = this._targetSessions.get(targetId);
|
|
+ if (!targetSessions) {
|
|
+ targetSessions = new Map();
|
|
+ this._targetSessions.set(targetId, targetSessions);
|
|
+ }
|
|
+
|
|
+ const sessionId = helper.generateId();
|
|
+ const contentSession = targetInfo.type === 'page' ? new ContentSession(this, sessionId, targetInfo) : null;
|
|
+ const chromeSession = new ChromeSession(this, sessionId, contentSession, targetInfo);
|
|
+ targetSessions.set(sessionId, chromeSession);
|
|
+ this._sessions.set(sessionId, chromeSession);
|
|
+ this._emitEvent(this._rootSession._sessionId, 'Target.attachedToTarget', {
|
|
+ sessionId: sessionId,
|
|
+ targetInfo
|
|
+ });
|
|
+ return sessionId;
|
|
+ }
|
|
+
|
|
+ _dispose() {
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ this._connection.onmessage = null;
|
|
+ this._connection.onclose = null;
|
|
+ this._rootSession.dispose();
|
|
+ this._rootSession = null;
|
|
+ for (const session of this._sessions.values())
|
|
+ session.dispose();
|
|
+ this._sessions.clear();
|
|
+ this._targetSessions.clear();
|
|
+ }
|
|
+
|
|
+ _onTargetDestroyed({targetId}) {
|
|
+ const sessions = this._targetSessions.get(targetId);
|
|
+ if (!sessions)
|
|
+ return;
|
|
+ this._targetSessions.delete(targetId);
|
|
+ for (const [sessionId, session] of sessions) {
|
|
+ session.dispose();
|
|
+ this._sessions.delete(sessionId);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ async _dispatch(event) {
|
|
+ const data = JSON.parse(event.data);
|
|
+ const id = data.id;
|
|
+ const sessionId = data.sessionId;
|
|
+ delete data.sessionId;
|
|
+ try {
|
|
+ const session = sessionId ? this._sessions.get(sessionId) : this._rootSession;
|
|
+ if (!session)
|
|
+ throw new Error(`ERROR: cannot find session with id "${sessionId}"`);
|
|
+ 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 [domain, methodName] = method.split('.');
|
|
+ const descriptor = protocol.domains[domain] ? protocol.domains[domain].methods[methodName] : null;
|
|
+ if (!descriptor)
|
|
+ throw new Error(`ERROR: method '${method}' is not supported`);
|
|
+ let details = {};
|
|
+ if (!checkScheme(descriptor.params || {}, params, details))
|
|
+ throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`);
|
|
+
|
|
+ const result = await session.dispatch(method, params);
|
|
+
|
|
+ details = {};
|
|
+ if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details))
|
|
+ throw new Error(`ERROR: failed to dispatch method '${method}' result ${JSON.stringify(result, null, 2)}\n${details.error}`);
|
|
+
|
|
+ this._connection.send(JSON.stringify({id, sessionId, result}));
|
|
+ } catch (e) {
|
|
+ this._connection.send(JSON.stringify({id, sessionId, error: {
|
|
+ message: e.message,
|
|
+ data: e.stack
|
|
+ }}));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ _emitEvent(sessionId, eventName, params) {
|
|
+ const [domain, eName] = eventName.split('.');
|
|
+ const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null;
|
|
+ if (!scheme)
|
|
+ throw new Error(`ERROR: event '${eventName}' is not supported`);
|
|
+ const details = {};
|
|
+ if (!checkScheme(scheme, params || {}, details))
|
|
+ throw new Error(`ERROR: failed to emit event '${eventName}' ${JSON.stringify(params, null, 2)}\n${details.error}`);
|
|
+ this._connection.send(JSON.stringify({method: eventName, params, sessionId}));
|
|
+ }
|
|
+}
|
|
+
|
|
+class ChromeSession {
|
|
+ /**
|
|
+ * @param {Connection} connection
|
|
+ */
|
|
+ constructor(dispatcher, sessionId, contentSession, targetInfo) {
|
|
+ this._dispatcher = dispatcher;
|
|
+ this._sessionId = sessionId;
|
|
+ this._contentSession = contentSession;
|
|
+ this._targetInfo = targetInfo;
|
|
+
|
|
+ this._handlers = {};
|
|
+ for (const [domainName, handlerFactory] of Object.entries(PROTOCOL_HANDLERS)) {
|
|
+ if (protocol.domains[domainName].targets.includes(targetInfo.type))
|
|
+ this._handlers[domainName] = new handlerFactory(this, contentSession);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ dispatcher() {
|
|
+ return this._dispatcher;
|
|
+ }
|
|
+
|
|
+ targetId() {
|
|
+ return this._targetInfo.targetId;
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ if (this._contentSession)
|
|
+ this._contentSession.dispose();
|
|
+ this._contentSession = null;
|
|
+ for (const [domainName, handler] of Object.entries(this._handlers)) {
|
|
+ if (!handler.dispose)
|
|
+ throw new Error(`Handler for "${domainName}" domain does not define |dispose| method!`);
|
|
+ handler.dispose();
|
|
+ delete this._handlers[domainName];
|
|
+ }
|
|
+ // Root session don't have sessionId and don't emit detachedFromTarget.
|
|
+ if (this._sessionId) {
|
|
+ this._dispatcher._emitEvent(this._sessionId, 'Target.detachedFromTarget', {
|
|
+ sessionId: this._sessionId,
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+
|
|
+ emitEvent(eventName, params) {
|
|
+ this._dispatcher._emitEvent(this._sessionId, eventName, params);
|
|
+ }
|
|
+
|
|
+ async dispatch(method, params) {
|
|
+ const [domainName, methodName] = method.split('.');
|
|
+ if (!this._handlers[domainName])
|
|
+ throw new Error(`Domain "${domainName}" does not exist`);
|
|
+ if (!this._handlers[domainName][methodName])
|
|
+ throw new Error(`Handler for domain "${domainName}" does not implement method "${methodName}"`);
|
|
+ return await this._handlers[domainName][methodName](params);
|
|
+ }
|
|
+}
|
|
+
|
|
+class ContentSession {
|
|
+ constructor(dispatcher, sessionId, targetInfo) {
|
|
+ this._dispatcher = dispatcher;
|
|
+ const tab = TargetRegistry.instance().tabForTarget(targetInfo.targetId);
|
|
+ this._browser = tab.linkedBrowser;
|
|
+ this._messageId = 0;
|
|
+ this._pendingMessages = new Map();
|
|
+ this._sessionId = sessionId;
|
|
+ this._browser.messageManager.sendAsyncMessage('juggler:create-content-session', this._sessionId);
|
|
+ this._disposed = false;
|
|
+ this._eventListeners = [
|
|
+ helper.addMessageListener(this._browser.messageManager, this._sessionId, {
|
|
+ receiveMessage: message => this._onMessage(message)
|
|
+ }),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ isDisposed() {
|
|
+ return this._disposed;
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ if (this._disposed)
|
|
+ return;
|
|
+ this._disposed = true;
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ for (const {resolve, reject, methodName} of this._pendingMessages.values())
|
|
+ reject(new Error(`Failed "${methodName}": Page closed.`));
|
|
+ this._pendingMessages.clear();
|
|
+ if (this._browser.messageManager)
|
|
+ this._browser.messageManager.sendAsyncMessage('juggler:dispose-content-session', this._sessionId);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @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, methodName});
|
|
+ });
|
|
+ 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;
|
|
+ this._dispatcher._emitEvent(this._sessionId, eventName, params);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+
|
|
+this.EXPORTED_SYMBOLS = ['Dispatcher'];
|
|
+this.Dispatcher = Dispatcher;
|
|
+
|
|
diff --git a/testing/juggler/protocol/NetworkHandler.js b/testing/juggler/protocol/NetworkHandler.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..f5e7e919594b3778fd3046bf69d34878cccefa64
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/NetworkHandler.js
|
|
@@ -0,0 +1,154 @@
|
|
+"use strict";
|
|
+
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const {NetworkObserver} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
|
|
+const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.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 NetworkHandler {
|
|
+ constructor(chromeSession, contentSession) {
|
|
+ this._chromeSession = chromeSession;
|
|
+ this._contentSession = contentSession;
|
|
+ this._networkObserver = NetworkObserver.instance();
|
|
+ this._httpActivity = new Map();
|
|
+ this._enabled = false;
|
|
+ this._browser = TargetRegistry.instance().tabForTarget(this._chromeSession.targetId()).linkedBrowser;
|
|
+ this._requestInterception = false;
|
|
+ this._eventListeners = [];
|
|
+ this._pendingRequstWillBeSentEvents = new Set();
|
|
+ }
|
|
+
|
|
+ async enable() {
|
|
+ if (this._enabled)
|
|
+ return;
|
|
+ this._enabled = true;
|
|
+ this._eventListeners = [
|
|
+ helper.on(this._networkObserver, 'request', this._onRequest.bind(this)),
|
|
+ helper.on(this._networkObserver, 'response', this._onResponse.bind(this)),
|
|
+ helper.on(this._networkObserver, 'requestfinished', this._onRequestFinished.bind(this)),
|
|
+ helper.on(this._networkObserver, 'requestfailed', this._onRequestFailed.bind(this)),
|
|
+ this._networkObserver.startTrackingBrowserNetwork(this._browser),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ async getResponseBody({requestId}) {
|
|
+ return this._networkObserver.getResponseBody(this._browser, requestId);
|
|
+ }
|
|
+
|
|
+ async setExtraHTTPHeaders({headers}) {
|
|
+ this._networkObserver.setExtraHTTPHeaders(this._browser, headers);
|
|
+ }
|
|
+
|
|
+ async setRequestInterception({enabled}) {
|
|
+ if (enabled)
|
|
+ this._networkObserver.enableRequestInterception(this._browser);
|
|
+ else
|
|
+ this._networkObserver.disableRequestInterception(this._browser);
|
|
+ // Right after we enable/disable request interception we need to await all pending
|
|
+ // requestWillBeSent events before successfully returning from the method.
|
|
+ await Promise.all(Array.from(this._pendingRequstWillBeSentEvents));
|
|
+ }
|
|
+
|
|
+ async resumeSuspendedRequest({requestId, headers}) {
|
|
+ this._networkObserver.resumeSuspendedRequest(this._browser, requestId, headers);
|
|
+ }
|
|
+
|
|
+ async abortSuspendedRequest({requestId}) {
|
|
+ this._networkObserver.abortSuspendedRequest(this._browser, requestId);
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+
|
|
+ _ensureHTTPActivity(requestId) {
|
|
+ let activity = this._httpActivity.get(requestId);
|
|
+ if (!activity) {
|
|
+ activity = {
|
|
+ _id: requestId,
|
|
+ _lastSentEvent: null,
|
|
+ request: null,
|
|
+ response: null,
|
|
+ complete: null,
|
|
+ failed: null,
|
|
+ };
|
|
+ this._httpActivity.set(requestId, activity);
|
|
+ }
|
|
+ return activity;
|
|
+ }
|
|
+
|
|
+ _reportHTTPAcitivityEvents(activity) {
|
|
+ // State machine - sending network events.
|
|
+ if (!activity._lastSentEvent && activity.request) {
|
|
+ this._chromeSession.emitEvent('Network.requestWillBeSent', activity.request);
|
|
+ activity._lastSentEvent = 'requestWillBeSent';
|
|
+ }
|
|
+ if (activity._lastSentEvent === 'requestWillBeSent' && activity.response) {
|
|
+ this._chromeSession.emitEvent('Network.responseReceived', activity.response);
|
|
+ activity._lastSentEvent = 'responseReceived';
|
|
+ }
|
|
+ if (activity._lastSentEvent === 'responseReceived' && activity.complete) {
|
|
+ this._chromeSession.emitEvent('Network.requestFinished', activity.complete);
|
|
+ activity._lastSentEvent = 'requestFinished';
|
|
+ }
|
|
+ if (activity._lastSentEvent && activity.failed) {
|
|
+ this._chromeSession.emitEvent('Network.requestFailed', activity.failed);
|
|
+ activity._lastSentEvent = 'requestFailed';
|
|
+ }
|
|
+
|
|
+ // Clean up if request lifecycle is over.
|
|
+ if (activity._lastSentEvent === 'requestFinished' || activity._lastSentEvent === 'requestFailed')
|
|
+ this._httpActivity.delete(activity._id);
|
|
+ }
|
|
+
|
|
+ async _onRequest(httpChannel, eventDetails) {
|
|
+ let pendingRequestCallback;
|
|
+ let pendingRequestPromise = new Promise(x => pendingRequestCallback = x);
|
|
+ this._pendingRequstWillBeSentEvents.add(pendingRequestPromise);
|
|
+ let details = null;
|
|
+ try {
|
|
+ details = await this._contentSession.send('Page.requestDetails', {channelId: httpChannel.channelId});
|
|
+ } catch (e) {
|
|
+ if (this._contentSession.isDisposed()) {
|
|
+ pendingRequestCallback();
|
|
+ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ const activity = this._ensureHTTPActivity(eventDetails.requestId);
|
|
+ activity.request = {
|
|
+ frameId: details ? details.frameId : undefined,
|
|
+ ...eventDetails,
|
|
+ };
|
|
+ this._reportHTTPAcitivityEvents(activity);
|
|
+ pendingRequestCallback();
|
|
+ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
|
|
+ }
|
|
+
|
|
+ async _onResponse(httpChannel, eventDetails) {
|
|
+ const activity = this._ensureHTTPActivity(eventDetails.requestId);
|
|
+ activity.response = eventDetails;
|
|
+ this._reportHTTPAcitivityEvents(activity);
|
|
+ }
|
|
+
|
|
+ async _onRequestFinished(httpChannel, eventDetails) {
|
|
+ const activity = this._ensureHTTPActivity(eventDetails.requestId);
|
|
+ activity.complete = eventDetails;
|
|
+ this._reportHTTPAcitivityEvents(activity);
|
|
+ }
|
|
+
|
|
+ async _onRequestFailed(httpChannel, eventDetails) {
|
|
+ const activity = this._ensureHTTPActivity(eventDetails.requestId);
|
|
+ activity.failed = eventDetails;
|
|
+ this._reportHTTPAcitivityEvents(activity);
|
|
+ }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['NetworkHandler'];
|
|
+this.NetworkHandler = NetworkHandler;
|
|
diff --git a/testing/juggler/protocol/PageHandler.js b/testing/juggler/protocol/PageHandler.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..13e659902758eeb3482d33a7084d8dfd0d330f1e
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/PageHandler.js
|
|
@@ -0,0 +1,281 @@
|
|
+"use strict";
|
|
+
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+
|
|
+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, contentSession) {
|
|
+ this._chromeSession = chromeSession;
|
|
+ this._contentSession = contentSession;
|
|
+ this._browser = TargetRegistry.instance().tabForTarget(chromeSession.targetId()).linkedBrowser;
|
|
+ this._dialogs = new Map();
|
|
+
|
|
+ this._eventListeners = [];
|
|
+ this._enabled = false;
|
|
+ }
|
|
+
|
|
+ async close({runBeforeUnload}) {
|
|
+ // Postpone target close to deliver response in session.
|
|
+ Services.tm.dispatchToMainThread(() => {
|
|
+ TargetRegistry.instance().closePage(this._chromeSession.targetId(), runBeforeUnload);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ async enable() {
|
|
+ if (this._enabled)
|
|
+ return;
|
|
+ this._enabled = true;
|
|
+ this._updateModalDialogs();
|
|
+
|
|
+ this._eventListeners = [
|
|
+ helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => {
|
|
+ // wait for the dialog to be actually added to DOM.
|
|
+ await Promise.resolve();
|
|
+ this._updateModalDialogs();
|
|
+ }),
|
|
+ helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
|
|
+ ];
|
|
+ await this._contentSession.send('Page.enable');
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+
|
|
+ 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('Page.setViewport', {
|
|
+ deviceScaleFactor: viewport ? viewport.deviceScaleFactor : 0,
|
|
+ isMobile: viewport && viewport.isMobile,
|
|
+ hasTouch: viewport && viewport.hasTouch,
|
|
+ }),
|
|
+ this._contentSession.send('Page.awaitViewportDimensions', {
|
|
+ width: dimensions.width,
|
|
+ height: dimensions.height
|
|
+ }),
|
|
+ ]);
|
|
+ }
|
|
+
|
|
+ _updateModalDialogs() {
|
|
+ const prompts = new Set(this._browser.tabModalPromptBox ? this._browser.tabModalPromptBox.listPrompts() : []);
|
|
+ for (const dialog of this._dialogs.values()) {
|
|
+ if (!prompts.has(dialog.prompt())) {
|
|
+ this._dialogs.delete(dialog.id());
|
|
+ this._chromeSession.emitEvent('Page.dialogClosed', {
|
|
+ dialogId: dialog.id(),
|
|
+ });
|
|
+ } else {
|
|
+ prompts.delete(dialog.prompt());
|
|
+ }
|
|
+ }
|
|
+ for (const prompt of prompts) {
|
|
+ const dialog = Dialog.createIfSupported(prompt);
|
|
+ if (!dialog)
|
|
+ continue;
|
|
+ this._dialogs.set(dialog.id(), dialog);
|
|
+ this._chromeSession.emitEvent('Page.dialogOpened', {
|
|
+ dialogId: dialog.id(),
|
|
+ type: dialog.type(),
|
|
+ message: dialog.message(),
|
|
+ defaultValue: dialog.defaultValue(),
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+
|
|
+ async setUserAgent(options) {
|
|
+ return await this._contentSession.send('Page.setUserAgent', options);
|
|
+ }
|
|
+
|
|
+ async setFileInputFiles(options) {
|
|
+ return await this._contentSession.send('Page.setFileInputFiles', options);
|
|
+ }
|
|
+
|
|
+ async setBypassCSP(options) {
|
|
+ return await this._contentSession.send('Page.setBypassCSP', options);
|
|
+ }
|
|
+
|
|
+ async setEmulatedMedia(options) {
|
|
+ return await this._contentSession.send('Page.setEmulatedMedia', options);
|
|
+ }
|
|
+
|
|
+ async setJavascriptEnabled(options) {
|
|
+ return await this._contentSession.send('Page.setJavascriptEnabled', options);
|
|
+ }
|
|
+
|
|
+ async setCacheDisabled(options) {
|
|
+ return await this._contentSession.send('Page.setCacheDisabled', options);
|
|
+ }
|
|
+
|
|
+ async addBinding(options) {
|
|
+ return await this._contentSession.send('Page.addBinding', options);
|
|
+ }
|
|
+
|
|
+ async screenshot(options) {
|
|
+ return await this._contentSession.send('Page.screenshot', options);
|
|
+ }
|
|
+
|
|
+ async getBoundingBox(options) {
|
|
+ return await this._contentSession.send('Page.getBoundingBox', options);
|
|
+ }
|
|
+
|
|
+ async getContentQuads(options) {
|
|
+ return await this._contentSession.send('Page.getContentQuads', options);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {{frameId: string, url: string}} options
|
|
+ */
|
|
+ async navigate(options) {
|
|
+ return await this._contentSession.send('Page.navigate', options);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {{frameId: string, url: string}} options
|
|
+ */
|
|
+ async goBack(options) {
|
|
+ return await this._contentSession.send('Page.goBack', options);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {{frameId: string, url: string}} options
|
|
+ */
|
|
+ async goForward(options) {
|
|
+ return await this._contentSession.send('Page.goForward', options);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {{frameId: string, url: string}} options
|
|
+ */
|
|
+ async reload(options) {
|
|
+ return await this._contentSession.send('Page.reload', options);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {{frameId: String, objectId: String}} options
|
|
+ * @return {!Promise<*>}
|
|
+ */
|
|
+ async contentFrame(options) {
|
|
+ return await this._contentSession.send('Page.contentFrame', options);
|
|
+ }
|
|
+
|
|
+ async addScriptToEvaluateOnNewDocument(options) {
|
|
+ return await this._contentSession.send('Page.addScriptToEvaluateOnNewDocument', options);
|
|
+ }
|
|
+
|
|
+ async removeScriptToEvaluateOnNewDocument(options) {
|
|
+ return await this._contentSession.send('Page.removeScriptToEvaluateOnNewDocument', options);
|
|
+ }
|
|
+
|
|
+ async dispatchKeyEvent(options) {
|
|
+ return await this._contentSession.send('Page.dispatchKeyEvent', options);
|
|
+ }
|
|
+
|
|
+ async dispatchTouchEvent(options) {
|
|
+ return await this._contentSession.send('Page.dispatchTouchEvent', options);
|
|
+ }
|
|
+
|
|
+ async dispatchMouseEvent(options) {
|
|
+ return await this._contentSession.send('Page.dispatchMouseEvent', options);
|
|
+ }
|
|
+
|
|
+ async insertText(options) {
|
|
+ return await this._contentSession.send('Page.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();
|
|
+ }
|
|
+
|
|
+ async setInterceptFileChooserDialog(options) {
|
|
+ return await this._contentSession.send('Page.setInterceptFileChooserDialog', options);
|
|
+ }
|
|
+
|
|
+ async handleFileChooser(options) {
|
|
+ return await this._contentSession.send('Page.handleFileChooser', options);
|
|
+ }
|
|
+}
|
|
+
|
|
+class Dialog {
|
|
+ static createIfSupported(prompt) {
|
|
+ const type = prompt.args.promptType;
|
|
+ switch (type) {
|
|
+ case 'alert':
|
|
+ case 'prompt':
|
|
+ case 'confirm':
|
|
+ return new Dialog(prompt, type);
|
|
+ case 'confirmEx':
|
|
+ return new Dialog(prompt, 'beforeunload');
|
|
+ default:
|
|
+ return null;
|
|
+ };
|
|
+ }
|
|
+
|
|
+ constructor(prompt, type) {
|
|
+ this._id = helper.generateId();
|
|
+ this._type = type;
|
|
+ this._prompt = prompt;
|
|
+ }
|
|
+
|
|
+ id() {
|
|
+ return this._id;
|
|
+ }
|
|
+
|
|
+ message() {
|
|
+ return this._prompt.ui.infoBody.textContent;
|
|
+ }
|
|
+
|
|
+ type() {
|
|
+ return this._type;
|
|
+ }
|
|
+
|
|
+ prompt() {
|
|
+ return this._prompt;
|
|
+ }
|
|
+
|
|
+ dismiss() {
|
|
+ if (this._prompt.ui.button1)
|
|
+ this._prompt.ui.button1.click();
|
|
+ else
|
|
+ this._prompt.ui.button0.click();
|
|
+ }
|
|
+
|
|
+ defaultValue() {
|
|
+ return this._prompt.ui.loginTextbox.value;
|
|
+ }
|
|
+
|
|
+ accept(promptValue) {
|
|
+ if (typeof promptValue === 'string' && this._type === 'prompt')
|
|
+ this._prompt.ui.loginTextbox.value = promptValue;
|
|
+ this._prompt.ui.button0.click();
|
|
+ }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['PageHandler'];
|
|
+this.PageHandler = PageHandler;
|
|
diff --git a/testing/juggler/protocol/PrimitiveTypes.js b/testing/juggler/protocol/PrimitiveTypes.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..78b6601b91d0b7fcda61114e6846aa07f95a06fa
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/PrimitiveTypes.js
|
|
@@ -0,0 +1,143 @@
|
|
+const t = {};
|
|
+
|
|
+t.String = function(x, details = {}, path = ['<root>']) {
|
|
+ if (typeof x === 'string' || typeof x === 'String')
|
|
+ return true;
|
|
+ details.error = `Expected "${path.join('.')}" to be |string|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`;
|
|
+ return false;
|
|
+}
|
|
+
|
|
+t.Number = function(x, details = {}, path = ['<root>']) {
|
|
+ if (typeof x === 'number')
|
|
+ return true;
|
|
+ details.error = `Expected "${path.join('.')}" to be |number|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`;
|
|
+ return false;
|
|
+}
|
|
+
|
|
+t.Boolean = function(x, details = {}, path = ['<root>']) {
|
|
+ if (typeof x === 'boolean')
|
|
+ return true;
|
|
+ details.error = `Expected "${path.join('.')}" to be |boolean|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`;
|
|
+ return false;
|
|
+}
|
|
+
|
|
+t.Null = function(x, details = {}, path = ['<root>']) {
|
|
+ if (Object.is(x, null))
|
|
+ return true;
|
|
+ details.error = `Expected "${path.join('.')}" to be \`null\`; found \`${JSON.stringify(x)}\` instead.`;
|
|
+ return false;
|
|
+}
|
|
+
|
|
+t.Undefined = function(x, details = {}, path = ['<root>']) {
|
|
+ if (Object.is(x, undefined))
|
|
+ return true;
|
|
+ details.error = `Expected "${path.join('.')}" to be \`undefined\`; found \`${JSON.stringify(x)}\` instead.`;
|
|
+ return false;
|
|
+}
|
|
+
|
|
+t.Any = x => true,
|
|
+
|
|
+t.Enum = function(values) {
|
|
+ return function(x, details = {}, path = ['<root>']) {
|
|
+ if (values.indexOf(x) !== -1)
|
|
+ return true;
|
|
+ details.error = `Expected "${path.join('.')}" to be one of [${values.join(', ')}]; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`;
|
|
+ return false;
|
|
+ }
|
|
+}
|
|
+
|
|
+t.Nullable = function(scheme) {
|
|
+ return function(x, details = {}, path = ['<root>']) {
|
|
+ if (Object.is(x, null))
|
|
+ return true;
|
|
+ return checkScheme(scheme, x, details, path);
|
|
+ }
|
|
+}
|
|
+
|
|
+t.Optional = function(scheme) {
|
|
+ return function(x, details = {}, path = ['<root>']) {
|
|
+ if (Object.is(x, undefined))
|
|
+ return true;
|
|
+ return checkScheme(scheme, x, details, path);
|
|
+ }
|
|
+}
|
|
+
|
|
+t.Array = function(scheme) {
|
|
+ return function(x, details = {}, path = ['<root>']) {
|
|
+ if (!Array.isArray(x)) {
|
|
+ details.error = `Expected "${path.join('.')}" to be an array; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`;
|
|
+ return false;
|
|
+ }
|
|
+ const lastPathElement = path[path.length - 1];
|
|
+ for (let i = 0; i < x.length; ++i) {
|
|
+ path[path.length - 1] = lastPathElement + `[${i}]`;
|
|
+ if (!checkScheme(scheme, x[i], details, path))
|
|
+ return false;
|
|
+ }
|
|
+ path[path.length - 1] = lastPathElement;
|
|
+ return true;
|
|
+ }
|
|
+}
|
|
+
|
|
+t.Recursive = function(types, schemeName) {
|
|
+ return function(x, details = {}, path = ['<root>']) {
|
|
+ const scheme = types[schemeName];
|
|
+ return checkScheme(scheme, x, details, path);
|
|
+ }
|
|
+}
|
|
+
|
|
+function beauty(path, obj) {
|
|
+ if (path.length === 1)
|
|
+ return `object ${JSON.stringify(obj, null, 2)}`;
|
|
+ return `property "${path.join('.')}" - ${JSON.stringify(obj, null, 2)}`;
|
|
+}
|
|
+
|
|
+function checkScheme(scheme, x, details = {}, path = ['<root>']) {
|
|
+ if (!scheme)
|
|
+ throw new Error(`ILLDEFINED SCHEME: ${path.join('.')}`);
|
|
+ if (typeof scheme === 'object') {
|
|
+ if (!x) {
|
|
+ details.error = `Object "${path.join('.')}" is undefined, but has some scheme`;
|
|
+ return false;
|
|
+ }
|
|
+ for (const [propertyName, aScheme] of Object.entries(scheme)) {
|
|
+ path.push(propertyName);
|
|
+ const result = checkScheme(aScheme, x[propertyName], details, path);
|
|
+ path.pop();
|
|
+ if (!result)
|
|
+ return false;
|
|
+ }
|
|
+ for (const propertyName of Object.keys(x)) {
|
|
+ if (!scheme[propertyName]) {
|
|
+ path.push(propertyName);
|
|
+ details.error = `Found ${beauty(path, x[propertyName])} which is not described in this scheme`;
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ return scheme(x, details, path);
|
|
+}
|
|
+
|
|
+/*
|
|
+
|
|
+function test(scheme, obj) {
|
|
+ const details = {};
|
|
+ if (!checkScheme(scheme, obj, details)) {
|
|
+ dump(`FAILED: ${JSON.stringify(obj)}
|
|
+ details.error: ${details.error}
|
|
+ `);
|
|
+ } else {
|
|
+ dump(`SUCCESS: ${JSON.stringify(obj)}
|
|
+`);
|
|
+ }
|
|
+}
|
|
+
|
|
+test(t.Array(t.String), ['a', 'b', 2, 'c']);
|
|
+test(t.Either(t.String, t.Number), {});
|
|
+
|
|
+*/
|
|
+
|
|
+this.t = t;
|
|
+this.checkScheme = checkScheme;
|
|
+this.EXPORTED_SYMBOLS = ['t', 'checkScheme'];
|
|
diff --git a/testing/juggler/protocol/Protocol.js b/testing/juggler/protocol/Protocol.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..700571aeed4509ad5429adb35d9fd4c2d7cd8113
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/Protocol.js
|
|
@@ -0,0 +1,668 @@
|
|
+const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js');
|
|
+
|
|
+// Protocol-specific types.
|
|
+const types = {};
|
|
+
|
|
+types.TargetInfo = {
|
|
+ type: t.Enum(['page', 'browser']),
|
|
+ targetId: t.String,
|
|
+ browserContextId: t.Optional(t.String),
|
|
+ url: t.String,
|
|
+ // PageId of parent tab, if any.
|
|
+ openerId: t.Optional(t.String),
|
|
+};
|
|
+
|
|
+types.DOMPoint = {
|
|
+ x: t.Number,
|
|
+ y: t.Number,
|
|
+};
|
|
+
|
|
+types.DOMQuad = {
|
|
+ p1: types.DOMPoint,
|
|
+ p2: types.DOMPoint,
|
|
+ p3: types.DOMPoint,
|
|
+ p4: types.DOMPoint,
|
|
+};
|
|
+
|
|
+types.TouchPoint = {
|
|
+ x: t.Number,
|
|
+ y: t.Number,
|
|
+ radiusX: t.Optional(t.Number),
|
|
+ radiusY: t.Optional(t.Number),
|
|
+ rotationAngle: t.Optional(t.Number),
|
|
+ force: t.Optional(t.Number),
|
|
+};
|
|
+
|
|
+types.RemoteObject = {
|
|
+ type: t.Optional(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.Optional(t.String),
|
|
+ unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])),
|
|
+ value: t.Any
|
|
+};
|
|
+
|
|
+types.AXTree = {
|
|
+ role: t.String,
|
|
+ name: t.String,
|
|
+ children: t.Optional(t.Array(t.Recursive(types, 'AXTree'))),
|
|
+
|
|
+ selected: t.Optional(t.Boolean),
|
|
+ focused: t.Optional(t.Boolean),
|
|
+ pressed: t.Optional(t.Boolean),
|
|
+ focusable: t.Optional(t.Boolean),
|
|
+ haspopup: t.Optional(t.Boolean),
|
|
+ required: t.Optional(t.Boolean),
|
|
+ invalid: t.Optional(t.Boolean),
|
|
+ modal: t.Optional(t.Boolean),
|
|
+ editable: t.Optional(t.Boolean),
|
|
+ busy: t.Optional(t.Boolean),
|
|
+ multiline: t.Optional(t.Boolean),
|
|
+ readonly: t.Optional(t.Boolean),
|
|
+ checked: t.Optional(t.Enum(['mixed', true])),
|
|
+ expanded: t.Optional(t.Boolean),
|
|
+ disabled: t.Optional(t.Boolean),
|
|
+ multiselectable: t.Optional(t.Boolean),
|
|
+
|
|
+ value: t.Optional(t.String),
|
|
+ description: t.Optional(t.String),
|
|
+
|
|
+ value: t.Optional(t.String),
|
|
+ roledescription: t.Optional(t.String),
|
|
+ valuetext: t.Optional(t.String),
|
|
+ orientation: t.Optional(t.String),
|
|
+ autocomplete: t.Optional(t.String),
|
|
+ keyshortcuts: t.Optional(t.String),
|
|
+
|
|
+ level: t.Optional(t.Number),
|
|
+
|
|
+ tag: t.Optional(t.String),
|
|
+}
|
|
+
|
|
+const Browser = {
|
|
+ targets: ['browser'],
|
|
+
|
|
+ events: {},
|
|
+
|
|
+ methods: {
|
|
+ 'close': {},
|
|
+ 'getInfo': {
|
|
+ returns: {
|
|
+ userAgent: t.String,
|
|
+ version: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'setIgnoreHTTPSErrors': {
|
|
+ params: {
|
|
+ enabled: t.Boolean,
|
|
+ },
|
|
+ },
|
|
+ 'grantPermissions': {
|
|
+ params: {
|
|
+ origin: t.String,
|
|
+ browserContextId: t.Optional(t.String),
|
|
+ permissions: t.Array(t.Enum([
|
|
+ 'geo', 'microphone', 'camera', 'desktop-notifications'
|
|
+ ])),
|
|
+ },
|
|
+ },
|
|
+ 'resetPermissions': {
|
|
+ params: {
|
|
+ browserContextId: t.Optional(t.String),
|
|
+ }
|
|
+ },
|
|
+ 'setCookies': {
|
|
+ params: {
|
|
+ browserContextId: t.Optional(t.String),
|
|
+ cookies: t.Array({
|
|
+ name: t.String,
|
|
+ value: t.String,
|
|
+ url: t.Optional(t.String),
|
|
+ domain: t.Optional(t.String),
|
|
+ path: t.Optional(t.String),
|
|
+ secure: t.Optional(t.Boolean),
|
|
+ httpOnly: t.Optional(t.Boolean),
|
|
+ sameSite: t.Optional(t.Enum(['Strict', 'Lax', 'None'])),
|
|
+ expires: t.Optional(t.Number),
|
|
+ }),
|
|
+ }
|
|
+ },
|
|
+ 'clearCookies': {
|
|
+ params: {
|
|
+ browserContextId: t.Optional(t.String),
|
|
+ }
|
|
+ },
|
|
+ 'getCookies': {
|
|
+ params: {
|
|
+ browserContextId: t.Optional(t.String)
|
|
+ },
|
|
+ returns: {
|
|
+ cookies: t.Array({
|
|
+ name: t.String,
|
|
+ domain: t.String,
|
|
+ path: t.String,
|
|
+ value: t.String,
|
|
+ expires: t.Number,
|
|
+ size: t.Number,
|
|
+ httpOnly: t.Boolean,
|
|
+ secure: t.Boolean,
|
|
+ session: t.Boolean,
|
|
+ sameSite: t.Enum(['Strict', 'Lax', 'None']),
|
|
+ }),
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+const Target = {
|
|
+ targets: ['browser'],
|
|
+
|
|
+ events: {
|
|
+ 'attachedToTarget': {
|
|
+ sessionId: t.String,
|
|
+ targetInfo: types.TargetInfo,
|
|
+ },
|
|
+ 'detachedFromTarget': {
|
|
+ sessionId: t.String,
|
|
+ },
|
|
+ 'targetCreated': types.TargetInfo,
|
|
+ 'targetDestroyed': types.TargetInfo,
|
|
+ 'targetInfoChanged': types.TargetInfo,
|
|
+ },
|
|
+
|
|
+ methods: {
|
|
+ // Start emitting tagOpened/tabClosed events
|
|
+ 'enable': {},
|
|
+ 'attachToTarget': {
|
|
+ params: {
|
|
+ targetId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ sessionId: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'newPage': {
|
|
+ params: {
|
|
+ browserContextId: t.Optional(t.String),
|
|
+ },
|
|
+ returns: {
|
|
+ targetId: t.String,
|
|
+ }
|
|
+ },
|
|
+ 'createBrowserContext': {
|
|
+ returns: {
|
|
+ browserContextId: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'removeBrowserContext': {
|
|
+ params: {
|
|
+ browserContextId: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'getBrowserContexts': {
|
|
+ returns: {
|
|
+ browserContextIds: t.Array(t.String),
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+const Network = {
|
|
+ targets: ['page'],
|
|
+ events: {
|
|
+ 'requestWillBeSent': {
|
|
+ // frameId may be absent for redirected requests.
|
|
+ frameId: t.Optional(t.String),
|
|
+ requestId: t.String,
|
|
+ // RequestID of redirected request.
|
|
+ redirectedFrom: t.Optional(t.String),
|
|
+ postData: t.Optional(t.String),
|
|
+ headers: t.Array({
|
|
+ name: t.String,
|
|
+ value: t.String,
|
|
+ }),
|
|
+ suspended: t.Optional(t.Boolean),
|
|
+ url: t.String,
|
|
+ method: t.String,
|
|
+ navigationId: t.Optional(t.String),
|
|
+ cause: t.String,
|
|
+ },
|
|
+ 'responseReceived': {
|
|
+ securityDetails: t.Nullable({
|
|
+ protocol: t.String,
|
|
+ subjectName: t.String,
|
|
+ issuer: t.String,
|
|
+ validFrom: t.Number,
|
|
+ validTo: t.Number,
|
|
+ }),
|
|
+ requestId: t.String,
|
|
+ fromCache: t.Boolean,
|
|
+ remoteIPAddress: t.Optional(t.String),
|
|
+ remotePort: t.Optional(t.Number),
|
|
+ status: t.Number,
|
|
+ statusText: t.String,
|
|
+ headers: t.Array({
|
|
+ name: t.String,
|
|
+ value: t.String,
|
|
+ }),
|
|
+ },
|
|
+ 'requestFinished': {
|
|
+ requestId: t.String,
|
|
+ },
|
|
+ 'requestFailed': {
|
|
+ requestId: t.String,
|
|
+ errorCode: t.String,
|
|
+ },
|
|
+ },
|
|
+ methods: {
|
|
+ 'enable': {},
|
|
+ 'setRequestInterception': {
|
|
+ params: {
|
|
+ enabled: t.Boolean,
|
|
+ },
|
|
+ },
|
|
+ 'setExtraHTTPHeaders': {
|
|
+ params: {
|
|
+ headers: t.Array({
|
|
+ name: t.String,
|
|
+ value: t.String,
|
|
+ }),
|
|
+ },
|
|
+ },
|
|
+ 'abortSuspendedRequest': {
|
|
+ params: {
|
|
+ requestId: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'resumeSuspendedRequest': {
|
|
+ params: {
|
|
+ requestId: t.String,
|
|
+ headers: t.Optional(t.Array({
|
|
+ name: t.String,
|
|
+ value: t.String,
|
|
+ })),
|
|
+ },
|
|
+ },
|
|
+ 'getResponseBody': {
|
|
+ params: {
|
|
+ requestId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ base64body: t.String,
|
|
+ evicted: t.Optional(t.Boolean),
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+const Runtime = {
|
|
+ targets: ['page'],
|
|
+ events: {
|
|
+ 'executionContextCreated': {
|
|
+ executionContextId: t.String,
|
|
+ auxData: t.Any,
|
|
+ },
|
|
+ 'executionContextDestroyed': {
|
|
+ executionContextId: t.String,
|
|
+ },
|
|
+ 'console': {
|
|
+ executionContextId: t.String,
|
|
+ args: t.Array(types.RemoteObject),
|
|
+ type: t.String,
|
|
+ location: {
|
|
+ columnNumber: t.Number,
|
|
+ lineNumber: t.Number,
|
|
+ url: t.String,
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+ methods: {
|
|
+ 'enable': {
|
|
+ params: {},
|
|
+ },
|
|
+ 'evaluate': {
|
|
+ params: {
|
|
+ // Pass frameId here.
|
|
+ executionContextId: t.String,
|
|
+ expression: t.String,
|
|
+ returnByValue: t.Optional(t.Boolean),
|
|
+ },
|
|
+
|
|
+ returns: {
|
|
+ result: t.Optional(types.RemoteObject),
|
|
+ exceptionDetails: t.Optional({
|
|
+ text: t.Optional(t.String),
|
|
+ stack: t.Optional(t.String),
|
|
+ value: t.Optional(t.Any),
|
|
+ }),
|
|
+ }
|
|
+ },
|
|
+ 'callFunction': {
|
|
+ params: {
|
|
+ // Pass frameId here.
|
|
+ executionContextId: t.String,
|
|
+ functionDeclaration: t.String,
|
|
+ returnByValue: t.Optional(t.Boolean),
|
|
+ args: t.Array({
|
|
+ objectId: t.Optional(t.String),
|
|
+ unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])),
|
|
+ value: t.Any,
|
|
+ }),
|
|
+ },
|
|
+
|
|
+ returns: {
|
|
+ result: t.Optional(types.RemoteObject),
|
|
+ exceptionDetails: t.Optional({
|
|
+ text: t.Optional(t.String),
|
|
+ stack: t.Optional(t.String),
|
|
+ value: t.Optional(t.Any),
|
|
+ }),
|
|
+ }
|
|
+ },
|
|
+ 'disposeObject': {
|
|
+ params: {
|
|
+ executionContextId: t.String,
|
|
+ objectId: t.String,
|
|
+ },
|
|
+ },
|
|
+
|
|
+ 'getObjectProperties': {
|
|
+ params: {
|
|
+ executionContextId: t.String,
|
|
+ objectId: t.String,
|
|
+ },
|
|
+
|
|
+ returns: {
|
|
+ properties: t.Array({
|
|
+ name: t.String,
|
|
+ value: types.RemoteObject,
|
|
+ }),
|
|
+ }
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+const Page = {
|
|
+ targets: ['page'],
|
|
+ events: {
|
|
+ 'eventFired': {
|
|
+ frameId: t.String,
|
|
+ name: t.Enum(['load', 'DOMContentLoaded']),
|
|
+ },
|
|
+ 'uncaughtError': {
|
|
+ frameId: t.String,
|
|
+ message: t.String,
|
|
+ stack: t.String,
|
|
+ },
|
|
+ 'frameAttached': {
|
|
+ frameId: t.String,
|
|
+ parentFrameId: t.Optional(t.String),
|
|
+ },
|
|
+ 'frameDetached': {
|
|
+ frameId: t.String,
|
|
+ },
|
|
+ 'navigationStarted': {
|
|
+ frameId: t.String,
|
|
+ navigationId: t.String,
|
|
+ url: t.String,
|
|
+ },
|
|
+ 'navigationCommitted': {
|
|
+ frameId: t.String,
|
|
+ navigationId: t.String,
|
|
+ url: t.String,
|
|
+ // frame.id or frame.name
|
|
+ name: t.String,
|
|
+ },
|
|
+ 'navigationAborted': {
|
|
+ frameId: t.String,
|
|
+ navigationId: t.String,
|
|
+ errorText: t.String,
|
|
+ },
|
|
+ 'sameDocumentNavigation': {
|
|
+ frameId: t.String,
|
|
+ url: t.String,
|
|
+ },
|
|
+ 'dialogOpened': {
|
|
+ dialogId: t.String,
|
|
+ type: t.Enum(['prompt', 'alert', 'confirm', 'beforeunload']),
|
|
+ message: t.String,
|
|
+ defaultValue: t.Optional(t.String),
|
|
+ },
|
|
+ 'dialogClosed': {
|
|
+ dialogId: t.String,
|
|
+ },
|
|
+ 'bindingCalled': {
|
|
+ executionContextId: t.String,
|
|
+ name: t.String,
|
|
+ payload: t.Any,
|
|
+ },
|
|
+ 'fileChooserOpened': {
|
|
+ executionContextId: t.String,
|
|
+ element: types.RemoteObject
|
|
+ },
|
|
+ },
|
|
+
|
|
+ methods: {
|
|
+ 'enable': {
|
|
+ params: {},
|
|
+ },
|
|
+ 'close': {
|
|
+ params: {
|
|
+ runBeforeUnload: t.Optional(t.Boolean),
|
|
+ },
|
|
+ },
|
|
+ 'setFileInputFiles': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ objectId: t.String,
|
|
+ files: t.Array(t.String),
|
|
+ },
|
|
+ },
|
|
+ 'addBinding': {
|
|
+ params: {
|
|
+ name: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'setViewport': {
|
|
+ params: {
|
|
+ viewport: t.Nullable({
|
|
+ width: t.Number,
|
|
+ height: t.Number,
|
|
+ deviceScaleFactor: t.Number,
|
|
+ isMobile: t.Boolean,
|
|
+ hasTouch: t.Boolean,
|
|
+ isLandscape: t.Boolean,
|
|
+ }),
|
|
+ },
|
|
+ },
|
|
+ 'setUserAgent': {
|
|
+ params: {
|
|
+ userAgent: t.Nullable(t.String),
|
|
+ },
|
|
+ },
|
|
+ 'setEmulatedMedia': {
|
|
+ params: {
|
|
+ type: t.Optional(t.Enum(['screen', 'print', ''])),
|
|
+ colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])),
|
|
+ },
|
|
+ },
|
|
+ 'setBypassCSP': {
|
|
+ params: {
|
|
+ enabled: t.Boolean
|
|
+ }
|
|
+ },
|
|
+ 'setCacheDisabled': {
|
|
+ params: {
|
|
+ cacheDisabled: t.Boolean,
|
|
+ },
|
|
+ },
|
|
+ 'setJavascriptEnabled': {
|
|
+ params: {
|
|
+ enabled: t.Boolean,
|
|
+ },
|
|
+ },
|
|
+ 'contentFrame': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ objectId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ frameId: t.Nullable(t.String),
|
|
+ },
|
|
+ },
|
|
+ 'addScriptToEvaluateOnNewDocument': {
|
|
+ params: {
|
|
+ script: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ scriptId: t.String,
|
|
+ }
|
|
+ },
|
|
+ 'removeScriptToEvaluateOnNewDocument': {
|
|
+ params: {
|
|
+ scriptId: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'navigate': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ url: t.String,
|
|
+ referer: t.Optional(t.String),
|
|
+ },
|
|
+ returns: {
|
|
+ navigationId: t.Nullable(t.String),
|
|
+ navigationURL: t.Nullable(t.String),
|
|
+ }
|
|
+ },
|
|
+ 'goBack': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ navigationId: t.Nullable(t.String),
|
|
+ navigationURL: t.Nullable(t.String),
|
|
+ }
|
|
+ },
|
|
+ 'goForward': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ navigationId: t.Nullable(t.String),
|
|
+ navigationURL: t.Nullable(t.String),
|
|
+ }
|
|
+ },
|
|
+ 'reload': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ navigationId: t.String,
|
|
+ navigationURL: t.String,
|
|
+ }
|
|
+ },
|
|
+ 'getBoundingBox': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ objectId: t.String,
|
|
+ },
|
|
+ returns: t.Nullable({
|
|
+ x: t.Number,
|
|
+ y: t.Number,
|
|
+ width: t.Number,
|
|
+ height: t.Number,
|
|
+ }),
|
|
+ },
|
|
+ 'screenshot': {
|
|
+ params: {
|
|
+ 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,
|
|
+ }
|
|
+ },
|
|
+ 'getContentQuads': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ objectId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ quads: t.Array(types.DOMQuad),
|
|
+ },
|
|
+ },
|
|
+ 'dispatchKeyEvent': {
|
|
+ params: {
|
|
+ type: t.String,
|
|
+ key: t.String,
|
|
+ keyCode: t.Number,
|
|
+ location: t.Number,
|
|
+ code: t.String,
|
|
+ repeat: t.Boolean,
|
|
+ }
|
|
+ },
|
|
+ 'dispatchTouchEvent': {
|
|
+ params: {
|
|
+ type: t.Enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']),
|
|
+ touchPoints: t.Array(types.TouchPoint),
|
|
+ modifiers: t.Number,
|
|
+ },
|
|
+ returns: {
|
|
+ defaultPrevented: t.Boolean,
|
|
+ }
|
|
+ },
|
|
+ 'dispatchMouseEvent': {
|
|
+ params: {
|
|
+ type: t.String,
|
|
+ button: t.Number,
|
|
+ x: t.Number,
|
|
+ y: t.Number,
|
|
+ modifiers: t.Number,
|
|
+ clickCount: t.Optional(t.Number),
|
|
+ buttons: t.Number,
|
|
+ }
|
|
+ },
|
|
+ 'insertText': {
|
|
+ params: {
|
|
+ text: t.String,
|
|
+ }
|
|
+ },
|
|
+ 'handleDialog': {
|
|
+ params: {
|
|
+ dialogId: t.String,
|
|
+ accept: t.Boolean,
|
|
+ promptText: t.Optional(t.String),
|
|
+ },
|
|
+ },
|
|
+ 'setInterceptFileChooserDialog': {
|
|
+ params: {
|
|
+ enabled: t.Boolean,
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+
|
|
+const Accessibility = {
|
|
+ targets: ['page'],
|
|
+ events: {},
|
|
+ methods: {
|
|
+ 'getFullAXTree': {
|
|
+ params: {},
|
|
+ returns: {
|
|
+ tree:types.AXTree
|
|
+ },
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+this.protocol = {
|
|
+ domains: {Browser, Target, Page, Runtime, Network, Accessibility},
|
|
+};
|
|
+this.checkScheme = checkScheme;
|
|
+this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];
|
|
diff --git a/testing/juggler/protocol/RuntimeHandler.js b/testing/juggler/protocol/RuntimeHandler.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..0026e8ff58ef6268f4c63783d0ff68ff355b1e72
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/RuntimeHandler.js
|
|
@@ -0,0 +1,41 @@
|
|
+"use strict";
|
|
+
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+
|
|
+const Cc = Components.classes;
|
|
+const Ci = Components.interfaces;
|
|
+const Cu = Components.utils;
|
|
+const helper = new Helper();
|
|
+
|
|
+class RuntimeHandler {
|
|
+ constructor(chromeSession, contentSession) {
|
|
+ this._chromeSession = chromeSession;
|
|
+ this._contentSession = contentSession;
|
|
+ }
|
|
+
|
|
+ async enable(options) {
|
|
+ return await this._contentSession.send('Runtime.enable', options);
|
|
+ }
|
|
+
|
|
+ async evaluate(options) {
|
|
+ return await this._contentSession.send('Runtime.evaluate', options);
|
|
+ }
|
|
+
|
|
+ async callFunction(options) {
|
|
+ return await this._contentSession.send('Runtime.callFunction', options);
|
|
+ }
|
|
+
|
|
+ async getObjectProperties(options) {
|
|
+ return await this._contentSession.send('Runtime.getObjectProperties', options);
|
|
+ }
|
|
+
|
|
+ async disposeObject(options) {
|
|
+ return await this._contentSession.send('Runtime.disposeObject', options);
|
|
+ }
|
|
+
|
|
+ dispose() {}
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['RuntimeHandler'];
|
|
+this.RuntimeHandler = RuntimeHandler;
|
|
diff --git a/testing/juggler/protocol/TargetHandler.js b/testing/juggler/protocol/TargetHandler.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..4ea36eeba75864ddb09d4a9c0814f18ccfdd7bde
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/TargetHandler.js
|
|
@@ -0,0 +1,75 @@
|
|
+"use strict";
|
|
+
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
|
|
+const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js");
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const helper = new Helper();
|
|
+
|
|
+class TargetHandler {
|
|
+ /**
|
|
+ * @param {ChromeSession} session
|
|
+ */
|
|
+ constructor(session) {
|
|
+ this._session = session;
|
|
+ this._contextManager = BrowserContextManager.instance();
|
|
+ this._targetRegistry = TargetRegistry.instance();
|
|
+ this._enabled = false;
|
|
+ this._eventListeners = [];
|
|
+ }
|
|
+
|
|
+ async attachToTarget({targetId}) {
|
|
+ const sessionId = await this._session.dispatcher().createSession(targetId);
|
|
+ return {sessionId};
|
|
+ }
|
|
+
|
|
+ async createBrowserContext() {
|
|
+ return {browserContextId: this._contextManager.createBrowserContext()};
|
|
+ }
|
|
+
|
|
+ async removeBrowserContext({browserContextId}) {
|
|
+ this._contextManager.removeBrowserContext(browserContextId);
|
|
+ }
|
|
+
|
|
+ async getBrowserContexts() {
|
|
+ return {browserContextIds: this._contextManager.getBrowserContexts()};
|
|
+ }
|
|
+
|
|
+ async enable() {
|
|
+ if (this._enabled)
|
|
+ return;
|
|
+ this._enabled = true;
|
|
+ for (const targetInfo of this._targetRegistry.targetInfos())
|
|
+ this._onTargetCreated(targetInfo);
|
|
+
|
|
+ this._eventListeners = [
|
|
+ helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)),
|
|
+ helper.on(this._targetRegistry, TargetRegistry.Events.TargetChanged, this._onTargetChanged.bind(this)),
|
|
+ helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+
|
|
+ _onTargetCreated(targetInfo) {
|
|
+ this._session.emitEvent('Target.targetCreated', targetInfo);
|
|
+ }
|
|
+
|
|
+ _onTargetChanged(targetInfo) {
|
|
+ this._session.emitEvent('Target.targetInfoChanged', targetInfo);
|
|
+ }
|
|
+
|
|
+ _onTargetDestroyed(targetInfo) {
|
|
+ this._session.emitEvent('Target.targetDestroyed', targetInfo);
|
|
+ }
|
|
+
|
|
+ async newPage({browserContextId}) {
|
|
+ const targetId = await this._targetRegistry.newPage({browserContextId});
|
|
+ return {targetId};
|
|
+ }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['TargetHandler'];
|
|
+this.TargetHandler = TargetHandler;
|
|
diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
|
|
index 9aea55ddf7739d4ec129f9c48ebcc0736a7b7365..188a0f28b8e12a61e1fe89ba86350a844b809625 100644
|
|
--- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
|
|
+++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
|
|
@@ -179,8 +179,16 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress* aWebProgress,
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
-nsBrowserStatusFilter::OnProgressChange(nsIWebProgress* aWebProgress,
|
|
- nsIRequest* aRequest,
|
|
+nsBrowserStatusFilter::OnFrameLocationChange(nsIWebProgress *aWebProgress,
|
|
+ nsIRequest *aRequest,
|
|
+ nsIURI *aLocation,
|
|
+ uint32_t aFlags) {
|
|
+ return NS_OK;
|
|
+}
|
|
+
|
|
+NS_IMETHODIMP
|
|
+nsBrowserStatusFilter::OnProgressChange(nsIWebProgress *aWebProgress,
|
|
+ nsIRequest *aRequest,
|
|
int32_t aCurSelfProgress,
|
|
int32_t aMaxSelfProgress,
|
|
int32_t aCurTotalProgress,
|
|
diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild
|
|
index bec85cc1eb7317221e20afa7ca0643b7bfba6b1a..f0691908886e549cc583714cb53383b027d25044 100644
|
|
--- a/toolkit/toolkit.mozbuild
|
|
+++ b/toolkit/toolkit.mozbuild
|
|
@@ -169,6 +169,7 @@ if CONFIG['ENABLE_MARIONETTE']:
|
|
DIRS += [
|
|
'/testing/firefox-ui',
|
|
'/testing/marionette',
|
|
+ '/testing/juggler',
|
|
'/toolkit/components/telemetry/tests/marionette',
|
|
]
|
|
|
|
diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp
|
|
index 92cb5f3cf6dad25b375f942428f5403a06842d7c..bfcb01b081566cb9aa8ecd80a1f67497778d2519 100644
|
|
--- a/uriloader/base/nsDocLoader.cpp
|
|
+++ b/uriloader/base/nsDocLoader.cpp
|
|
@@ -763,6 +763,13 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) {
|
|
("DocLoader:%p: Firing load event for document.open\n",
|
|
this));
|
|
|
|
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
|
+ if (os) {
|
|
+ nsIPrincipal* principal = doc->NodePrincipal();
|
|
+ if (!nsContentUtils::IsSystemPrincipal(principal))
|
|
+ os->NotifyObservers(ToSupports(doc), "juggler-document-open-loaded", nullptr);
|
|
+ }
|
|
+
|
|
// This is a very cut-down version of
|
|
// nsDocumentViewer::LoadComplete that doesn't do various things
|
|
// that are not relevant here because this wasn't an actual
|
|
@@ -1370,6 +1377,24 @@ void 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, nsresult aStatus,
|
|
const char16_t* aMessage) {
|
|
diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h
|
|
index 14d9d1052ef2a44ba2066572de306034d9f3a8c6..c9b2f0676e3010967b784eccb6f29ec6f50d926a 100644
|
|
--- a/uriloader/base/nsDocLoader.h
|
|
+++ b/uriloader/base/nsDocLoader.h
|
|
@@ -212,6 +212,11 @@ class nsDocLoader : public nsIDocumentLoader,
|
|
void FireOnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
|
|
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, bool aSameURI);
|
|
|
|
diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl
|
|
index b0cde5026dc7c414e8f20300ac2b7d735dbd846e..09ebb0ef6799cf6a74fe529d4d000c6bed2c9497 100644
|
|
--- a/uriloader/base/nsIWebProgress.idl
|
|
+++ b/uriloader/base/nsIWebProgress.idl
|
|
@@ -87,6 +87,10 @@ 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;
|
|
@@ -94,11 +98,12 @@ interface nsIWebProgress : nsISupports
|
|
const unsigned long NOTIFY_LOCATION = 0x00000080;
|
|
const unsigned long NOTIFY_REFRESH = 0x00000100;
|
|
const unsigned long NOTIFY_CONTENT_BLOCKING = 0x00000200;
|
|
+ const unsigned long NOTIFY_FRAME_LOCATION = 0x00000400;
|
|
|
|
/**
|
|
* This flag enables all notifications.
|
|
*/
|
|
- const unsigned long NOTIFY_ALL = 0x000003ff;
|
|
+ const unsigned long NOTIFY_ALL = 0x000007ff;
|
|
|
|
/**
|
|
* Registers a listener to receive web progress events.
|
|
diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl
|
|
index 87701f8d2cfee8bd84acd28c62b3be4989c9474c..ae1aa85c019cb21d4f7e79c35e8afe72709468a1 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);
|
|
};
|