mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00

Review URL: 4753d0121f
This patch:
- Moves Workers to FrameTree
- Introduces WorkerData in PageAgent that proxies runtime agent
in worker to browser process
- Introduces WorkerHandler in PageHandler that handles communicates
with runtime agent in worker and handles Juggler protocol
As part of this patch, SimpleChannel no longer manages lifetime of
any of its handlers.
6348 lines
218 KiB
Diff
6348 lines
218 KiB
Diff
diff --git a/accessible/base/NotificationController.h b/accessible/base/NotificationController.h
|
|
index c6aa1cf44c8ba339704a18ebe92fe5a7751e52f5..cfe64bdda54d49ee5b11b2368a2f9856cc9ea3cf 100644
|
|
--- a/accessible/base/NotificationController.h
|
|
+++ b/accessible/base/NotificationController.h
|
|
@@ -270,6 +270,8 @@ class NotificationController final : public EventQueue,
|
|
}
|
|
#endif
|
|
|
|
+ bool IsUpdatePendingForJugglerAccessibility() { return IsUpdatePending(); }
|
|
+
|
|
protected:
|
|
virtual ~NotificationController();
|
|
|
|
diff --git a/accessible/interfaces/nsIAccessibleDocument.idl b/accessible/interfaces/nsIAccessibleDocument.idl
|
|
index a91df31c96afda66f478a5a38eaa4352039c2a0b..ee777c1746284027fb3aa2f1686f8082af9d89ee 100644
|
|
--- a/accessible/interfaces/nsIAccessibleDocument.idl
|
|
+++ b/accessible/interfaces/nsIAccessibleDocument.idl
|
|
@@ -72,4 +72,9 @@ interface nsIAccessibleDocument : nsISupports
|
|
* Return the child document accessible at the given index.
|
|
*/
|
|
nsIAccessibleDocument getChildDocumentAt(in unsigned long index);
|
|
+
|
|
+ /**
|
|
+ * Return whether it is updating.
|
|
+ */
|
|
+ readonly attribute boolean isUpdatePendingForJugglerAccessibility;
|
|
};
|
|
diff --git a/accessible/xpcom/xpcAccessibleDocument.cpp b/accessible/xpcom/xpcAccessibleDocument.cpp
|
|
index e3dbe73f22252f11080c3f266b2309f842eba9dc..87f50fe3df7cc8f9bc26dabd5ee571cae270912a 100644
|
|
--- a/accessible/xpcom/xpcAccessibleDocument.cpp
|
|
+++ b/accessible/xpcom/xpcAccessibleDocument.cpp
|
|
@@ -143,6 +143,15 @@ xpcAccessibleDocument::GetVirtualCursor(nsIAccessiblePivot** aVirtualCursor) {
|
|
return NS_OK;
|
|
}
|
|
|
|
+
|
|
+NS_IMETHODIMP
|
|
+xpcAccessibleDocument::GetIsUpdatePendingForJugglerAccessibility(bool* updating) {
|
|
+ NS_ENSURE_ARG_POINTER(updating);
|
|
+ *updating = Intl()->Controller()->IsUpdatePendingForJugglerAccessibility();
|
|
+ return NS_OK;
|
|
+}
|
|
+
|
|
+
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// xpcAccessibleDocument
|
|
|
|
diff --git a/accessible/xpcom/xpcAccessibleDocument.h b/accessible/xpcom/xpcAccessibleDocument.h
|
|
index f042cc1081850ac60e329b70b5569f8b97d4e4dc..65bcff9b41b9471ef1427e3ea330481c194409bc 100644
|
|
--- a/accessible/xpcom/xpcAccessibleDocument.h
|
|
+++ b/accessible/xpcom/xpcAccessibleDocument.h
|
|
@@ -48,6 +48,8 @@ class xpcAccessibleDocument : public xpcAccessibleHyperText,
|
|
nsIAccessibleDocument** aDocument) final;
|
|
NS_IMETHOD GetVirtualCursor(nsIAccessiblePivot** aVirtualCursor) final;
|
|
|
|
+ NS_IMETHOD GetIsUpdatePendingForJugglerAccessibility(bool* aUpdating) final;
|
|
+
|
|
/**
|
|
* Return XPCOM wrapper for the internal accessible.
|
|
*/
|
|
diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn
|
|
index cf0ae812a9f9741128fac124db03fb158ca54c30..7a0657ae0784e13929daf301549151236f1e53c1 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 7054749357ec13f175be8022852b42fcfeda9134..c9064880ecf7e70290c6a84bfc209e084aa37ddf 100644
|
|
--- a/browser/installer/package-manifest.in
|
|
+++ b/browser/installer/package-manifest.in
|
|
@@ -211,6 +211,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/devtools/server/socket/websocket-server.js b/devtools/server/socket/websocket-server.js
|
|
index 040c7b124dec6bb254563bbe74fe50012cb077a3..b4e6b8132786af70e8ad0dce88b67c2835307f88 100644
|
|
--- a/devtools/server/socket/websocket-server.js
|
|
+++ b/devtools/server/socket/websocket-server.js
|
|
@@ -133,13 +133,12 @@ function writeHttpResponse(output, response) {
|
|
* Process the WebSocket handshake headers and return the key to be sent in
|
|
* Sec-WebSocket-Accept response header.
|
|
*/
|
|
-function processRequest({ requestLine, headers }) {
|
|
+function processRequest({ requestLine, headers }, expectedPath) {
|
|
const [method, path] = requestLine.split(" ");
|
|
if (method !== "GET") {
|
|
throw new Error("The handshake request must use GET method");
|
|
}
|
|
-
|
|
- if (path !== "/") {
|
|
+ if (path !== expectedPath) {
|
|
throw new Error("The handshake request has unknown path");
|
|
}
|
|
|
|
@@ -189,13 +188,13 @@ function computeKey(key) {
|
|
/**
|
|
* Perform the server part of a WebSocket opening handshake on an incoming connection.
|
|
*/
|
|
-const serverHandshake = async function(input, output) {
|
|
+const serverHandshake = async function(input, output, expectedPath) {
|
|
// Read the request
|
|
const request = await readHttpRequest(input);
|
|
|
|
try {
|
|
// Check and extract info from the request
|
|
- const { acceptKey } = processRequest(request);
|
|
+ const { acceptKey } = processRequest(request, expectedPath);
|
|
|
|
// Send response headers
|
|
await writeHttpResponse(output, [
|
|
@@ -217,8 +216,8 @@ const serverHandshake = async function(input, output) {
|
|
* Performs the WebSocket handshake and waits for the WebSocket to open.
|
|
* Returns Promise with a WebSocket ready to send and receive messages.
|
|
*/
|
|
-const accept = async function(transport, input, output) {
|
|
- await serverHandshake(input, output);
|
|
+const accept = async function(transport, input, output, expectedPath) {
|
|
+ await serverHandshake(input, output, expectedPath || "/");
|
|
|
|
const transportProvider = {
|
|
setListener(upgradeListener) {
|
|
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
|
|
index b30c186c88daa7dd62f69e452dedc9e968511bb5..3a9bda87d5c577fd578bf3a523854d46c2a8db6a 100644
|
|
--- a/docshell/base/nsDocShell.cpp
|
|
+++ b/docshell/base/nsDocShell.cpp
|
|
@@ -95,6 +95,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"
|
|
@@ -351,6 +352,8 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext,
|
|
mUseStrictSecurityChecks(false),
|
|
mObserveErrorPages(true),
|
|
mCSSErrorReportingEnabled(false),
|
|
+ mFileInputInterceptionEnabled(false),
|
|
+ mBypassCSPEnabled(false),
|
|
mAllowAuth(mItemType == typeContent),
|
|
mAllowKeywordFixup(false),
|
|
mIsOffScreenBrowser(false),
|
|
@@ -1213,6 +1216,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
|
|
@@ -3347,6 +3351,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 e88da0c1e9adcc6f50ca4b3cb4a55d12430736e8..1544de7726143464e204532dae12dd2ad7373a0f 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"
|
|
@@ -485,6 +486,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(
|
|
@@ -1036,6 +1042,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
|
|
@@ -1292,6 +1300,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 267856626bdf2e7411b4ac975d4f64b824c826a1..03b7a44b39b5b3303c2519614dd39863b595fac6 100644
|
|
--- a/docshell/base/nsIDocShell.idl
|
|
+++ b/docshell/base/nsIDocShell.idl
|
|
@@ -1142,4 +1142,8 @@ interface nsIDocShell : nsIDocShellTreeItem
|
|
* @see nsISHEntry synchronizeLayoutHistoryState().
|
|
*/
|
|
void synchronizeLayoutHistoryState();
|
|
+
|
|
+ attribute boolean fileInputInterceptionEnabled;
|
|
+
|
|
+ attribute boolean bypassCSPEnabled;
|
|
};
|
|
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
|
|
index 9eac6924968638025556b8e97546ed3d91bfe28d..8839c880b75f00cb475bee7f68c48112e5263a1c 100644
|
|
--- a/dom/base/Document.cpp
|
|
+++ b/dom/base/Document.cpp
|
|
@@ -3267,6 +3267,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
|
|
@@ -3316,6 +3319,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 5fda32ce01630bb9151e47cb4a8cdb1180d120bd..5e4cfe38dc69f257f3057dbf1197f1f3e12fe654 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"
|
|
@@ -730,6 +731,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 edda707be08292a767f66d20f2abca98af113796..f7031a8e1fd813a9371b8f6d3a987a32e47b1dac 100644
|
|
--- a/dom/ipc/BrowserChild.cpp
|
|
+++ b/dom/ipc/BrowserChild.cpp
|
|
@@ -3632,6 +3632,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/script/ScriptSettings.cpp b/dom/script/ScriptSettings.cpp
|
|
index 9e3c1a56d10394d98de9e84fb8cd6ee8e3be5870..8c661de349d6cb64fd8d81d5db9c28f2a2af9138 100644
|
|
--- a/dom/script/ScriptSettings.cpp
|
|
+++ b/dom/script/ScriptSettings.cpp
|
|
@@ -140,6 +140,30 @@ ScriptSettingsStackEntry::~ScriptSettingsStackEntry() {
|
|
MOZ_ASSERT_IF(mGlobalObject, mGlobalObject->HasJSGlobal());
|
|
}
|
|
|
|
+static nsIGlobalObject* UnwrapSandboxGlobal(nsIGlobalObject* global) {
|
|
+ if (!global)
|
|
+ return global;
|
|
+ JSObject* globalObject = global->GetGlobalJSObject();
|
|
+ if (!globalObject)
|
|
+ return global;
|
|
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
|
|
+ if (!cx)
|
|
+ return global;
|
|
+ JS::Rooted<JSObject*> proto(cx);
|
|
+ JS::RootedObject rootedGlobal(cx, globalObject);
|
|
+ if (!JS_GetPrototype(cx, rootedGlobal, &proto))
|
|
+ return global;
|
|
+ if (!proto || !xpc::IsSandboxPrototypeProxy(proto))
|
|
+ return global;
|
|
+ // If this is a sandbox associated with a DOMWindow via a
|
|
+ // sandboxPrototype, use that DOMWindow. This supports GreaseMonkey
|
|
+ // and JetPack content scripts.
|
|
+ proto = js::CheckedUnwrapDynamic(proto, cx, /* stopAtWindowProxy = */ false);
|
|
+ if (!proto)
|
|
+ return global;
|
|
+ return xpc::WindowGlobalOrNull(proto);
|
|
+}
|
|
+
|
|
// If the entry or incumbent global ends up being something that the subject
|
|
// principal doesn't subsume, we don't want to use it. This never happens on
|
|
// the web, but can happen with asymmetric privilege relationships (i.e.
|
|
@@ -167,7 +191,7 @@ static nsIGlobalObject* ClampToSubject(nsIGlobalObject* aGlobalOrNull) {
|
|
NS_ENSURE_TRUE(globalPrin, GetCurrentGlobal());
|
|
if (!nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller()
|
|
->SubsumesConsideringDomain(globalPrin)) {
|
|
- return GetCurrentGlobal();
|
|
+ return UnwrapSandboxGlobal(GetCurrentGlobal());
|
|
}
|
|
|
|
return aGlobalOrNull;
|
|
diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp
|
|
index f0c28cfdae1c9ac33013e9688e0142d161763543..a38ab106e37dbab58e91ef5a873f8954c35881e7 100644
|
|
--- a/dom/security/nsCSPUtils.cpp
|
|
+++ b/dom/security/nsCSPUtils.cpp
|
|
@@ -121,6 +121,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 9b667d3a4c29e71297dc0bd33bfe30ab670a9f36..0971b5ca7930cfd6d7ac6e21f7187718bfc2499b 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 d99e7f91503e84690d711bca2c2d916e34e56214..b63e9d96219420f5e4fb58797e4c3d901cba77cc 100644
|
|
--- a/parser/html/nsHtml5TreeOpExecutor.cpp
|
|
+++ b/parser/html/nsHtml5TreeOpExecutor.cpp
|
|
@@ -1065,9 +1065,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 6dca2b78830edc1ddbd66264bd332853729dac71..fbe89c9682834e11b9d9219d9eb056ede084435a 100644
|
|
--- a/security/manager/ssl/nsCertOverrideService.cpp
|
|
+++ b/security/manager/ssl/nsCertOverrideService.cpp
|
|
@@ -634,7 +634,7 @@ static bool IsDebugger() {
|
|
NS_IMETHODIMP
|
|
nsCertOverrideService::
|
|
SetDisableAllSecurityChecksAndLetAttackersInterceptMyData(bool aDisable) {
|
|
- if (!(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) {
|
|
+ if (false /* juggler hacks */ && !(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) {
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
diff --git a/testing/juggler/BrowserContextManager.js b/testing/juggler/BrowserContextManager.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..483667dbec8e4c76533e4cf5e69ca9e322f2e708
|
|
--- /dev/null
|
|
+++ b/testing/juggler/BrowserContextManager.js
|
|
@@ -0,0 +1,194 @@
|
|
+"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 {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
|
|
+const helper = new Helper();
|
|
+
|
|
+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._browserContextIdToBrowserContext = new Map();
|
|
+ this._userContextIdToBrowserContext = 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);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ this._defaultContext = new BrowserContext(this, undefined, undefined);
|
|
+ }
|
|
+
|
|
+ createBrowserContext(options) {
|
|
+ return new BrowserContext(this, helper.generateId(), options);
|
|
+ }
|
|
+
|
|
+ browserContextForId(browserContextId) {
|
|
+ return this._browserContextIdToBrowserContext.get(browserContextId);
|
|
+ }
|
|
+
|
|
+ browserContextForUserContextId(userContextId) {
|
|
+ return this._userContextIdToBrowserContext.get(userContextId);
|
|
+ }
|
|
+
|
|
+ getBrowserContexts() {
|
|
+ return Array.from(this._browserContextIdToBrowserContext.values());
|
|
+ }
|
|
+}
|
|
+
|
|
+class BrowserContext {
|
|
+ constructor(manager, browserContextId, options) {
|
|
+ EventEmitter.decorate(this);
|
|
+
|
|
+ this._manager = manager;
|
|
+ this.browserContextId = browserContextId;
|
|
+ this.userContextId = undefined;
|
|
+ if (browserContextId !== undefined) {
|
|
+ const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId);
|
|
+ this.userContextId = identity.userContextId;
|
|
+ }
|
|
+ this._principals = [];
|
|
+ this._manager._browserContextIdToBrowserContext.set(this.browserContextId, this);
|
|
+ this._manager._userContextIdToBrowserContext.set(this.userContextId, this);
|
|
+ this.options = options || {};
|
|
+ this.options.scriptsToEvaluateOnNewDocument = [];
|
|
+ }
|
|
+
|
|
+ destroy() {
|
|
+ if (this.userContextId !== undefined) {
|
|
+ ContextualIdentityService.remove(this.userContextId);
|
|
+ ContextualIdentityService.closeContainerTabs(this.userContextId);
|
|
+ }
|
|
+ this._manager._browserContextIdToBrowserContext.delete(this.browserContextId);
|
|
+ this._manager._userContextIdToBrowserContext.delete(this.userContextId);
|
|
+ }
|
|
+
|
|
+ addScriptToEvaluateOnNewDocument(script) {
|
|
+ this.options.scriptsToEvaluateOnNewDocument.push(script);
|
|
+ this.emit(BrowserContext.Events.ScriptToEvaluateOnNewDocumentAdded, script);
|
|
+ }
|
|
+
|
|
+ grantPermissions(origin, permissions) {
|
|
+ const attrs = {userContextId: this.userContextId};
|
|
+ const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(origin), attrs);
|
|
+ this._principals.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() {
|
|
+ for (const principal of this._principals) {
|
|
+ for (const permission of ALL_PERMISSIONS)
|
|
+ Services.perms.removeFromPrincipal(principal, permission);
|
|
+ }
|
|
+ this._principals = [];
|
|
+ }
|
|
+
|
|
+ setCookies(cookies) {
|
|
+ const protocolToSameSite = {
|
|
+ [undefined]: Ci.nsICookie.SAMESITE_NONE,
|
|
+ 'Lax': Ci.nsICookie.SAMESITE_LAX,
|
|
+ 'Strict': Ci.nsICookie.SAMESITE_STRICT,
|
|
+ };
|
|
+ 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: this.userContextId } /* originAttributes */,
|
|
+ protocolToSameSite[cookie.sameSite],
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+
|
|
+ clearCookies() {
|
|
+ Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId: this.userContextId }));
|
|
+ }
|
|
+
|
|
+ getCookies() {
|
|
+ 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.cookies) {
|
|
+ if (cookie.originAttributes.userContextId !== (this.userContextId || 0))
|
|
+ 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;
|
|
+ }
|
|
+}
|
|
+
|
|
+BrowserContext.Events = {
|
|
+ ScriptToEvaluateOnNewDocumentAdded: Symbol('BrowserContext.Events.ScriptToEvaluateOnNewDocumentAdded'),
|
|
+};
|
|
+
|
|
+function dirPath(path) {
|
|
+ return path.substring(0, path.lastIndexOf('/') + 1);
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['BrowserContextManager', 'BrowserContext'];
|
|
+this.BrowserContextManager = BrowserContextManager;
|
|
+this.BrowserContext = BrowserContext;
|
|
+
|
|
diff --git a/testing/juggler/Helper.js b/testing/juggler/Helper.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..862c680198bbb503a5f04c19bdb8fdf2cd8c9cef
|
|
--- /dev/null
|
|
+++ b/testing/juggler/Helper.js
|
|
@@ -0,0 +1,102 @@
|
|
+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() {
|
|
+ const string = uuidGen.generateUUID().toString();
|
|
+ return string.substring(1, string.length - 1);
|
|
+ }
|
|
+
|
|
+ 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..8fe6a596bda3f58e6f93ba943fbbc0819bf0fc01
|
|
--- /dev/null
|
|
+++ b/testing/juggler/NetworkObserver.js
|
|
@@ -0,0 +1,689 @@
|
|
+"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 {TargetRegistry} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
|
|
+const {BrowserContextManager} = ChromeUtils.import('chrome://juggler/content/BrowserContextManager.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(); // oldId => newId
|
|
+ this._resumedRequestIdToHeaders = new Map(); // requestId => { headers }
|
|
+ this._postResumeChannelIdToRequestId = new Map(); // post-resume channel id => pre-resume request id
|
|
+
|
|
+ this._channelSink = {
|
|
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
|
|
+ asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => {
|
|
+ this._onRedirect(oldChannel, newChannel, flags);
|
|
+ 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);
|
|
+
|
|
+ this._browserInterceptors = new Map(); // Browser => (requestId => interceptor).
|
|
+ this._extraHTTPHeaders = new Map();
|
|
+ this._browserResponseStorages = new Map();
|
|
+ this._browserAuthCredentials = 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._browserInterceptors.has(browser))
|
|
+ this._browserInterceptors.set(browser, new Map());
|
|
+ }
|
|
+
|
|
+ disableRequestInterception(browser) {
|
|
+ const interceptors = this._browserInterceptors.get(browser);
|
|
+ if (!interceptors)
|
|
+ return;
|
|
+ this._browserInterceptors.delete(browser);
|
|
+ for (const interceptor of interceptors.values())
|
|
+ interceptor._resume();
|
|
+ }
|
|
+
|
|
+ _takeInterceptor(browser, requestId) {
|
|
+ const interceptors = this._browserInterceptors.get(browser);
|
|
+ if (!interceptors)
|
|
+ throw new Error(`Request interception is not enabled`);
|
|
+ const interceptor = interceptors.get(requestId);
|
|
+ if (!interceptor)
|
|
+ throw new Error(`Cannot find request "${requestId}"`);
|
|
+ interceptors.delete(requestId);
|
|
+ return interceptor;
|
|
+ }
|
|
+
|
|
+ resumeInterceptedRequest(browser, requestId, method, headers, postData) {
|
|
+ this._takeInterceptor(browser, requestId)._resume(method, headers, postData);
|
|
+ }
|
|
+
|
|
+ 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);
|
|
+ }
|
|
+
|
|
+ fulfillInterceptedRequest(browser, requestId, status, statusText, headers, base64body) {
|
|
+ this._takeInterceptor(browser, requestId)._fulfill(status, statusText, headers, base64body);
|
|
+ }
|
|
+
|
|
+ abortInterceptedRequest(browser, requestId, errorCode) {
|
|
+ this._takeInterceptor(browser, requestId)._abort(errorCode);
|
|
+ }
|
|
+
|
|
+ setAuthCredentials(browser, username, password) {
|
|
+ this._browserAuthCredentials.set(browser, { username, password });
|
|
+ }
|
|
+
|
|
+ _requestId(httpChannel) {
|
|
+ const id = httpChannel.channelId + '';
|
|
+ return this._postResumeChannelIdToRequestId.get(id) || id;
|
|
+ }
|
|
+
|
|
+ _onRedirect(oldChannel, newChannel, flags) {
|
|
+ if (!(oldChannel instanceof Ci.nsIHttpChannel) || !(newChannel instanceof Ci.nsIHttpChannel))
|
|
+ return;
|
|
+ const oldHttpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ const newHttpChannel = newChannel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ const browser = this._getBrowserForChannel(oldHttpChannel);
|
|
+ if (!browser)
|
|
+ return;
|
|
+ const oldRequestId = this._requestId(oldHttpChannel);
|
|
+ const newRequestId = this._requestId(newHttpChannel);
|
|
+ if (this._resumedRequestIdToHeaders.has(oldRequestId)) {
|
|
+ // When we call resetInterception on a request, we get a new "redirected" request for it.
|
|
+ const { method, headers, postData } = this._resumedRequestIdToHeaders.get(oldRequestId);
|
|
+ if (headers) {
|
|
+ // Apply new request headers from interception resume.
|
|
+ for (const header of requestHeaders(newChannel))
|
|
+ newChannel.setRequestHeader(header.name, '', false /* merge */);
|
|
+ for (const header of headers)
|
|
+ newChannel.setRequestHeader(header.name, header.value, false /* merge */);
|
|
+ }
|
|
+ if (method)
|
|
+ newChannel.requestMethod = method;
|
|
+ if (postData && newChannel instanceof Ci.nsIUploadChannel) {
|
|
+ const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
|
+ synthesized.data = atob(postData);
|
|
+ newChannel.setUploadStream(synthesized, 'application/octet-stream', -1);
|
|
+ }
|
|
+ // Use the old request id for the new "redirected" request for protocol consistency.
|
|
+ this._resumedRequestIdToHeaders.delete(oldRequestId);
|
|
+ this._postResumeChannelIdToRequestId.set(newRequestId, oldRequestId);
|
|
+ } else if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) {
|
|
+ // Regular (non-internal) redirect.
|
|
+ this._redirectMap.set(newRequestId, oldRequestId);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ 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 browser = this._getBrowserForChannel(httpChannel);
|
|
+ if (!browser)
|
|
+ return;
|
|
+ if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
|
|
+ return;
|
|
+ if (this._isResumedChannel(httpChannel))
|
|
+ return;
|
|
+ this._sendOnRequestFinished(httpChannel);
|
|
+ }
|
|
+
|
|
+ _getBrowserForChannel(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) { }
|
|
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement))
|
|
+ return;
|
|
+ return loadContext.topFrameElement;
|
|
+ }
|
|
+
|
|
+ _isResumedChannel(httpChannel) {
|
|
+ return this._postResumeChannelIdToRequestId.has(httpChannel.channelId + '');
|
|
+ }
|
|
+
|
|
+ _onRequest(channel, topic) {
|
|
+ if (!(channel instanceof Ci.nsIHttpChannel))
|
|
+ return;
|
|
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ const browser = this._getBrowserForChannel(httpChannel);
|
|
+ if (!browser)
|
|
+ return;
|
|
+ if (this._isResumedChannel(httpChannel)) {
|
|
+ // Ignore onRequest for resumed requests, but listen to their response.
|
|
+ new ResponseBodyListener(this, browser, httpChannel);
|
|
+ return;
|
|
+ }
|
|
+ const browserContext = TargetRegistry.instance().browserContextForBrowser(browser);
|
|
+ if (browserContext)
|
|
+ this._appendExtraHTTPHeaders(httpChannel, browserContext.options.extraHTTPHeaders);
|
|
+ this._appendExtraHTTPHeaders(httpChannel, this._extraHTTPHeaders.get(browser));
|
|
+ const requestId = this._requestId(httpChannel);
|
|
+ const isRedirect = this._redirectMap.has(requestId);
|
|
+ const interceptors = this._browserInterceptors.get(browser);
|
|
+ if (!interceptors) {
|
|
+ new NotificationCallbacks(this, browser, httpChannel, false);
|
|
+ this._sendOnRequest(httpChannel, false);
|
|
+ new ResponseBodyListener(this, browser, httpChannel);
|
|
+ } else if (isRedirect) {
|
|
+ // We pretend that redirect is interceptable in the protocol, although it's actually not
|
|
+ // and therefore we do not instantiate the interceptor.
|
|
+ // TODO: look into REDIRECT_MODE_MANUAL.
|
|
+ interceptors.set(requestId, {
|
|
+ _resume: () => {},
|
|
+ _abort: () => {},
|
|
+ _fulfill: () => {},
|
|
+ });
|
|
+ new NotificationCallbacks(this, browser, httpChannel, false);
|
|
+ this._sendOnRequest(httpChannel, true);
|
|
+ new ResponseBodyListener(this, browser, httpChannel);
|
|
+ } else {
|
|
+ new NotificationCallbacks(this, browser, httpChannel, true);
|
|
+ // We'll issue onRequest once it's intercepted.
|
|
+ }
|
|
+ }
|
|
+
|
|
+ _appendExtraHTTPHeaders(httpChannel, headers) {
|
|
+ if (!headers)
|
|
+ return;
|
|
+ for (const header of headers)
|
|
+ httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
|
|
+ }
|
|
+
|
|
+ _onIntercepted(httpChannel, interceptor) {
|
|
+ const browser = this._getBrowserForChannel(httpChannel);
|
|
+ if (!browser) {
|
|
+ interceptor._resume();
|
|
+ return;
|
|
+ }
|
|
+ const interceptors = this._browserInterceptors.get(browser);
|
|
+ this._sendOnRequest(httpChannel, !!interceptors);
|
|
+ if (interceptors)
|
|
+ interceptors.set(this._requestId(httpChannel), interceptor);
|
|
+ else
|
|
+ interceptor._resume();
|
|
+ }
|
|
+
|
|
+ _sendOnRequest(httpChannel, isIntercepted) {
|
|
+ const browser = this._getBrowserForChannel(httpChannel);
|
|
+ if (!browser)
|
|
+ return;
|
|
+ const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
|
|
+ const requestId = this._requestId(httpChannel);
|
|
+ const redirectedFrom = this._redirectMap.get(requestId);
|
|
+ this._redirectMap.delete(requestId);
|
|
+ this.emit('request', httpChannel, {
|
|
+ url: httpChannel.URI.spec,
|
|
+ isIntercepted,
|
|
+ requestId,
|
|
+ redirectedFrom,
|
|
+ postData: readRequestPostData(httpChannel),
|
|
+ headers: requestHeaders(httpChannel),
|
|
+ method: httpChannel.requestMethod,
|
|
+ navigationId: httpChannel.isMainDocumentChannel ? this._requestId(httpChannel) : undefined,
|
|
+ cause: causeTypeToString(causeType),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _sendOnRequestFinished(httpChannel) {
|
|
+ this.emit('requestfinished', httpChannel, {
|
|
+ requestId: this._requestId(httpChannel),
|
|
+ });
|
|
+ this._postResumeChannelIdToRequestId.delete(httpChannel.channelId + '');
|
|
+ }
|
|
+
|
|
+ _sendOnRequestFailed(httpChannel, error) {
|
|
+ this.emit('requestfailed', httpChannel, {
|
|
+ requestId: this._requestId(httpChannel),
|
|
+ errorCode: helper.getNetworkErrorStatusText(error),
|
|
+ });
|
|
+ this._postResumeChannelIdToRequestId.delete(httpChannel.channelId + '');
|
|
+ }
|
|
+
|
|
+ _onResponse(fromCache, httpChannel, topic) {
|
|
+ const browser = this._getBrowserForChannel(httpChannel);
|
|
+ if (!browser)
|
|
+ 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: this._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._sendOnRequestFinished(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(this, 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);
|
|
+ this._browserAuthCredentials.delete(browser);
|
|
+ this._browserInterceptors.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 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(networkObserver, maxTotalSize, maxResponseSize) {
|
|
+ this._networkObserver = networkObserver;
|
|
+ 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(this._networkObserver._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);
|
|
+ }
|
|
+}
|
|
+
|
|
+class NotificationCallbacks {
|
|
+ constructor(networkObserver, browser, httpChannel, shouldIntercept) {
|
|
+ this._networkObserver = networkObserver;
|
|
+ this._browser = browser;
|
|
+ this._shouldIntercept = shouldIntercept;
|
|
+ this._httpChannel = httpChannel;
|
|
+ this._previousCallbacks = httpChannel.notificationCallbacks;
|
|
+ httpChannel.notificationCallbacks = this;
|
|
+
|
|
+ const qis = [
|
|
+ Ci.nsIAuthPrompt2,
|
|
+ Ci.nsIAuthPromptProvider,
|
|
+ Ci.nsIInterfaceRequestor,
|
|
+ ];
|
|
+ if (shouldIntercept)
|
|
+ qis.push(Ci.nsINetworkInterceptController);
|
|
+ this.QueryInterface = ChromeUtils.generateQI(qis);
|
|
+ }
|
|
+
|
|
+ getInterface(iid) {
|
|
+ if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider))
|
|
+ return this;
|
|
+ if (this._shouldIntercept && iid.equals(Ci.nsINetworkInterceptController))
|
|
+ return this;
|
|
+ if (iid.equals(Ci.nsIAuthPrompt)) // Block nsIAuthPrompt - we want nsIAuthPrompt2 to be used instead.
|
|
+ throw Cr.NS_ERROR_NO_INTERFACE;
|
|
+ if (this._previousCallbacks)
|
|
+ return this._previousCallbacks.getInterface(iid);
|
|
+ throw Cr.NS_ERROR_NO_INTERFACE;
|
|
+ }
|
|
+
|
|
+ _forward(iid, method, args) {
|
|
+ if (!this._previousCallbacks)
|
|
+ return;
|
|
+ try {
|
|
+ const impl = this._previousCallbacks.getInterface(iid);
|
|
+ impl[method].apply(impl, args);
|
|
+ } catch (e) {
|
|
+ if (e.result != Cr.NS_ERROR_NO_INTERFACE)
|
|
+ throw e;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // nsIAuthPromptProvider
|
|
+ getAuthPrompt(aPromptReason, iid) {
|
|
+ return this;
|
|
+ }
|
|
+
|
|
+ // nsIAuthPrompt2
|
|
+ asyncPromptAuth(aChannel, aCallback, aContext, level, authInfo) {
|
|
+ let canceled = false;
|
|
+ Promise.resolve().then(() => {
|
|
+ if (canceled)
|
|
+ return;
|
|
+ const hasAuth = this.promptAuth(aChannel, level, authInfo);
|
|
+ if (hasAuth)
|
|
+ aCallback.onAuthAvailable(aContext, authInfo);
|
|
+ else
|
|
+ aCallback.onAuthCancelled(aContext, true);
|
|
+ });
|
|
+ return {
|
|
+ QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
|
|
+ cancel: () => {
|
|
+ aCallback.onAuthCancelled(aContext, false);
|
|
+ canceled = true;
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+
|
|
+ // nsIAuthPrompt2
|
|
+ promptAuth(aChannel, level, authInfo) {
|
|
+ if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED)
|
|
+ return false;
|
|
+ const credentials = this._networkObserver._browserAuthCredentials.get(this._browser);
|
|
+ if (!credentials || credentials.username === null)
|
|
+ return false;
|
|
+ authInfo.username = credentials.username;
|
|
+ authInfo.password = credentials.password;
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ // nsINetworkInterceptController
|
|
+ shouldPrepareForIntercept(aURI, channel) {
|
|
+ if (!(channel instanceof Ci.nsIHttpChannel))
|
|
+ return false;
|
|
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ return httpChannel.channelId === this._httpChannel.channelId;
|
|
+ }
|
|
+
|
|
+ // nsINetworkInterceptController
|
|
+ channelIntercepted(intercepted) {
|
|
+ this._intercepted = intercepted.QueryInterface(Ci.nsIInterceptedChannel);
|
|
+ const httpChannel = this._intercepted.channel.QueryInterface(Ci.nsIHttpChannel);
|
|
+ this._networkObserver._onIntercepted(httpChannel, this);
|
|
+ }
|
|
+
|
|
+ _resume(method, headers, postData) {
|
|
+ this._networkObserver._resumedRequestIdToHeaders.set(this._networkObserver._requestId(this._httpChannel), { method, headers, postData });
|
|
+ this._intercepted.resetInterception();
|
|
+ }
|
|
+
|
|
+ _fulfill(status, statusText, headers, base64body) {
|
|
+ this._intercepted.synthesizeStatus(status, statusText);
|
|
+ for (const header of headers)
|
|
+ this._intercepted.synthesizeHeader(header.name, header.value);
|
|
+ const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
|
+ if (base64body)
|
|
+ synthesized.data = atob(base64body);
|
|
+ else
|
|
+ synthesized.data = '';
|
|
+ this._intercepted.startSynthesizedResponse(synthesized, null, null, '', false);
|
|
+ this._intercepted.finishSynthesizedResponse();
|
|
+ this._networkObserver.emit('response', this._httpChannel, {
|
|
+ requestId: this._networkObserver._requestId(this._httpChannel),
|
|
+ securityDetails: null,
|
|
+ fromCache: false,
|
|
+ headers,
|
|
+ status,
|
|
+ statusText,
|
|
+ });
|
|
+ this._networkObserver._sendOnRequestFinished(this._httpChannel);
|
|
+ }
|
|
+
|
|
+ _abort(errorCode) {
|
|
+ const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
|
|
+ this._intercepted.cancelInterception(error);
|
|
+ this._networkObserver._sendOnRequestFailed(this._httpChannel, error);
|
|
+ }
|
|
+}
|
|
+
|
|
+const errorMap = {
|
|
+ 'aborted': Cr.NS_ERROR_ABORT,
|
|
+ 'accessdenied': Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED,
|
|
+ 'addressunreachable': Cr.NS_ERROR_UNKNOWN_HOST,
|
|
+ 'blockedbyclient': Cr.NS_ERROR_FAILURE,
|
|
+ 'blockedbyresponse': Cr.NS_ERROR_FAILURE,
|
|
+ 'connectionaborted': Cr.NS_ERROR_NET_INTERRUPT,
|
|
+ 'connectionclosed': Cr.NS_ERROR_FAILURE,
|
|
+ 'connectionfailed': Cr.NS_ERROR_FAILURE,
|
|
+ 'connectionrefused': Cr.NS_ERROR_CONNECTION_REFUSED,
|
|
+ 'connectionreset': Cr.NS_ERROR_NET_RESET,
|
|
+ 'internetdisconnected': Cr.NS_ERROR_OFFLINE,
|
|
+ 'namenotresolved': Cr.NS_ERROR_UNKNOWN_HOST,
|
|
+ 'timedout': Cr.NS_ERROR_NET_TIMEOUT,
|
|
+ 'failed': Cr.NS_ERROR_FAILURE,
|
|
+};
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['NetworkObserver'];
|
|
+this.NetworkObserver = NetworkObserver;
|
|
diff --git a/testing/juggler/SimpleChannel.js b/testing/juggler/SimpleChannel.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..ba34976ad05e7f5f1a99777f76ac08b171af40b7
|
|
--- /dev/null
|
|
+++ b/testing/juggler/SimpleChannel.js
|
|
@@ -0,0 +1,130 @@
|
|
+"use strict";
|
|
+// Note: this file should be loadabale with eval() into worker environment.
|
|
+// Avoid Components.*, ChromeUtils and global const variables.
|
|
+
|
|
+const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel';
|
|
+
|
|
+class SimpleChannel {
|
|
+ static createForMessageManager(name, mm) {
|
|
+ const channel = new SimpleChannel(name);
|
|
+
|
|
+ const messageListener = {
|
|
+ receiveMessage: message => channel._onMessage(message.data)
|
|
+ };
|
|
+ mm.addMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener);
|
|
+
|
|
+ channel.transport.sendMessage = obj => mm.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj);
|
|
+ channel.transport.dispose = () => {
|
|
+ mm.removeMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener);
|
|
+ };
|
|
+ return channel;
|
|
+ }
|
|
+
|
|
+ constructor(name) {
|
|
+ this._name = name;
|
|
+ this._messageId = 0;
|
|
+ this._connectorId = 0;
|
|
+ this._pendingMessages = new Map();
|
|
+ this._handlers = new Map();
|
|
+ this.transport = {
|
|
+ sendMessage: null,
|
|
+ dispose: null,
|
|
+ };
|
|
+ this._disposed = false;
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ if (this._disposed)
|
|
+ return;
|
|
+ this._disposed = true;
|
|
+ for (const {resolve, reject, methodName} of this._pendingMessages.values())
|
|
+ reject(new Error(`Failed "${methodName}": ${this._name} is disposed.`));
|
|
+ this._pendingMessages.clear();
|
|
+ this._handlers.clear();
|
|
+ this.transport.dispose();
|
|
+ }
|
|
+
|
|
+ _rejectCallbacksFromConnector(connectorId) {
|
|
+ for (const [messageId, callback] of this._pendingMessages) {
|
|
+ if (callback.connectorId === connectorId) {
|
|
+ callback.reject(new Error(`Failed "${callback.methodName}": connector for namespace "${callback.namespace}" in channel "${this._name}" is disposed.`));
|
|
+ this._pendingMessages.delete(messageId);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ connect(namespace) {
|
|
+ const connectorId = ++this._connectorId;
|
|
+ return {
|
|
+ send: (...args) => this._send(namespace, connectorId, ...args),
|
|
+ emit: (...args) => void this._send(namespace, connectorId, ...args).catch(e => {}),
|
|
+ dispose: () => this._rejectCallbacksFromConnector(connectorId),
|
|
+ };
|
|
+ }
|
|
+
|
|
+ register(namespace, handler) {
|
|
+ if (this._handlers.has(namespace))
|
|
+ throw new Error('ERROR: double-register for namespace ' + namespace);
|
|
+ this._handlers.set(namespace, handler);
|
|
+ return () => this.unregister(namespace);
|
|
+ }
|
|
+
|
|
+ unregister(namespace) {
|
|
+ this._handlers.delete(namespace);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {string} namespace
|
|
+ * @param {number} connectorId
|
|
+ * @param {string} methodName
|
|
+ * @param {...*} params
|
|
+ * @return {!Promise<*>}
|
|
+ */
|
|
+ async _send(namespace, connectorId, methodName, ...params) {
|
|
+ if (this._disposed)
|
|
+ throw new Error(`ERROR: channel ${this._name} is already disposed! Cannot send "${methodName}" to "${namespace}"`);
|
|
+ const id = ++this._messageId;
|
|
+ const promise = new Promise((resolve, reject) => {
|
|
+ this._pendingMessages.set(id, {connectorId, resolve, reject, methodName, namespace});
|
|
+ });
|
|
+ this.transport.sendMessage({requestId: id, methodName, params, namespace});
|
|
+ return promise;
|
|
+ }
|
|
+
|
|
+ async _onMessage(data) {
|
|
+ if (data.responseId) {
|
|
+ const {resolve, reject} = this._pendingMessages.get(data.responseId);
|
|
+ this._pendingMessages.delete(data.responseId);
|
|
+ if (data.error)
|
|
+ reject(new Error(data.error));
|
|
+ else
|
|
+ resolve(data.result);
|
|
+ } else if (data.requestId) {
|
|
+ const namespace = data.namespace;
|
|
+ const handler = this._handlers.get(namespace);
|
|
+ if (!handler) {
|
|
+ this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No handler for namespace "${namespace}"`});
|
|
+ return;
|
|
+ }
|
|
+ const method = handler[data.methodName];
|
|
+ if (!method) {
|
|
+ this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`});
|
|
+ return;
|
|
+ }
|
|
+ try {
|
|
+ const result = await method.call(handler, ...data.params);
|
|
+ this.transport.sendMessage({responseId: data.requestId, result});
|
|
+ } catch (error) {
|
|
+ this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`});
|
|
+ return;
|
|
+ }
|
|
+ } else {
|
|
+ dump(`
|
|
+ ERROR: unknown message in channel "${this._name}": ${JSON.stringify(data)}
|
|
+ `);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+var EXPORTED_SYMBOLS = ['SimpleChannel'];
|
|
+this.SimpleChannel = SimpleChannel;
|
|
diff --git a/testing/juggler/TargetRegistry.js b/testing/juggler/TargetRegistry.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..2cb5f24b079289f00d84d0d7b266443635edd2b2
|
|
--- /dev/null
|
|
+++ b/testing/juggler/TargetRegistry.js
|
|
@@ -0,0 +1,257 @@
|
|
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
|
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+const {BrowserContext} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js");
|
|
+
|
|
+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._createTargetForTab(tab);
|
|
+ this._mainWindow.gBrowser.tabContainer.addEventListener('TabOpen', event => {
|
|
+ const target = this._createTargetForTab(event.target);
|
|
+ // If we come here, content will have juggler script from the start,
|
|
+ // and we should wait for initial navigation, unless the tab was window.open'ed.
|
|
+ target._waitForInitialNavigation = !event.target.linkedBrowser.hasContentOpener;
|
|
+ // For pages created before we attach to them, we don't wait for initial
|
|
+ // navigation (target._waitForInitialNavigation is false by default).
|
|
+ });
|
|
+ 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());
|
|
+ });
|
|
+ Services.obs.addObserver(this, 'oop-frameloader-crashed');
|
|
+ }
|
|
+
|
|
+ async newPage({browserContextId}) {
|
|
+ const browserContext = this._contextManager.browserContextForId(browserContextId);
|
|
+ const tab = this._mainWindow.gBrowser.addTab('about:blank', {
|
|
+ userContextId: browserContext.userContextId,
|
|
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
+ });
|
|
+ this._mainWindow.gBrowser.selectedTab = tab;
|
|
+ const target = this._tabToTarget.get(tab);
|
|
+ await target._contentReadyPromise;
|
|
+ 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;
|
|
+ }
|
|
+
|
|
+ contentChannelForTarget(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._channel;
|
|
+ }
|
|
+
|
|
+ targetForId(targetId) {
|
|
+ return this._targets.get(targetId);
|
|
+ }
|
|
+
|
|
+ targetForBrowser(browser) {
|
|
+ const tab = this._mainWindow.gBrowser.getTabForBrowser(browser);
|
|
+ return tab ? this._tabToTarget.get(tab) : undefined;
|
|
+ }
|
|
+
|
|
+ browserContextForBrowser(browser) {
|
|
+ const tab = this._mainWindow.gBrowser.getTabForBrowser(browser);
|
|
+ return tab ? this._contextManager.browserContextForUserContextId(tab.userContextId) : undefined;
|
|
+ }
|
|
+
|
|
+ _createTargetForTab(tab) {
|
|
+ if (this._tabToTarget.has(tab))
|
|
+ throw new Error(`Internal error: two targets per tab`);
|
|
+ const openerTarget = tab.openerTab ? this._tabToTarget.get(tab.openerTab) : null;
|
|
+ const target = new PageTarget(this, tab, this._contextManager.browserContextForUserContextId(tab.userContextId), openerTarget);
|
|
+ this._targets.set(target.id(), target);
|
|
+ this._tabToTarget.set(tab, target);
|
|
+ this.emit(TargetRegistry.Events.TargetCreated, target.info());
|
|
+ return target;
|
|
+ }
|
|
+
|
|
+ observe(subject, topic, data) {
|
|
+ if (topic === 'oop-frameloader-crashed') {
|
|
+ const browser = subject.ownerElement;
|
|
+ if (!browser)
|
|
+ return;
|
|
+ const target = this.targetForBrowser(browser);
|
|
+ if (!target)
|
|
+ return;
|
|
+ this.emit(TargetRegistry.Events.TargetCrashed, target.id());
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+class PageTarget {
|
|
+ constructor(registry, tab, browserContext, opener) {
|
|
+ this._targetId = helper.generateId();
|
|
+ this._registry = registry;
|
|
+ this._tab = tab;
|
|
+ this._browserContext = browserContext;
|
|
+ this._openerId = opener ? opener.id() : undefined;
|
|
+ this._url = tab.linkedBrowser.currentURI.spec;
|
|
+ this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, tab.linkedBrowser.messageManager);
|
|
+
|
|
+ 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),
|
|
+ helper.addMessageListener(tab.linkedBrowser.messageManager, 'juggler:content-ready', {
|
|
+ receiveMessage: () => this._onContentReady()
|
|
+ }),
|
|
+ ];
|
|
+
|
|
+ this._contentReadyPromise = new Promise(f => this._contentReadyCallback = f);
|
|
+ this._waitForInitialNavigation = false;
|
|
+
|
|
+ if (browserContext) {
|
|
+ this._eventListeners.push(helper.on(browserContext, BrowserContext.Events.ScriptToEvaluateOnNewDocumentAdded, script => {
|
|
+ this._channel.connect('').emit('addScriptToEvaluateOnNewDocument', {script});
|
|
+ }));
|
|
+ }
|
|
+
|
|
+ if (browserContext && browserContext.options.viewport)
|
|
+ this.setViewportSize(browserContext.options.viewport.viewportSize);
|
|
+ }
|
|
+
|
|
+ setViewportSize(viewportSize) {
|
|
+ if (viewportSize) {
|
|
+ const {width, height} = viewportSize;
|
|
+ this._tab.linkedBrowser.style.setProperty('min-width', width + 'px');
|
|
+ this._tab.linkedBrowser.style.setProperty('min-height', height + 'px');
|
|
+ this._tab.linkedBrowser.style.setProperty('max-width', width + 'px');
|
|
+ this._tab.linkedBrowser.style.setProperty('max-height', height + 'px');
|
|
+ } else {
|
|
+ this._tab.linkedBrowser.style.removeProperty('min-width');
|
|
+ this._tab.linkedBrowser.style.removeProperty('min-height');
|
|
+ this._tab.linkedBrowser.style.removeProperty('max-width');
|
|
+ this._tab.linkedBrowser.style.removeProperty('max-height');
|
|
+ }
|
|
+ const rect = this._tab.linkedBrowser.getBoundingClientRect();
|
|
+ return { width: rect.width, height: rect.height };
|
|
+ }
|
|
+
|
|
+ _onContentReady() {
|
|
+ const sessionIds = [];
|
|
+ const data = { sessionIds, targetInfo: this.info() };
|
|
+ this._registry.emit(TargetRegistry.Events.PageTargetReady, data);
|
|
+ this._contentReadyCallback();
|
|
+ return {
|
|
+ browserContextOptions: this._browserContext ? this._browserContext.options : {},
|
|
+ waitForInitialNavigation: this._waitForInitialNavigation,
|
|
+ sessionIds
|
|
+ };
|
|
+ }
|
|
+
|
|
+ id() {
|
|
+ return this._targetId;
|
|
+ }
|
|
+
|
|
+ info() {
|
|
+ return {
|
|
+ targetId: this.id(),
|
|
+ type: 'page',
|
|
+ url: this._url,
|
|
+ browserContextId: this._browserContext ? this._browserContext.browserContextId : undefined,
|
|
+ openerId: this._openerId,
|
|
+ };
|
|
+ }
|
|
+
|
|
+ _onNavigated(aLocation) {
|
|
+ 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'),
|
|
+ TargetCrashed: Symbol('TargetRegistry.Events.TargetCrashed'),
|
|
+ PageTargetReady: Symbol('TargetRegistry.Events.PageTargetReady'),
|
|
+};
|
|
+
|
|
+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..055b032beff4b7d66a9f33d600dd8d2926867a34
|
|
--- /dev/null
|
|
+++ b/testing/juggler/components/juggler.js
|
|
@@ -0,0 +1,116 @@
|
|
+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 {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const helper = new Helper();
|
|
+
|
|
+const Cc = Components.classes;
|
|
+const Ci = Components.interfaces;
|
|
+
|
|
+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);
|
|
+
|
|
+ const token = helper.generateId();
|
|
+
|
|
+ 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, "/" + token);
|
|
+ new Dispatcher(webSocket);
|
|
+ }
|
|
+ });
|
|
+
|
|
+ Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */);
|
|
+ dump(`Juggler listening on ws://127.0.0.1:${this._server.port}/${token}\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/FrameTree.js b/testing/juggler/content/FrameTree.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..ba3aa173d496bdd5f9ff6dcffa56c755fa871763
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/FrameTree.js
|
|
@@ -0,0 +1,373 @@
|
|
+"use strict";
|
|
+const Ci = Components.interfaces;
|
|
+const Cr = Components.results;
|
|
+const Cu = Components.utils;
|
|
+
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
+const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
|
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
|
|
+
|
|
+const helper = new Helper();
|
|
+
|
|
+class FrameTree {
|
|
+ constructor(rootDocShell, waitForInitialNavigation) {
|
|
+ EventEmitter.decorate(this);
|
|
+
|
|
+ this._browsingContextGroup = rootDocShell.browsingContext.group;
|
|
+ if (!this._browsingContextGroup.__jugglerFrameTrees)
|
|
+ this._browsingContextGroup.__jugglerFrameTrees = new Set();
|
|
+ this._browsingContextGroup.__jugglerFrameTrees.add(this);
|
|
+
|
|
+ this._workers = new Map();
|
|
+ this._docShellToFrame = new Map();
|
|
+ this._frameIdToFrame = new Map();
|
|
+ this._pageReady = !waitForInitialNavigation;
|
|
+ this._mainFrame = this._createFrame(rootDocShell);
|
|
+ const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
+ .getInterface(Ci.nsIWebProgress);
|
|
+ this.QueryInterface = ChromeUtils.generateQI([
|
|
+ Ci.nsIWebProgressListener,
|
|
+ Ci.nsIWebProgressListener2,
|
|
+ Ci.nsISupportsWeakReference,
|
|
+ ]);
|
|
+ this._scriptsToEvaluateOnNewDocument = [];
|
|
+
|
|
+ this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager);
|
|
+ this._wdmListener = {
|
|
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]),
|
|
+ onRegister: this._onWorkerCreated.bind(this),
|
|
+ onUnregister: this._onWorkerDestroyed.bind(this),
|
|
+ };
|
|
+ this._wdm.addListener(this._wdmListener);
|
|
+ for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator())
|
|
+ this._onWorkerCreated(workerDebugger);
|
|
+
|
|
+
|
|
+ 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),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ workers() {
|
|
+ return [...this._workers.values()];
|
|
+ }
|
|
+
|
|
+ _frameForWorker(workerDebugger) {
|
|
+ if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED)
|
|
+ return null;
|
|
+ const docShell = workerDebugger.window.docShell;
|
|
+ return this._docShellToFrame.get(docShell) || null;
|
|
+ }
|
|
+
|
|
+ _onWorkerCreated(workerDebugger) {
|
|
+ // Note: we do not interoperate with firefox devtools.
|
|
+ if (workerDebugger.isInitialized)
|
|
+ return;
|
|
+ const frame = this._frameForWorker(workerDebugger);
|
|
+ if (!frame)
|
|
+ return;
|
|
+ const worker = new Worker(frame, workerDebugger);
|
|
+ this._workers.set(workerDebugger, worker);
|
|
+ this.emit(FrameTree.Events.WorkerCreated, worker);
|
|
+ }
|
|
+
|
|
+ _onWorkerDestroyed(workerDebugger) {
|
|
+ const frame = this._frameForWorker(workerDebugger);
|
|
+ if (!frame)
|
|
+ return;
|
|
+ const worker = this._workers.get(workerDebugger);
|
|
+ if (!worker)
|
|
+ return;
|
|
+ worker.dispose();
|
|
+ this._workers.delete(workerDebugger);
|
|
+ this.emit(FrameTree.Events.WorkerDestroyed, worker);
|
|
+ }
|
|
+
|
|
+ allFramesInBrowsingContextGroup(group) {
|
|
+ const frames = [];
|
|
+ for (const frameTree of (group.__jugglerFrameTrees || []))
|
|
+ frames.push(...frameTree.frames());
|
|
+ return frames;
|
|
+ }
|
|
+
|
|
+ isPageReady() {
|
|
+ return this._pageReady;
|
|
+ }
|
|
+
|
|
+ addScriptToEvaluateOnNewDocument(script) {
|
|
+ this._scriptsToEvaluateOnNewDocument.push(script);
|
|
+ }
|
|
+
|
|
+ scriptsToEvaluateOnNewDocument() {
|
|
+ return this._scriptsToEvaluateOnNewDocument;
|
|
+ }
|
|
+
|
|
+ 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() {
|
|
+ this._browsingContextGroup.__jugglerFrameTrees.delete(this);
|
|
+ this._wdm.removeListener(this._wdmListener);
|
|
+ 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);
|
|
+ if (frame === this._mainFrame && !this._pageReady) {
|
|
+ this._pageReady = true;
|
|
+ this.emit(FrameTree.Events.PageReady);
|
|
+ }
|
|
+ } 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',
|
|
+ WorkerCreated: 'workercreated',
|
|
+ WorkerDestroyed: 'workerdestroyed',
|
|
+ NavigationStarted: 'navigationstarted',
|
|
+ NavigationCommitted: 'navigationcommitted',
|
|
+ NavigationAborted: 'navigationaborted',
|
|
+ SameDocumentNavigation: 'samedocumentnavigation',
|
|
+ PageReady: 'pageready',
|
|
+};
|
|
+
|
|
+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 (docShell.domWindow && docShell.domWindow.location)
|
|
+ this._url = docShell.domWindow.location.href;
|
|
+ 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;
|
|
+ }
|
|
+
|
|
+}
|
|
+
|
|
+class Worker {
|
|
+ constructor(frame, workerDebugger) {
|
|
+ this._frame = frame;
|
|
+ this._workerId = helper.generateId();
|
|
+ this._workerDebugger = workerDebugger;
|
|
+
|
|
+ workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js');
|
|
+
|
|
+ this._channel = new SimpleChannel(`content::worker[${this._workerId}]`);
|
|
+ this._channel.transport = {
|
|
+ sendMessage: obj => workerDebugger.postMessage(JSON.stringify(obj)),
|
|
+ dispose: () => {},
|
|
+ };
|
|
+ this._workerDebuggerListener = {
|
|
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerListener]),
|
|
+ onMessage: msg => void this._channel._onMessage(JSON.parse(msg)),
|
|
+ onClose: () => void this._channel.dispose(),
|
|
+ onError: (filename, lineno, message) => {
|
|
+ dump(`Error in worker: ${message} @${filename}:${lineno}\n`);
|
|
+ },
|
|
+ };
|
|
+ workerDebugger.addListener(this._workerDebuggerListener);
|
|
+ }
|
|
+
|
|
+ channel() {
|
|
+ return this._channel;
|
|
+ }
|
|
+
|
|
+ frame() {
|
|
+ return this._frame;
|
|
+ }
|
|
+
|
|
+ id() {
|
|
+ return this._workerId;
|
|
+ }
|
|
+
|
|
+ url() {
|
|
+ return this._workerDebugger.url;
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._channel.dispose();
|
|
+ this._workerDebugger.removeListener(this._workerDebuggerListener);
|
|
+ }
|
|
+}
|
|
+
|
|
+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..be70ea364f9534bb3b344f64970366c32e8c11be
|
|
--- /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..8c47b147be6fee0a013edd7021b6f8deb640f831
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/PageAgent.js
|
|
@@ -0,0 +1,885 @@
|
|
+"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 WorkerData {
|
|
+ constructor(pageAgent, browserChannel, sessionId, worker) {
|
|
+ this._workerRuntime = worker.channel().connect(sessionId + 'runtime');
|
|
+ this._browserWorker = browserChannel.connect(sessionId + worker.id());
|
|
+ this._worker = worker;
|
|
+ this._sessionId = sessionId;
|
|
+ const emit = name => {
|
|
+ return (...args) => this._browserWorker.emit(name, ...args);
|
|
+ };
|
|
+ this._eventListeners = [
|
|
+ worker.channel().register(sessionId + 'runtime', {
|
|
+ runtimeConsole: emit('runtimeConsole'),
|
|
+ runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'),
|
|
+ runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'),
|
|
+ workerConsoleMessage: (hash) => pageAgent._runtime.filterConsoleMessage(hash),
|
|
+ }),
|
|
+ browserChannel.register(sessionId + worker.id(), {
|
|
+ evaluate: (options) => this._workerRuntime.send('evaluate', options),
|
|
+ callFunction: (options) => this._workerRuntime.send('callFunction', options),
|
|
+ getObjectProperties: (options) => this._workerRuntime.send('getObjectProperties', options),
|
|
+ disposeObject: (options) =>this._workerRuntime.send('disposeObject', options),
|
|
+ }),
|
|
+ ];
|
|
+ worker.channel().connect('').emit('connect', {sessionId});
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._worker.channel().connect('').emit('disconnect', {sessionId: this._sessionId});
|
|
+ this._workerRuntime.dispose();
|
|
+ this._browserWorker.dispose();
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+}
|
|
+
|
|
+class FrameData {
|
|
+ constructor(agent, frame) {
|
|
+ this._agent = agent;
|
|
+ this._frame = frame;
|
|
+ this._isolatedWorlds = new Map();
|
|
+ this.reset();
|
|
+ }
|
|
+
|
|
+ reset() {
|
|
+ if (this.mainContext)
|
|
+ this._agent._runtime.destroyExecutionContext(this.mainContext);
|
|
+ for (const world of this._isolatedWorlds.values())
|
|
+ this._agent._runtime.destroyExecutionContext(world);
|
|
+ this._isolatedWorlds.clear();
|
|
+
|
|
+ this.mainContext = this._agent._runtime.createExecutionContext(this._frame.domWindow(), this._frame.domWindow(), {
|
|
+ frameId: this._frame.id(),
|
|
+ name: '',
|
|
+ });
|
|
+
|
|
+ for (const bindingName of this._agent._bindingsToAdd.values())
|
|
+ this.exposeFunction(bindingName);
|
|
+ for (const script of this._agent._frameTree.scriptsToEvaluateOnNewDocument()) {
|
|
+ // TODO: this should actually be handled in FrameTree, but first we have to move
|
|
+ // execution contexts there.
|
|
+ try {
|
|
+ let result = this.mainContext.evaluateScript(script);
|
|
+ if (result && result.objectId)
|
|
+ this.mainContext.disposeObject(result.objectId);
|
|
+ } catch (e) {
|
|
+ }
|
|
+ }
|
|
+ for (const {script, worldName} of this._agent._scriptsToEvaluateOnNewDocument.values()) {
|
|
+ const context = worldName ? this.createIsolatedWorld(worldName) : this.mainContext;
|
|
+ try {
|
|
+ let result = context.evaluateScript(script);
|
|
+ if (result && result.objectId)
|
|
+ context.disposeObject(result.objectId);
|
|
+ } catch (e) {
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ exposeFunction(name) {
|
|
+ Cu.exportFunction((...args) => {
|
|
+ this._agent._session.emit('pageBindingCalled', {
|
|
+ executionContextId: this.mainContext.id(),
|
|
+ name,
|
|
+ payload: args[0]
|
|
+ });
|
|
+ }, this._frame.domWindow(), {
|
|
+ defineAs: name,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ createIsolatedWorld(name) {
|
|
+ const principal = [this._frame.domWindow()]; // extended principal
|
|
+ const sandbox = Cu.Sandbox(principal, {
|
|
+ sandboxPrototype: this._frame.domWindow(),
|
|
+ wantComponents: false,
|
|
+ wantExportHelpers: false,
|
|
+ wantXrays: true,
|
|
+ });
|
|
+ const world = this._agent._runtime.createExecutionContext(this._frame.domWindow(), sandbox, {
|
|
+ frameId: this._frame.id(),
|
|
+ name,
|
|
+ });
|
|
+ this._isolatedWorlds.set(world.id(), world);
|
|
+ return world;
|
|
+ }
|
|
+
|
|
+ unsafeObject(objectId) {
|
|
+ if (this.mainContext) {
|
|
+ const result = this.mainContext.unsafeObject(objectId);
|
|
+ if (result)
|
|
+ return result.object;
|
|
+ }
|
|
+ for (const world of this._isolatedWorlds.values()) {
|
|
+ const result = world.unsafeObject(objectId);
|
|
+ if (result)
|
|
+ return result.object;
|
|
+ }
|
|
+ throw new Error('Cannot find object with id = ' + objectId);
|
|
+ }
|
|
+
|
|
+ dispose() {}
|
|
+}
|
|
+
|
|
+class PageAgent {
|
|
+ constructor(messageManager, browserChannel, sessionId, runtimeAgent, frameTree, networkMonitor) {
|
|
+ this._messageManager = messageManager;
|
|
+ this._browserChannel = browserChannel;
|
|
+ this._sessionId = sessionId;
|
|
+ this._session = browserChannel.connect(sessionId + 'page');
|
|
+ this._runtime = runtimeAgent;
|
|
+ this._frameTree = frameTree;
|
|
+ this._networkMonitor = networkMonitor;
|
|
+
|
|
+ this._frameData = new Map();
|
|
+ this._workerData = new Map();
|
|
+ this._scriptsToEvaluateOnNewDocument = new Map();
|
|
+ this._bindingsToAdd = new Set();
|
|
+
|
|
+ this._eventListeners = [
|
|
+ browserChannel.register(sessionId + 'page', this),
|
|
+ ];
|
|
+ this._enabled = false;
|
|
+
|
|
+ const docShell = frameTree.mainFrame().docShell();
|
|
+ this._docShell = docShell;
|
|
+ this._initialDPPX = docShell.contentViewer.overrideDPPX;
|
|
+ this._customScrollbars = null;
|
|
+
|
|
+ this._runtime.setOnErrorFromWorker((domWindow, message, stack) => {
|
|
+ const frame = this._frameTree.frameForDocShell(domWindow.docShell);
|
|
+ if (!frame)
|
|
+ return;
|
|
+ this._session.emit('pageUncaughtError', {
|
|
+ frameId: frame.id(),
|
|
+ message,
|
|
+ stack,
|
|
+ });
|
|
+ });
|
|
+ }
|
|
+
|
|
+ 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 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;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ addScriptToEvaluateOnNewDocument({script, worldName}) {
|
|
+ const scriptId = helper.generateId();
|
|
+ this._scriptsToEvaluateOnNewDocument.set(scriptId, {script, worldName});
|
|
+ if (worldName) {
|
|
+ for (const frameData of this._frameData.values())
|
|
+ frameData.createIsolatedWorld(worldName);
|
|
+ }
|
|
+ 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;
|
|
+ }
|
|
+
|
|
+ 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);
|
|
+ }
|
|
+
|
|
+ for (const worker of this._frameTree.workers())
|
|
+ this._onWorkerCreated(worker);
|
|
+
|
|
+ this._eventListeners.push(...[
|
|
+ helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'),
|
|
+ helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'),
|
|
+ helper.addEventListener(this._messageManager, 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)),
|
|
+ helper.addEventListener(this._messageManager, 'pageshow', this._onLoad.bind(this)),
|
|
+ helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'),
|
|
+ helper.addEventListener(this._messageManager, '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)),
|
|
+ helper.on(this._frameTree, 'pageready', () => this._session.emit('pageReady', {})),
|
|
+ helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)),
|
|
+ helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)),
|
|
+ ]);
|
|
+
|
|
+ if (this._frameTree.isPageReady())
|
|
+ this._session.emit('pageReady', {});
|
|
+ }
|
|
+
|
|
+ _onWorkerCreated(worker) {
|
|
+ const workerData = new WorkerData(this, this._browserChannel, this._sessionId, worker);
|
|
+ this._workerData.set(worker.id(), workerData);
|
|
+ this._session.emit('pageWorkerCreated', {
|
|
+ workerId: worker.id(),
|
|
+ frameId: worker.frame().id(),
|
|
+ url: worker.url(),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onWorkerDestroyed(worker) {
|
|
+ const workerData = this._workerData.get(worker.id());
|
|
+ if (!workerData)
|
|
+ return;
|
|
+ this._workerData.delete(worker.id());
|
|
+ workerData.dispose();
|
|
+ this._session.emit('pageWorkerDestroyed', {
|
|
+ workerId: worker.id(),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ setInterceptFileChooserDialog({enabled}) {
|
|
+ this._docShell.fileInputInterceptionEnabled = !!enabled;
|
|
+ }
|
|
+
|
|
+ _filePickerShown(inputElement) {
|
|
+ if (inputElement.ownerGlobal.docShell !== this._docShell)
|
|
+ return;
|
|
+ const frameData = this._findFrameForNode(inputElement);
|
|
+ this._session.emit('pageFileChooserOpened', {
|
|
+ executionContextId: frameData.mainContext.id(),
|
|
+ element: frameData.mainContext.rawValueToRemoteObject(inputElement)
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _findFrameForNode(node) {
|
|
+ return Array.from(this._frameData.values()).find(data => {
|
|
+ const doc = data._frame.domWindow().document;
|
|
+ return node === doc || node.ownerDocument === doc;
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onDOMContentLoaded(event) {
|
|
+ const docShell = event.target.ownerGlobal.docShell;
|
|
+ const frame = this._frameTree.frameForDocShell(docShell);
|
|
+ if (!frame)
|
|
+ return;
|
|
+ this._session.emit('pageEventFired', {
|
|
+ frameId: frame.id(),
|
|
+ name: 'DOMContentLoaded',
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onError(errorEvent) {
|
|
+ const docShell = errorEvent.target.ownerGlobal.docShell;
|
|
+ const frame = this._frameTree.frameForDocShell(docShell);
|
|
+ if (!frame)
|
|
+ return;
|
|
+ this._session.emit('pageUncaughtError', {
|
|
+ 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.emit('pageEventFired', {
|
|
+ frameId: frame.id(),
|
|
+ name: 'load'
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onLoad(event) {
|
|
+ const docShell = event.target.ownerGlobal.docShell;
|
|
+ const frame = this._frameTree.frameForDocShell(docShell);
|
|
+ if (!frame)
|
|
+ return;
|
|
+ this._session.emit('pageEventFired', {
|
|
+ frameId: frame.id(),
|
|
+ name: 'load'
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onNavigationStarted(frame) {
|
|
+ this._session.emit('pageNavigationStarted', {
|
|
+ frameId: frame.id(),
|
|
+ navigationId: frame.pendingNavigationId(),
|
|
+ url: frame.pendingNavigationURL(),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onNavigationAborted(frame, navigationId, errorText) {
|
|
+ this._session.emit('pageNavigationAborted', {
|
|
+ frameId: frame.id(),
|
|
+ navigationId,
|
|
+ errorText,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onSameDocumentNavigation(frame) {
|
|
+ this._session.emit('pageSameDocumentNavigation', {
|
|
+ frameId: frame.id(),
|
|
+ url: frame.url(),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onNavigationCommitted(frame) {
|
|
+ this._session.emit('pageNavigationCommitted', {
|
|
+ frameId: frame.id(),
|
|
+ navigationId: frame.lastCommittedNavigationId() || undefined,
|
|
+ url: frame.url(),
|
|
+ name: frame.name(),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _onDOMWindowCreated(window) {
|
|
+ const docShell = window.docShell;
|
|
+ const frame = this._frameTree.frameForDocShell(docShell);
|
|
+ if (!frame)
|
|
+ return;
|
|
+ this._frameData.get(frame).reset();
|
|
+ }
|
|
+
|
|
+ _onFrameAttached(frame) {
|
|
+ this._session.emit('pageFrameAttached', {
|
|
+ frameId: frame.id(),
|
|
+ parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined,
|
|
+ });
|
|
+ this._frameData.set(frame, new FrameData(this, frame));
|
|
+ }
|
|
+
|
|
+ _onFrameDetached(frame) {
|
|
+ this._frameData.delete(frame);
|
|
+ this._session.emit('pageFrameDetached', {
|
|
+ frameId: frame.id(),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ for (const workerData of this._workerData.values())
|
|
+ workerData.dispose();
|
|
+ this._workerData.clear();
|
|
+ for (const frameData of this._frameData.values())
|
|
+ frameData.dispose();
|
|
+ this._frameData.clear();
|
|
+ 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 frameData of this._frameData.values())
|
|
+ frameData.exposeFunction(name);
|
|
+ }
|
|
+
|
|
+ async adoptNode({frameId, objectId, executionContextId}) {
|
|
+ const frame = this._frameTree.frame(frameId);
|
|
+ if (!frame)
|
|
+ throw new Error('Failed to find frame with id = ' + frameId);
|
|
+ const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
|
|
+ const context = this._runtime.findExecutionContext(executionContextId);
|
|
+ const fromPrincipal = unsafeObject.nodePrincipal;
|
|
+ const toFrame = this._frameTree.frame(context.auxData().frameId);
|
|
+ const toPrincipal = toFrame.domWindow().document.nodePrincipal;
|
|
+ if (!toPrincipal.subsumes(fromPrincipal))
|
|
+ return { remoteObject: null };
|
|
+ return { remoteObject: context.rawValueToRemoteObject(unsafeObject) };
|
|
+ }
|
|
+
|
|
+ async setFileInputFiles({objectId, frameId, files}) {
|
|
+ const frame = this._frameTree.frame(frameId);
|
|
+ if (!frame)
|
|
+ throw new Error('Failed to find frame with id = ' + frameId);
|
|
+ const unsafeObject = this._frameData.get(frame).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 unsafeObject = this._frameData.get(frame).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};
|
|
+ }
|
|
+
|
|
+ describeNode({objectId, frameId}) {
|
|
+ const frame = this._frameTree.frame(frameId);
|
|
+ if (!frame)
|
|
+ throw new Error('Failed to find frame with id = ' + frameId);
|
|
+ const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
|
|
+ const browsingContextGroup = frame.docShell().browsingContext.group;
|
|
+ const frames = this._frameTree.allFramesInBrowsingContextGroup(browsingContextGroup);
|
|
+ let contentFrame;
|
|
+ let ownerFrame;
|
|
+ for (const frame of frames) {
|
|
+ if (unsafeObject.contentWindow && frame.docShell() === unsafeObject.contentWindow.docShell)
|
|
+ contentFrame = frame;
|
|
+ const document = frame.domWindow().document;
|
|
+ if (unsafeObject === document || unsafeObject.ownerDocument === document)
|
|
+ ownerFrame = frame;
|
|
+ }
|
|
+ return {
|
|
+ contentFrameId: contentFrame ? contentFrame.id() : undefined,
|
|
+ ownerFrameId: ownerFrame ? ownerFrame.id() : undefined,
|
|
+ };
|
|
+ }
|
|
+
|
|
+ async scrollIntoViewIfNeeded({objectId, frameId, rect}) {
|
|
+ const frame = this._frameTree.frame(frameId);
|
|
+ if (!frame)
|
|
+ throw new Error('Failed to find frame with id = ' + frameId);
|
|
+ const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
|
|
+ if (!unsafeObject.isConnected)
|
|
+ throw new Error('Node is detached from document');
|
|
+ await this._scrollNodeIntoViewIfNeeded(unsafeObject);
|
|
+ const box = this._getBoundingBox(unsafeObject);
|
|
+ if (rect) {
|
|
+ box.x += rect.x;
|
|
+ box.y += rect.y;
|
|
+ box.width = rect.width;
|
|
+ box.height = rect.height;
|
|
+ }
|
|
+ this._scrollRectIntoViewIfNeeded(unsafeObject, box);
|
|
+ }
|
|
+
|
|
+ async _scrollNodeIntoViewIfNeeded(node) {
|
|
+ if (node.nodeType !== 1)
|
|
+ node = node.parentElement;
|
|
+ if (!node.ownerDocument || !node.ownerDocument.defaultView)
|
|
+ return;
|
|
+ const global = node.ownerDocument.defaultView;
|
|
+ const visibleRatio = await new Promise(resolve => {
|
|
+ const observer = new global.IntersectionObserver(entries => {
|
|
+ resolve(entries[0].intersectionRatio);
|
|
+ observer.disconnect();
|
|
+ });
|
|
+ observer.observe(node);
|
|
+ // Firefox doesn't call IntersectionObserver callback unless
|
|
+ // there are rafs.
|
|
+ global.requestAnimationFrame(() => {});
|
|
+ });
|
|
+ if (visibleRatio !== 1.0)
|
|
+ node.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
|
+ }
|
|
+
|
|
+ _scrollRectIntoViewIfNeeded(node, rect) {
|
|
+ // TODO: implement.
|
|
+ }
|
|
+
|
|
+ _getBoundingBox(unsafeObject) {
|
|
+ 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;
|
|
+ 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, y: y1, width: x2 - x1, height: y2 - y1};
|
|
+ }
|
|
+
|
|
+ async getBoundingBox({frameId, objectId}) {
|
|
+ const frame = this._frameTree.frame(frameId);
|
|
+ if (!frame)
|
|
+ throw new Error('Failed to find frame with id = ' + frameId);
|
|
+ const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
|
|
+ const box = this._getBoundingBox(unsafeObject);
|
|
+ if (!box)
|
|
+ return {boundingBox: null};
|
|
+ return {boundingBox: {x: box.x + frame.domWindow().scrollX, y: box.y + frame.domWindow().scrollY, width: box.width, height: box.height}};
|
|
+ }
|
|
+
|
|
+ async screenshot({mimeType, fullPage, clip}) {
|
|
+ const content = this._messageManager.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, text}) {
|
|
+ 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
|
|
+ });
|
|
+ if (type === 'keydown') {
|
|
+ if (text && text !== key) {
|
|
+ tip.commitCompositionWith(text, keyEvent);
|
|
+ } else {
|
|
+ const flags = 0;
|
|
+ tip.keydown(keyEvent, flags);
|
|
+ }
|
|
+ } else if (type === 'keyup') {
|
|
+ if (text)
|
|
+ throw new Error(`keyup does not support text option`);
|
|
+ const flags = 0;
|
|
+ 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 crash() {
|
|
+ dump(`Crashing intentionally\n`);
|
|
+ // This is to intentionally crash the frame.
|
|
+ // We crash by using js-ctypes and dereferencing
|
|
+ // a bad pointer. The crash should happen immediately
|
|
+ // upon loading this frame script.
|
|
+ const { ctypes } = ChromeUtils.import('resource://gre/modules/ctypes.jsm');
|
|
+ ChromeUtils.privateNoteIntentionalCrash();
|
|
+ const zero = new ctypes.intptr_t(8);
|
|
+ const badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
|
|
+ badptr.contents;
|
|
+ }
|
|
+
|
|
+ async getFullAXTree({objectId}) {
|
|
+ let unsafeObject = null;
|
|
+ if (objectId) {
|
|
+ unsafeObject = this._frameData.get(this._frameTree.mainFrame()).unsafeObject(objectId);
|
|
+ if (!unsafeObject)
|
|
+ throw new Error(`No object found for id "${objectId}"`);
|
|
+ }
|
|
+
|
|
+ const service = Cc["@mozilla.org/accessibilityService;1"]
|
|
+ .getService(Ci.nsIAccessibilityService);
|
|
+ const document = this._frameTree.mainFrame().domWindow().document;
|
|
+ const docAcc = service.getAccessibleFor(document);
|
|
+
|
|
+ while (docAcc.document.isUpdatePendingForJugglerAccessibility)
|
|
+ await new Promise(x => this._frameTree.mainFrame().domWindow().requestAnimationFrame(x));
|
|
+
|
|
+ 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 || '',
|
|
+ };
|
|
+ if (unsafeObject && unsafeObject === accElement.DOMNode)
|
|
+ tree.foundObject = true;
|
|
+ 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..a65fe9c34601f1311578c3f84d701d33cf8ab901
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/RuntimeAgent.js
|
|
@@ -0,0 +1,551 @@
|
|
+"use strict";
|
|
+// Note: this file should be loadabale with eval() into worker environment.
|
|
+// Avoid Components.*, ChromeUtils and global const variables.
|
|
+
|
|
+if (!this.Debugger) {
|
|
+ // Worker has a Debugger defined already.
|
|
+ const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {});
|
|
+ addDebuggerToGlobal(Components.utils.getGlobalForObject(this));
|
|
+}
|
|
+
|
|
+let lastId = 0;
|
|
+function generateId() {
|
|
+ return 'id-' + (++lastId);
|
|
+}
|
|
+
|
|
+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(channel, channelId, isWorker = false) {
|
|
+ this._debugger = new Debugger();
|
|
+ this._pendingPromises = new Map();
|
|
+ this._executionContexts = new Map();
|
|
+ this._windowToExecutionContext = new Map();
|
|
+ this._session = channel.connect(channelId + 'runtime');
|
|
+ this._eventListeners = [
|
|
+ channel.register(channelId + 'runtime', this),
|
|
+ ];
|
|
+ this._enabled = false;
|
|
+ this._filteredConsoleMessageHashes = new Set();
|
|
+ this._onErrorFromWorker = null;
|
|
+ this._isWorker = isWorker;
|
|
+ }
|
|
+
|
|
+ enable() {
|
|
+ if (this._enabled)
|
|
+ return;
|
|
+ this._enabled = true;
|
|
+ for (const executionContext of this._executionContexts.values())
|
|
+ this._notifyExecutionContextCreated(executionContext);
|
|
+
|
|
+ if (this._isWorker) {
|
|
+ this._registerConsoleEventHandler();
|
|
+ } else {
|
|
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
+ this._registerConsoleServiceListener(Services);
|
|
+ this._registerConsoleObserver(Services);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ _registerConsoleServiceListener(Services) {
|
|
+ const Ci = Components.interfaces;
|
|
+ const 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);
|
|
+ if (message.category === 'Web Worker' && (message.flags & Ci.nsIScriptError.exceptionFlag)) {
|
|
+ if (this._onErrorFromWorker)
|
|
+ this._onErrorFromWorker(errorWindow, message.message, '' + message.stack);
|
|
+ return;
|
|
+ }
|
|
+ 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.emit('runtimeConsole', {
|
|
+ args: [{
|
|
+ value: message.message,
|
|
+ }],
|
|
+ type: typeNames[message.logLevel],
|
|
+ executionContextId: executionContext.id(),
|
|
+ location: {
|
|
+ lineNumber: message.lineNumber,
|
|
+ columnNumber: message.columnNumber,
|
|
+ url: message.sourceName,
|
|
+ },
|
|
+ });
|
|
+ },
|
|
+ };
|
|
+ Services.console.registerListener(consoleServiceListener);
|
|
+ this._eventListeners.push(() => Services.console.unregisterListener(consoleServiceListener));
|
|
+ }
|
|
+
|
|
+ _registerConsoleObserver(Services) {
|
|
+ const consoleObserver = ({wrappedJSObject}, topic, data) => {
|
|
+ const hash = this._consoleMessageHash(wrappedJSObject);
|
|
+ if (this._filteredConsoleMessageHashes.has(hash)) {
|
|
+ this._filteredConsoleMessageHashes.delete(hash);
|
|
+ return;
|
|
+ }
|
|
+ const executionContext = Array.from(this._executionContexts.values()).find(context => {
|
|
+ const domWindow = context._domWindow;
|
|
+ return domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID;
|
|
+ });
|
|
+ if (!executionContext)
|
|
+ return;
|
|
+ this._onConsoleMessage(executionContext, wrappedJSObject);
|
|
+ };
|
|
+ Services.obs.addObserver(consoleObserver, "console-api-log-event");
|
|
+ this._eventListeners.push(() => Services.obs.removeObserver(consoleObserver, "console-api-log-event"));
|
|
+ }
|
|
+
|
|
+ _registerConsoleEventHandler() {
|
|
+ setConsoleEventHandler(message => {
|
|
+ this._session.emit('workerConsoleMessage', this._consoleMessageHash(message));
|
|
+ const executionContext = Array.from(this._executionContexts.values())[0];
|
|
+ this._onConsoleMessage(executionContext, message);
|
|
+ });
|
|
+ this._eventListeners.push(() => setConsoleEventHandler(null));
|
|
+ }
|
|
+
|
|
+ filterConsoleMessage(messageHash) {
|
|
+ this._filteredConsoleMessageHashes.add(messageHash);
|
|
+ }
|
|
+
|
|
+ setOnErrorFromWorker(onErrorFromWorker) {
|
|
+ this._onErrorFromWorker = onErrorFromWorker;
|
|
+ }
|
|
+
|
|
+ _consoleMessageHash(message) {
|
|
+ return `${message.timeStamp}/${message.filename}/${message.lineNumber}/${message.columnNumber}/${message.sourceId}/${message.level}`;
|
|
+ }
|
|
+
|
|
+ _onConsoleMessage(executionContext, message) {
|
|
+ const type = consoleLevelToProtocolType[message.level];
|
|
+ if (!type)
|
|
+ return;
|
|
+ const args = message.arguments.map(arg => executionContext.rawValueToRemoteObject(arg));
|
|
+ this._session.emit('runtimeConsole', {
|
|
+ args,
|
|
+ type,
|
|
+ executionContextId: executionContext.id(),
|
|
+ location: {
|
|
+ lineNumber: message.lineNumber - 1,
|
|
+ columnNumber: message.columnNumber - 1,
|
|
+ url: message.filename,
|
|
+ },
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _notifyExecutionContextCreated(executionContext) {
|
|
+ if (!this._enabled)
|
|
+ return;
|
|
+ this._session.emit('runtimeExecutionContextCreated', {
|
|
+ executionContextId: executionContext._id,
|
|
+ auxData: executionContext._auxData,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ _notifyExecutionContextDestroyed(executionContext) {
|
|
+ if (!this._enabled)
|
|
+ return;
|
|
+ this._session.emit('runtimeExecutionContextDestroyed', {
|
|
+ executionContextId: executionContext._id,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._session.dispose();
|
|
+ for (const tearDown of this._eventListeners)
|
|
+ tearDown.call(null);
|
|
+ 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, contextGlobal, auxData) {
|
|
+ // Note: domWindow is null for workers.
|
|
+ const context = new ExecutionContext(this, domWindow, contextGlobal, this._debugger.addDebuggee(contextGlobal), auxData);
|
|
+ this._executionContexts.set(context._id, context);
|
|
+ if (domWindow)
|
|
+ this._windowToExecutionContext.set(domWindow, context);
|
|
+ this._notifyExecutionContextCreated(context);
|
|
+ return context;
|
|
+ }
|
|
+
|
|
+ findExecutionContext(executionContextId) {
|
|
+ const executionContext = this._executionContexts.get(executionContextId);
|
|
+ if (!executionContext)
|
|
+ throw new Error('Failed to find execution context with id = ' + executionContextId);
|
|
+ return executionContext;
|
|
+ }
|
|
+
|
|
+ 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._contextGlobal);
|
|
+ this._executionContexts.delete(destroyedContext._id);
|
|
+ if (destroyedContext._domWindow)
|
|
+ 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};
|
|
+ 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};
|
|
+ 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, contextGlobal, global, auxData) {
|
|
+ this._runtime = runtime;
|
|
+ this._domWindow = domWindow;
|
|
+ this._contextGlobal = contextGlobal;
|
|
+ this._global = global;
|
|
+ this._remoteObjects = new Map();
|
|
+ this._id = generateId();
|
|
+ this._auxData = auxData;
|
|
+ this._jsonStringifyObject = this._global.executeInGlobal(`((stringify, dateProto, object) => {
|
|
+ const oldToJson = dateProto.toJSON;
|
|
+ dateProto.toJSON = undefined;
|
|
+ let hasSymbol = false;
|
|
+ const result = stringify(object, (key, value) => {
|
|
+ if (typeof value === 'symbol')
|
|
+ hasSymbol = true;
|
|
+ return value;
|
|
+ });
|
|
+ dateProto.toJSON = oldToJson;
|
|
+ return hasSymbol ? undefined : result;
|
|
+ }).bind(null, JSON.stringify.bind(JSON), Date.prototype)`).return;
|
|
+ }
|
|
+
|
|
+ id() {
|
|
+ return this._id;
|
|
+ }
|
|
+
|
|
+ auxData() {
|
|
+ return this._auxData;
|
|
+ }
|
|
+
|
|
+ async evaluateScript(script, exceptionDetails = {}) {
|
|
+ const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null;
|
|
+ let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails);
|
|
+ userInputHelper && 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 ? this._domWindow.windowUtils.setHandlingUserInput(true) : null;
|
|
+ let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails);
|
|
+ userInputHelper && 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))
|
|
+ return;
|
|
+ return { object: this._remoteObjects.get(objectId).unsafeDereference() };
|
|
+ }
|
|
+
|
|
+ rawValueToRemoteObject(rawValue) {
|
|
+ const debuggerObj = this._global.makeDebuggeeValue(rawValue);
|
|
+ return this._createRemoteObject(debuggerObj);
|
|
+ }
|
|
+
|
|
+ _instanceOf(debuggerObj, rawObj, className) {
|
|
+ if (this._domWindow)
|
|
+ return rawObj instanceof this._domWindow[className];
|
|
+ return this._global.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._global.makeDebuggeeValue(className)}).return;
|
|
+ }
|
|
+
|
|
+ _createRemoteObject(debuggerObj) {
|
|
+ if (debuggerObj instanceof Debugger.Object) {
|
|
+ const objectId = 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 (this._instanceOf(debuggerObj, rawObj, 'Node'))
|
|
+ subtype = 'node';
|
|
+ else if (this._instanceOf(debuggerObj, rawObj, 'RegExp'))
|
|
+ subtype = 'regexp';
|
|
+ else if (this._instanceOf(debuggerObj, rawObj, 'Date'))
|
|
+ subtype = 'date';
|
|
+ else if (this._instanceOf(debuggerObj, rawObj, 'Map'))
|
|
+ subtype = 'map';
|
|
+ else if (this._instanceOf(debuggerObj, rawObj, 'Set'))
|
|
+ subtype = 'set';
|
|
+ else if (this._instanceOf(debuggerObj, rawObj, 'WeakMap'))
|
|
+ subtype = 'weakmap';
|
|
+ else if (this._instanceOf(debuggerObj, rawObj, 'WeakSet'))
|
|
+ subtype = 'weakset';
|
|
+ else if (this._instanceOf(debuggerObj, rawObj, 'Error'))
|
|
+ subtype = 'error';
|
|
+ else if (this._instanceOf(debuggerObj, rawObj, 'Promise'))
|
|
+ subtype = 'promise';
|
|
+ else if ((this._instanceOf(debuggerObj, rawObj, 'Int8Array')) || (this._instanceOf(debuggerObj, rawObj, 'Uint8Array')) ||
|
|
+ (this._instanceOf(debuggerObj, rawObj, 'Uint8ClampedArray')) || (this._instanceOf(debuggerObj, rawObj, 'Int16Array')) ||
|
|
+ (this._instanceOf(debuggerObj, rawObj, 'Uint16Array')) || (this._instanceOf(debuggerObj, rawObj, 'Int32Array')) ||
|
|
+ (this._instanceOf(debuggerObj, rawObj, 'Uint32Array')) || (this._instanceOf(debuggerObj, rawObj, 'Float32Array')) ||
|
|
+ (this._instanceOf(debuggerObj, rawObj, 'Float64Array'))) {
|
|
+ subtype = 'typedarray';
|
|
+ }
|
|
+ return {objectId, type, subtype};
|
|
+ }
|
|
+ if (typeof debuggerObj === 'symbol') {
|
|
+ const objectId = 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;
|
|
+ if (obj === null)
|
|
+ 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('stringify(e)', {e: obj, stringify: this._jsonStringifyObject});
|
|
+ if (result.throw)
|
|
+ throw new Error('Object is not serializable');
|
|
+ return result.return === undefined ? undefined : 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/WorkerMain.js b/testing/juggler/content/WorkerMain.js
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..6d93903d340a39e5d90654fa6006ade2f980ebb9
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/WorkerMain.js
|
|
@@ -0,0 +1,29 @@
|
|
+"use strict";
|
|
+loadSubScript('chrome://juggler/content/content/RuntimeAgent.js');
|
|
+loadSubScript('chrome://juggler/content/SimpleChannel.js');
|
|
+
|
|
+const runtimeAgents = new Map();
|
|
+
|
|
+const channel = new SimpleChannel('worker::worker');
|
|
+const eventListener = event => channel._onMessage(JSON.parse(event.data));
|
|
+this.addEventListener('message', eventListener);
|
|
+channel.transport = {
|
|
+ sendMessage: msg => postMessage(JSON.stringify(msg)),
|
|
+ dispose: () => this.removeEventListener('message', eventListener),
|
|
+};
|
|
+
|
|
+channel.register('', {
|
|
+ connect: ({sessionId}) => {
|
|
+ const runtimeAgent = new RuntimeAgent(channel, sessionId, true /* isWorker */);
|
|
+ runtimeAgents.set(sessionId, runtimeAgent);
|
|
+ runtimeAgent.createExecutionContext(null /* domWindow */, global, {});
|
|
+ runtimeAgent.enable();
|
|
+ },
|
|
+
|
|
+ disconnect: ({sessionId}) => {
|
|
+ const runtimeAgent = runtimeAgents.get(sessionId);
|
|
+ runtimeAgents.delete(sessionId);
|
|
+ runtimeAgent.dispose();
|
|
+ },
|
|
+});
|
|
+
|
|
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..887180f71ef78604d2756ffa6a026ac968bda276
|
|
--- /dev/null
|
|
+++ b/testing/juggler/content/main.js
|
|
@@ -0,0 +1,96 @@
|
|
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.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 {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
|
+const {RuntimeAgent} = ChromeUtils.import('chrome://juggler/content/content/RuntimeAgent.js');
|
|
+const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
|
|
+
|
|
+const scrollbarManager = new ScrollbarManager(docShell);
|
|
+let frameTree;
|
|
+let networkMonitor;
|
|
+const helper = new Helper();
|
|
+const messageManager = this;
|
|
+
|
|
+const sessions = new Map();
|
|
+
|
|
+function createContentSession(channel, sessionId) {
|
|
+ const runtimeAgent = new RuntimeAgent(channel, sessionId);
|
|
+ const pageAgent = new PageAgent(messageManager, channel, sessionId, runtimeAgent, frameTree, networkMonitor);
|
|
+ sessions.set(sessionId, [runtimeAgent, pageAgent]);
|
|
+
|
|
+ runtimeAgent.enable();
|
|
+ pageAgent.enable();
|
|
+}
|
|
+
|
|
+function disposeContentSession(sessionId) {
|
|
+ const handlers = sessions.get(sessionId);
|
|
+ sessions.delete(sessionId);
|
|
+ for (const handler of handlers)
|
|
+ handler.dispose();
|
|
+}
|
|
+
|
|
+function initialize() {
|
|
+ let response = sendSyncMessage('juggler:content-ready', {})[0];
|
|
+ if (!response)
|
|
+ response = { sessionIds: [], browserContextOptions: {}, waitForInitialNavigation: false };
|
|
+
|
|
+ const { sessionIds, browserContextOptions, waitForInitialNavigation } = response;
|
|
+ const { userAgent, bypassCSP, javaScriptDisabled, viewport, scriptsToEvaluateOnNewDocument } = browserContextOptions;
|
|
+
|
|
+ if (userAgent !== undefined)
|
|
+ docShell.customUserAgent = userAgent;
|
|
+ if (bypassCSP !== undefined)
|
|
+ docShell.bypassCSPEnabled = bypassCSP;
|
|
+ if (javaScriptDisabled !== undefined)
|
|
+ docShell.allowJavascript = !javaScriptDisabled;
|
|
+ if (viewport !== undefined) {
|
|
+ docShell.contentViewer.overrideDPPX = viewport.deviceScaleFactor || this._initialDPPX;
|
|
+ docShell.deviceSizeIsPageSize = viewport.isMobile;
|
|
+ docShell.touchEventsOverride = viewport.hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE;
|
|
+ scrollbarManager.setFloatingScrollbars(viewport.isMobile);
|
|
+ }
|
|
+
|
|
+ frameTree = new FrameTree(docShell, waitForInitialNavigation);
|
|
+ for (const script of scriptsToEvaluateOnNewDocument || [])
|
|
+ frameTree.addScriptToEvaluateOnNewDocument(script);
|
|
+ networkMonitor = new NetworkMonitor(docShell, frameTree);
|
|
+
|
|
+ const channel = SimpleChannel.createForMessageManager('content::page', messageManager);
|
|
+
|
|
+ for (const sessionId of sessionIds)
|
|
+ createContentSession(channel, sessionId);
|
|
+
|
|
+ channel.register('', {
|
|
+ attach({sessionId}) {
|
|
+ createContentSession(channel, sessionId);
|
|
+ },
|
|
+
|
|
+ detach({sessionId}) {
|
|
+ disposeContentSession(sessionId);
|
|
+ },
|
|
+
|
|
+ addScriptToEvaluateOnNewDocument({script}) {
|
|
+ frameTree.addScriptToEvaluateOnNewDocument(script);
|
|
+ },
|
|
+
|
|
+ dispose() {
|
|
+ },
|
|
+ });
|
|
+
|
|
+ const gListeners = [
|
|
+ helper.addEventListener(messageManager, 'unload', msg => {
|
|
+ helper.removeListeners(gListeners);
|
|
+ channel.dispose();
|
|
+
|
|
+ for (const sessionId of sessions.keys())
|
|
+ disposeContentSession(sessionId);
|
|
+
|
|
+ scrollbarManager.dispose();
|
|
+ networkMonitor.dispose();
|
|
+ frameTree.dispose();
|
|
+ }),
|
|
+ ];
|
|
+}
|
|
+
|
|
+initialize();
|
|
diff --git a/testing/juggler/jar.mn b/testing/juggler/jar.mn
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..cf3a7fa162cf22146a23521b8d31b6ac38de839e
|
|
--- /dev/null
|
|
+++ b/testing/juggler/jar.mn
|
|
@@ -0,0 +1,30 @@
|
|
+# 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/SimpleChannel.js (SimpleChannel.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/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/WorkerMain.js (content/WorkerMain.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..2f2b7ca247f6b6dff396fb4b644654de87598507
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/AccessibilityHandler.js
|
|
@@ -0,0 +1,17 @@
|
|
+class AccessibilityHandler {
|
|
+ constructor(chromeSession, sessionId, contentChannel) {
|
|
+ this._chromeSession = chromeSession;
|
|
+ this._contentPage = contentChannel.connect(sessionId + 'page');
|
|
+ }
|
|
+
|
|
+ async getFullAXTree(params) {
|
|
+ return await this._contentPage.send('getFullAXTree', params);
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._contentPage.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..061bcb4ff8a34610a5ec433393bf7901c4cafe86
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/BrowserHandler.js
|
|
@@ -0,0 +1,81 @@
|
|
+"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() {
|
|
+ let browserWindow = Services.wm.getMostRecentWindow(
|
|
+ "navigator:browser"
|
|
+ );
|
|
+ if (browserWindow && browserWindow.gBrowserInit) {
|
|
+ await browserWindow.gBrowserInit.idleTasksFinishedPromise;
|
|
+ }
|
|
+ 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.browserContextForId(browserContextId).grantPermissions(origin, permissions);
|
|
+ }
|
|
+
|
|
+ resetPermissions({browserContextId}) {
|
|
+ this._contextManager.browserContextForId(browserContextId).resetPermissions();
|
|
+ }
|
|
+
|
|
+ setExtraHTTPHeaders({browserContextId, headers}) {
|
|
+ this._contextManager.browserContextForId(browserContextId).options.extraHTTPHeaders = headers;
|
|
+ }
|
|
+
|
|
+ addScriptToEvaluateOnNewDocument({browserContextId, script}) {
|
|
+ this._contextManager.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script);
|
|
+ }
|
|
+
|
|
+ setCookies({browserContextId, cookies}) {
|
|
+ this._contextManager.browserContextForId(browserContextId).setCookies(cookies);
|
|
+ }
|
|
+
|
|
+ clearCookies({browserContextId}) {
|
|
+ this._contextManager.browserContextForId(browserContextId).clearCookies();
|
|
+ }
|
|
+
|
|
+ getCookies({browserContextId}) {
|
|
+ const cookies = this._contextManager.browserContextForId(browserContextId).getCookies();
|
|
+ return {cookies};
|
|
+ }
|
|
+
|
|
+ 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..42e4622ed51b28ee6a5c48cc59c5400d42da002f
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/Dispatcher.js
|
|
@@ -0,0 +1,197 @@
|
|
+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 {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
|
|
+
|
|
+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 /* contentChannel */, TargetRegistry.instance().browserTargetInfo());
|
|
+
|
|
+ this._eventListeners = [
|
|
+ helper.on(TargetRegistry.instance(), TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ createSession(targetId, shouldConnect) {
|
|
+ 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 contentChannel = targetInfo.type === 'page' ? TargetRegistry.instance().contentChannelForTarget(targetInfo.targetId) : null;
|
|
+ if (shouldConnect && contentChannel)
|
|
+ contentChannel.connect('').send('attach', {sessionId});
|
|
+ const chromeSession = new ChromeSession(this, sessionId, contentChannel, 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, contentChannel, targetInfo) {
|
|
+ this._dispatcher = dispatcher;
|
|
+ this._sessionId = sessionId;
|
|
+ this._contentChannel = contentChannel;
|
|
+ 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, sessionId, contentChannel);
|
|
+ }
|
|
+ const pageHandler = this._handlers['Page'];
|
|
+ if (pageHandler)
|
|
+ pageHandler.enable();
|
|
+ const networkHandler = this._handlers['Network'];
|
|
+ if (networkHandler)
|
|
+ networkHandler.enable();
|
|
+ }
|
|
+
|
|
+ dispatcher() {
|
|
+ return this._dispatcher;
|
|
+ }
|
|
+
|
|
+ targetId() {
|
|
+ return this._targetInfo.targetId;
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ if (this._contentChannel)
|
|
+ this._contentChannel.connect('').emit('detach', {sessionId: this._sessionId});
|
|
+ this._contentChannel = 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);
|
|
+ }
|
|
+}
|
|
+
|
|
+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..698290fdb37d0b000a40a5009a607a8c66683ecc
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/NetworkHandler.js
|
|
@@ -0,0 +1,164 @@
|
|
+"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 helper = new Helper();
|
|
+
|
|
+class NetworkHandler {
|
|
+ constructor(chromeSession, sessionId, contentChannel) {
|
|
+ this._chromeSession = chromeSession;
|
|
+ this._contentPage = contentChannel.connect(sessionId + 'page');
|
|
+ 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();
|
|
+ this._requestIdToFrameId = new Map();
|
|
+ }
|
|
+
|
|
+ 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 resumeInterceptedRequest({requestId, method, headers, postData}) {
|
|
+ this._networkObserver.resumeInterceptedRequest(this._browser, requestId, method, headers, postData);
|
|
+ }
|
|
+
|
|
+ async abortInterceptedRequest({requestId, errorCode}) {
|
|
+ this._networkObserver.abortInterceptedRequest(this._browser, requestId, errorCode);
|
|
+ }
|
|
+
|
|
+ async fulfillInterceptedRequest({requestId, status, statusText, headers, base64body}) {
|
|
+ this._networkObserver.fulfillInterceptedRequest(this._browser, requestId, status, statusText, headers, base64body);
|
|
+ }
|
|
+
|
|
+ async setAuthCredentials({username, password}) {
|
|
+ this._networkObserver.setAuthCredentials(this._browser, username, password);
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._contentPage.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._contentPage.send('requestDetails', {channelId: httpChannel.channelId});
|
|
+ } catch (e) {
|
|
+ pendingRequestCallback();
|
|
+ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
|
|
+ return;
|
|
+ }
|
|
+ // Inherit frameId for redirects when details are not available.
|
|
+ const frameId = details ? details.frameId : (eventDetails.redirectedFrom ? this._requestIdToFrameId.get(eventDetails.redirectedFrom) : undefined);
|
|
+ this._requestIdToFrameId.set(eventDetails.requestId, frameId);
|
|
+ const activity = this._ensureHTTPActivity(eventDetails.requestId);
|
|
+ activity.request = {
|
|
+ frameId,
|
|
+ ...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..cbc64728a5fd21d1f7dde389efb58e89fd209912
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/PageHandler.js
|
|
@@ -0,0 +1,351 @@
|
|
+"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 helper = new Helper();
|
|
+
|
|
+class WorkerHandler {
|
|
+ constructor(chromeSession, contentChannel, sessionId, workerId) {
|
|
+ this._chromeSession = chromeSession;
|
|
+ this._sessionId = sessionId;
|
|
+ this._contentWorker = contentChannel.connect(sessionId + workerId);
|
|
+ this._workerId = workerId;
|
|
+
|
|
+ const emitWrappedProtocolEvent = eventName => {
|
|
+ return params => {
|
|
+ this._chromeSession.emitEvent('Page.dispatchMessageFromWorker', {
|
|
+ workerId,
|
|
+ message: JSON.stringify({method: eventName, params}),
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+
|
|
+ this._eventListeners = [
|
|
+ contentChannel.register(sessionId + workerId, {
|
|
+ runtimeConsole: emitWrappedProtocolEvent('Runtime.console'),
|
|
+ runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'),
|
|
+ runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'),
|
|
+ }),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ async sendMessage(message) {
|
|
+ const [domain, method] = message.method.split('.');
|
|
+ if (domain !== 'Runtime')
|
|
+ throw new Error('ERROR: can only dispatch to Runtime domain inside worker');
|
|
+ const result = await this._contentWorker.send(method, message.params);
|
|
+ this._chromeSession.emitEvent('Page.dispatchMessageFromWorker', {
|
|
+ workerId: this._workerId,
|
|
+ message: JSON.stringify({result, id: message.id}),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._contentWorker.dispose();
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+}
|
|
+
|
|
+class PageHandler {
|
|
+ constructor(chromeSession, sessionId, contentChannel) {
|
|
+ this._chromeSession = chromeSession;
|
|
+ this._contentChannel = contentChannel;
|
|
+ this._sessionId = sessionId;
|
|
+ this._contentPage = contentChannel.connect(sessionId + 'page');
|
|
+ this._workers = new Map();
|
|
+
|
|
+ const emitProtocolEvent = eventName => {
|
|
+ return (...args) => this._chromeSession.emitEvent(eventName, ...args);
|
|
+ }
|
|
+
|
|
+ this._eventListeners = [
|
|
+ contentChannel.register(sessionId + 'page', {
|
|
+ pageBindingCalled: emitProtocolEvent('Page.bindingCalled'),
|
|
+ pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'),
|
|
+ pageEventFired: emitProtocolEvent('Page.eventFired'),
|
|
+ pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'),
|
|
+ pageFrameAttached: emitProtocolEvent('Page.frameAttached'),
|
|
+ pageFrameDetached: emitProtocolEvent('Page.frameDetached'),
|
|
+ pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'),
|
|
+ pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'),
|
|
+ pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'),
|
|
+ pageReady: emitProtocolEvent('Page.ready'),
|
|
+ pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'),
|
|
+ pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
|
|
+ pageWorkerCreated: this._onWorkerCreated.bind(this),
|
|
+ pageWorkerDestroyed: this._onWorkerDestroyed.bind(this),
|
|
+ }),
|
|
+ ];
|
|
+ this._pageTarget = TargetRegistry.instance().targetForId(chromeSession.targetId());
|
|
+ this._browser = TargetRegistry.instance().tabForTarget(chromeSession.targetId()).linkedBrowser;
|
|
+ this._dialogs = new Map();
|
|
+
|
|
+ this._enabled = false;
|
|
+ }
|
|
+
|
|
+ _onWorkerCreated({workerId, frameId, url}) {
|
|
+ const worker = new WorkerHandler(this._chromeSession, this._contentChannel, this._sessionId, workerId);
|
|
+ this._workers.set(workerId, worker);
|
|
+ this._chromeSession.emitEvent('Page.workerCreated', {workerId, frameId, url});
|
|
+ }
|
|
+
|
|
+ _onWorkerDestroyed({workerId}) {
|
|
+ const worker = this._workers.get(workerId);
|
|
+ if (!worker)
|
|
+ return;
|
|
+ this._workers.delete(workerId);
|
|
+ worker.dispose();
|
|
+ this._chromeSession.emitEvent('Page.workerDestroyed', {workerId});
|
|
+ }
|
|
+
|
|
+ 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.push(...[
|
|
+ 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()),
|
|
+ helper.on(TargetRegistry.instance(), TargetRegistry.Events.TargetCrashed, targetId => {
|
|
+ if (targetId === this._chromeSession.targetId())
|
|
+ this._chromeSession.emitEvent('Page.crashed', {});
|
|
+ }),
|
|
+ ]);
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._contentPage.dispose();
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+
|
|
+ async setViewportSize({viewportSize}) {
|
|
+ const size = this._pageTarget.setViewportSize(viewportSize);
|
|
+ await this._contentPage.send('awaitViewportDimensions', {
|
|
+ width: size.width,
|
|
+ height: size.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 setFileInputFiles(options) {
|
|
+ return await this._contentPage.send('setFileInputFiles', options);
|
|
+ }
|
|
+
|
|
+ async setEmulatedMedia(options) {
|
|
+ return await this._contentPage.send('setEmulatedMedia', options);
|
|
+ }
|
|
+
|
|
+ async setCacheDisabled(options) {
|
|
+ return await this._contentPage.send('setCacheDisabled', options);
|
|
+ }
|
|
+
|
|
+ async addBinding(options) {
|
|
+ return await this._contentPage.send('addBinding', options);
|
|
+ }
|
|
+
|
|
+ async adoptNode(options) {
|
|
+ return await this._contentPage.send('adoptNode', options);
|
|
+ }
|
|
+
|
|
+ async screenshot(options) {
|
|
+ return await this._contentPage.send('screenshot', options);
|
|
+ }
|
|
+
|
|
+ async getBoundingBox(options) {
|
|
+ return await this._contentPage.send('getBoundingBox', options);
|
|
+ }
|
|
+
|
|
+ async getContentQuads(options) {
|
|
+ return await this._contentPage.send('getContentQuads', options);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {{frameId: string, url: string}} options
|
|
+ */
|
|
+ async navigate(options) {
|
|
+ return await this._contentPage.send('navigate', options);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {{frameId: string, url: string}} options
|
|
+ */
|
|
+ async goBack(options) {
|
|
+ return await this._contentPage.send('goBack', options);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {{frameId: string, url: string}} options
|
|
+ */
|
|
+ async goForward(options) {
|
|
+ return await this._contentPage.send('goForward', options);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param {{frameId: string, url: string}} options
|
|
+ */
|
|
+ async reload(options) {
|
|
+ return await this._contentPage.send('reload', options);
|
|
+ }
|
|
+
|
|
+ async describeNode(options) {
|
|
+ return await this._contentPage.send('describeNode', options);
|
|
+ }
|
|
+
|
|
+ async scrollIntoViewIfNeeded(options) {
|
|
+ return await this._contentPage.send('scrollIntoViewIfNeeded', options);
|
|
+ }
|
|
+
|
|
+ async addScriptToEvaluateOnNewDocument(options) {
|
|
+ return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options);
|
|
+ }
|
|
+
|
|
+ async removeScriptToEvaluateOnNewDocument(options) {
|
|
+ return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options);
|
|
+ }
|
|
+
|
|
+ async dispatchKeyEvent(options) {
|
|
+ return await this._contentPage.send('dispatchKeyEvent', options);
|
|
+ }
|
|
+
|
|
+ async dispatchTouchEvent(options) {
|
|
+ return await this._contentPage.send('dispatchTouchEvent', options);
|
|
+ }
|
|
+
|
|
+ async dispatchMouseEvent(options) {
|
|
+ return await this._contentPage.send('dispatchMouseEvent', options);
|
|
+ }
|
|
+
|
|
+ async insertText(options) {
|
|
+ return await this._contentPage.send('insertText', options);
|
|
+ }
|
|
+
|
|
+ async crash(options) {
|
|
+ return await this._contentPage.send('crash', 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._contentPage.send('setInterceptFileChooserDialog', options);
|
|
+ }
|
|
+
|
|
+ async handleFileChooser(options) {
|
|
+ return await this._contentPage.send('handleFileChooser', options);
|
|
+ }
|
|
+
|
|
+ async sendMessageToWorker({workerId, message}) {
|
|
+ const worker = this._workers.get(workerId);
|
|
+ if (!worker)
|
|
+ throw new Error('ERROR: cannot find worker with id ' + workerId);
|
|
+ return await worker.sendMessage(JSON.parse(message));
|
|
+ }
|
|
+}
|
|
+
|
|
+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..838b642eb08efee8a8e6e61421731aa3555e8429
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/Protocol.js
|
|
@@ -0,0 +1,764 @@
|
|
+const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js');
|
|
+
|
|
+// Protocol-specific types.
|
|
+const targetTypes = {};
|
|
+targetTypes.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),
|
|
+};
|
|
+
|
|
+const browserTypes = {};
|
|
+
|
|
+browserTypes.CookieOptions = {
|
|
+ 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),
|
|
+};
|
|
+
|
|
+browserTypes.Cookie = {
|
|
+ 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 pageTypes = {};
|
|
+pageTypes.DOMPoint = {
|
|
+ x: t.Number,
|
|
+ y: t.Number,
|
|
+};
|
|
+
|
|
+pageTypes.Rect = {
|
|
+ x: t.Number,
|
|
+ y: t.Number,
|
|
+ width: t.Number,
|
|
+ height: t.Number,
|
|
+};
|
|
+
|
|
+pageTypes.Size = {
|
|
+ width: t.Number,
|
|
+ height: t.Number,
|
|
+};
|
|
+
|
|
+pageTypes.Viewport = {
|
|
+ viewportSize: pageTypes.Size,
|
|
+ deviceScaleFactor: t.Number,
|
|
+ isMobile: t.Boolean,
|
|
+ hasTouch: t.Boolean,
|
|
+};
|
|
+
|
|
+pageTypes.DOMQuad = {
|
|
+ p1: pageTypes.DOMPoint,
|
|
+ p2: pageTypes.DOMPoint,
|
|
+ p3: pageTypes.DOMPoint,
|
|
+ p4: pageTypes.DOMPoint,
|
|
+};
|
|
+
|
|
+pageTypes.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),
|
|
+};
|
|
+
|
|
+pageTypes.Clip = {
|
|
+ x: t.Number,
|
|
+ y: t.Number,
|
|
+ width: t.Number,
|
|
+ height: t.Number,
|
|
+};
|
|
+
|
|
+
|
|
+const runtimeTypes = {};
|
|
+runtimeTypes.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
|
|
+};
|
|
+
|
|
+runtimeTypes.ObjectProperty = {
|
|
+ name: t.String,
|
|
+ value: runtimeTypes.RemoteObject,
|
|
+};
|
|
+
|
|
+runtimeTypes.ScriptLocation = {
|
|
+ columnNumber: t.Number,
|
|
+ lineNumber: t.Number,
|
|
+ url: t.String,
|
|
+};
|
|
+
|
|
+runtimeTypes.ExceptionDetails = {
|
|
+ text: t.Optional(t.String),
|
|
+ stack: t.Optional(t.String),
|
|
+ value: t.Optional(t.Any),
|
|
+};
|
|
+
|
|
+runtimeTypes.CallFunctionArgument = {
|
|
+ objectId: t.Optional(t.String),
|
|
+ unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])),
|
|
+ value: t.Any,
|
|
+};
|
|
+
|
|
+const axTypes = {};
|
|
+axTypes.AXTree = {
|
|
+ role: t.String,
|
|
+ name: t.String,
|
|
+ children: t.Optional(t.Array(t.Recursive(axTypes, '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),
|
|
+
|
|
+ foundObject: t.Optional(t.Boolean),
|
|
+}
|
|
+
|
|
+const networkTypes = {};
|
|
+
|
|
+networkTypes.HTTPHeader = {
|
|
+ name: t.String,
|
|
+ value: t.String,
|
|
+};
|
|
+
|
|
+networkTypes.SecurityDetails = {
|
|
+ protocol: t.String,
|
|
+ subjectName: t.String,
|
|
+ issuer: t.String,
|
|
+ validFrom: t.Number,
|
|
+ validTo: t.Number,
|
|
+};
|
|
+
|
|
+const Browser = {
|
|
+ targets: ['browser'],
|
|
+
|
|
+ events: {},
|
|
+
|
|
+ types: browserTypes,
|
|
+
|
|
+ methods: {
|
|
+ 'close': {},
|
|
+ 'getInfo': {
|
|
+ returns: {
|
|
+ userAgent: t.String,
|
|
+ version: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'setIgnoreHTTPSErrors': {
|
|
+ params: {
|
|
+ enabled: t.Boolean,
|
|
+ },
|
|
+ },
|
|
+ 'setExtraHTTPHeaders': {
|
|
+ params: {
|
|
+ browserContextId: t.Optional(t.String),
|
|
+ headers: t.Array(networkTypes.HTTPHeader),
|
|
+ },
|
|
+ },
|
|
+ 'addScriptToEvaluateOnNewDocument': {
|
|
+ params: {
|
|
+ browserContextId: t.Optional(t.String),
|
|
+ script: t.String,
|
|
+ }
|
|
+ },
|
|
+ '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(browserTypes.CookieOptions),
|
|
+ }
|
|
+ },
|
|
+ 'clearCookies': {
|
|
+ params: {
|
|
+ browserContextId: t.Optional(t.String),
|
|
+ }
|
|
+ },
|
|
+ 'getCookies': {
|
|
+ params: {
|
|
+ browserContextId: t.Optional(t.String)
|
|
+ },
|
|
+ returns: {
|
|
+ cookies: t.Array(browserTypes.Cookie),
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+const Target = {
|
|
+ targets: ['browser'],
|
|
+
|
|
+ types: targetTypes,
|
|
+
|
|
+ events: {
|
|
+ 'attachedToTarget': {
|
|
+ sessionId: t.String,
|
|
+ targetInfo: targetTypes.TargetInfo,
|
|
+ },
|
|
+ 'detachedFromTarget': {
|
|
+ sessionId: t.String,
|
|
+ },
|
|
+ 'targetCreated': targetTypes.TargetInfo,
|
|
+ 'targetDestroyed': targetTypes.TargetInfo,
|
|
+ 'targetInfoChanged': targetTypes.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': {
|
|
+ params: {
|
|
+ removeOnDetach: t.Optional(t.Boolean),
|
|
+ userAgent: t.Optional(t.String),
|
|
+ bypassCSP: t.Optional(t.Boolean),
|
|
+ javaScriptDisabled: t.Optional(t.Boolean),
|
|
+ viewport: t.Optional(pageTypes.Viewport),
|
|
+ },
|
|
+ returns: {
|
|
+ browserContextId: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'removeBrowserContext': {
|
|
+ params: {
|
|
+ browserContextId: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'getBrowserContexts': {
|
|
+ returns: {
|
|
+ browserContextIds: t.Array(t.String),
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+const Network = {
|
|
+ targets: ['page'],
|
|
+ types: networkTypes,
|
|
+ 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(networkTypes.HTTPHeader),
|
|
+ isIntercepted: t.Boolean,
|
|
+ url: t.String,
|
|
+ method: t.String,
|
|
+ navigationId: t.Optional(t.String),
|
|
+ cause: t.String,
|
|
+ },
|
|
+ 'responseReceived': {
|
|
+ securityDetails: t.Nullable(networkTypes.SecurityDetails),
|
|
+ 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(networkTypes.HTTPHeader),
|
|
+ },
|
|
+ 'requestFinished': {
|
|
+ requestId: t.String,
|
|
+ },
|
|
+ 'requestFailed': {
|
|
+ requestId: t.String,
|
|
+ errorCode: t.String,
|
|
+ },
|
|
+ },
|
|
+ methods: {
|
|
+ 'setRequestInterception': {
|
|
+ params: {
|
|
+ enabled: t.Boolean,
|
|
+ },
|
|
+ },
|
|
+ 'setExtraHTTPHeaders': {
|
|
+ params: {
|
|
+ headers: t.Array(networkTypes.HTTPHeader),
|
|
+ },
|
|
+ },
|
|
+ 'abortInterceptedRequest': {
|
|
+ params: {
|
|
+ requestId: t.String,
|
|
+ errorCode: t.String,
|
|
+ },
|
|
+ },
|
|
+ 'resumeInterceptedRequest': {
|
|
+ params: {
|
|
+ requestId: t.String,
|
|
+ method: t.Optional(t.String),
|
|
+ headers: t.Optional(t.Array(networkTypes.HTTPHeader)),
|
|
+ postData: t.Optional(t.String),
|
|
+ },
|
|
+ },
|
|
+ 'fulfillInterceptedRequest': {
|
|
+ params: {
|
|
+ requestId: t.String,
|
|
+ status: t.Number,
|
|
+ statusText: t.String,
|
|
+ headers: t.Array(networkTypes.HTTPHeader),
|
|
+ base64body: t.Optional(t.String), // base64-encoded
|
|
+ },
|
|
+ },
|
|
+ 'getResponseBody': {
|
|
+ params: {
|
|
+ requestId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ base64body: t.String,
|
|
+ evicted: t.Optional(t.Boolean),
|
|
+ },
|
|
+ },
|
|
+ 'setAuthCredentials': {
|
|
+ params: {
|
|
+ username: t.Nullable(t.String),
|
|
+ password: t.Nullable(t.String),
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+const Runtime = {
|
|
+ targets: ['page'],
|
|
+ types: runtimeTypes,
|
|
+ events: {
|
|
+ 'executionContextCreated': {
|
|
+ executionContextId: t.String,
|
|
+ auxData: t.Any,
|
|
+ },
|
|
+ 'executionContextDestroyed': {
|
|
+ executionContextId: t.String,
|
|
+ },
|
|
+ 'console': {
|
|
+ executionContextId: t.String,
|
|
+ args: t.Array(runtimeTypes.RemoteObject),
|
|
+ type: t.String,
|
|
+ location: runtimeTypes.ScriptLocation,
|
|
+ },
|
|
+ },
|
|
+ methods: {
|
|
+ 'evaluate': {
|
|
+ params: {
|
|
+ // Pass frameId here.
|
|
+ executionContextId: t.String,
|
|
+ expression: t.String,
|
|
+ returnByValue: t.Optional(t.Boolean),
|
|
+ },
|
|
+
|
|
+ returns: {
|
|
+ result: t.Optional(runtimeTypes.RemoteObject),
|
|
+ exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails),
|
|
+ }
|
|
+ },
|
|
+ 'callFunction': {
|
|
+ params: {
|
|
+ // Pass frameId here.
|
|
+ executionContextId: t.String,
|
|
+ functionDeclaration: t.String,
|
|
+ returnByValue: t.Optional(t.Boolean),
|
|
+ args: t.Array(runtimeTypes.CallFunctionArgument),
|
|
+ },
|
|
+
|
|
+ returns: {
|
|
+ result: t.Optional(runtimeTypes.RemoteObject),
|
|
+ exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails),
|
|
+ }
|
|
+ },
|
|
+ 'disposeObject': {
|
|
+ params: {
|
|
+ executionContextId: t.String,
|
|
+ objectId: t.String,
|
|
+ },
|
|
+ },
|
|
+
|
|
+ 'getObjectProperties': {
|
|
+ params: {
|
|
+ executionContextId: t.String,
|
|
+ objectId: t.String,
|
|
+ },
|
|
+
|
|
+ returns: {
|
|
+ properties: t.Array(runtimeTypes.ObjectProperty),
|
|
+ }
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+const Page = {
|
|
+ targets: ['page'],
|
|
+
|
|
+ types: pageTypes,
|
|
+ events: {
|
|
+ 'ready': {
|
|
+ },
|
|
+ 'crashed': {
|
|
+ },
|
|
+ '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| can only be null in response to enable.
|
|
+ navigationId: t.Optional(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: runtimeTypes.RemoteObject
|
|
+ },
|
|
+ 'workerCreated': {
|
|
+ workerId: t.String,
|
|
+ frameId: t.String,
|
|
+ url: t.String,
|
|
+ },
|
|
+ 'workerDestroyed': {
|
|
+ workerId: t.String,
|
|
+ },
|
|
+ 'dispatchMessageFromWorker': {
|
|
+ workerId: t.String,
|
|
+ message: t.String,
|
|
+ },
|
|
+ },
|
|
+
|
|
+ methods: {
|
|
+ '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,
|
|
+ },
|
|
+ },
|
|
+ 'setViewportSize': {
|
|
+ params: {
|
|
+ viewportSize: t.Nullable(pageTypes.Size),
|
|
+ },
|
|
+ },
|
|
+ 'setEmulatedMedia': {
|
|
+ params: {
|
|
+ type: t.Optional(t.Enum(['screen', 'print', ''])),
|
|
+ colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])),
|
|
+ },
|
|
+ },
|
|
+ 'setCacheDisabled': {
|
|
+ params: {
|
|
+ cacheDisabled: t.Boolean,
|
|
+ },
|
|
+ },
|
|
+ 'describeNode': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ objectId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ contentFrameId: t.Optional(t.String),
|
|
+ ownerFrameId: t.Optional(t.String),
|
|
+ },
|
|
+ },
|
|
+ 'scrollIntoViewIfNeeded': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ objectId: t.String,
|
|
+ rect: t.Optional(pageTypes.Rect),
|
|
+ },
|
|
+ },
|
|
+ 'addScriptToEvaluateOnNewDocument': {
|
|
+ params: {
|
|
+ script: t.String,
|
|
+ worldName: t.Optional(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: {
|
|
+ boundingBox: t.Nullable(pageTypes.Rect),
|
|
+ },
|
|
+ },
|
|
+ 'adoptNode': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ objectId: t.String,
|
|
+ executionContextId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ remoteObject: t.Nullable(runtimeTypes.RemoteObject),
|
|
+ },
|
|
+ },
|
|
+ 'screenshot': {
|
|
+ params: {
|
|
+ mimeType: t.Enum(['image/png', 'image/jpeg']),
|
|
+ fullPage: t.Optional(t.Boolean),
|
|
+ clip: t.Optional(pageTypes.Clip),
|
|
+ },
|
|
+ returns: {
|
|
+ data: t.String,
|
|
+ }
|
|
+ },
|
|
+ 'getContentQuads': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ objectId: t.String,
|
|
+ },
|
|
+ returns: {
|
|
+ quads: t.Array(pageTypes.DOMQuad),
|
|
+ },
|
|
+ },
|
|
+ 'dispatchKeyEvent': {
|
|
+ params: {
|
|
+ type: t.String,
|
|
+ key: t.String,
|
|
+ keyCode: t.Number,
|
|
+ location: t.Number,
|
|
+ code: t.String,
|
|
+ repeat: t.Boolean,
|
|
+ text: t.Optional(t.String),
|
|
+ }
|
|
+ },
|
|
+ 'dispatchTouchEvent': {
|
|
+ params: {
|
|
+ type: t.Enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']),
|
|
+ touchPoints: t.Array(pageTypes.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,
|
|
+ }
|
|
+ },
|
|
+ 'crash': {
|
|
+ params: {}
|
|
+ },
|
|
+ 'handleDialog': {
|
|
+ params: {
|
|
+ dialogId: t.String,
|
|
+ accept: t.Boolean,
|
|
+ promptText: t.Optional(t.String),
|
|
+ },
|
|
+ },
|
|
+ 'setInterceptFileChooserDialog': {
|
|
+ params: {
|
|
+ enabled: t.Boolean,
|
|
+ },
|
|
+ },
|
|
+ 'sendMessageToWorker': {
|
|
+ params: {
|
|
+ frameId: t.String,
|
|
+ workerId: t.String,
|
|
+ message: t.String,
|
|
+ },
|
|
+ },
|
|
+ },
|
|
+};
|
|
+
|
|
+
|
|
+const Accessibility = {
|
|
+ targets: ['page'],
|
|
+ types: axTypes,
|
|
+ events: {},
|
|
+ methods: {
|
|
+ 'getFullAXTree': {
|
|
+ params: {
|
|
+ objectId: t.Optional(t.String),
|
|
+ },
|
|
+ returns: {
|
|
+ tree: axTypes.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..5cc68241bdb420668fd14b45f1a702284a43fad7
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/RuntimeHandler.js
|
|
@@ -0,0 +1,52 @@
|
|
+"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, sessionId, contentChannel) {
|
|
+ this._chromeSession = chromeSession;
|
|
+ this._contentRuntime = contentChannel.connect(sessionId + 'runtime');
|
|
+
|
|
+ const emitProtocolEvent = eventName => {
|
|
+ return (...args) => this._chromeSession.emitEvent(eventName, ...args);
|
|
+ }
|
|
+
|
|
+ this._eventListeners = [
|
|
+ contentChannel.register(sessionId + 'runtime', {
|
|
+ runtimeConsole: emitProtocolEvent('Runtime.console'),
|
|
+ runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
|
|
+ runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),
|
|
+ }),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ async evaluate(options) {
|
|
+ return await this._contentRuntime.send('evaluate', options);
|
|
+ }
|
|
+
|
|
+ async callFunction(options) {
|
|
+ return await this._contentRuntime.send('callFunction', options);
|
|
+ }
|
|
+
|
|
+ async getObjectProperties(options) {
|
|
+ return await this._contentRuntime.send('getObjectProperties', options);
|
|
+ }
|
|
+
|
|
+ async disposeObject(options) {
|
|
+ return await this._contentRuntime.send('disposeObject', options);
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ this._contentRuntime.dispose();
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ }
|
|
+}
|
|
+
|
|
+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..c0bab449971de13f993ac9825ac13368f8d8e226
|
|
--- /dev/null
|
|
+++ b/testing/juggler/protocol/TargetHandler.js
|
|
@@ -0,0 +1,100 @@
|
|
+"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 = [];
|
|
+ this._createdBrowserContextIds = new Set();
|
|
+ }
|
|
+
|
|
+ async attachToTarget({targetId}) {
|
|
+ if (!this._enabled)
|
|
+ throw new Error('Target domain is not enabled');
|
|
+ const sessionId = this._session.dispatcher().createSession(targetId, true /* shouldConnect */);
|
|
+ return {sessionId};
|
|
+ }
|
|
+
|
|
+ async createBrowserContext(options) {
|
|
+ if (!this._enabled)
|
|
+ throw new Error('Target domain is not enabled');
|
|
+ const browserContext = this._contextManager.createBrowserContext(options);
|
|
+ this._createdBrowserContextIds.add(browserContext.browserContextId);
|
|
+ return {browserContextId: browserContext.browserContextId};
|
|
+ }
|
|
+
|
|
+ async removeBrowserContext({browserContextId}) {
|
|
+ if (!this._enabled)
|
|
+ throw new Error('Target domain is not enabled');
|
|
+ this._createdBrowserContextIds.delete(browserContextId);
|
|
+ this._contextManager.browserContextForId(browserContextId).destroy();
|
|
+ }
|
|
+
|
|
+ async getBrowserContexts() {
|
|
+ const browserContexts = this._contextManager.getBrowserContexts();
|
|
+ return {browserContextIds: browserContexts.map(bc => bc.browserContextId)};
|
|
+ }
|
|
+
|
|
+ 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)),
|
|
+ helper.on(this._targetRegistry, TargetRegistry.Events.PageTargetReady, this._onPageTargetReady.bind(this)),
|
|
+ ];
|
|
+ }
|
|
+
|
|
+ dispose() {
|
|
+ helper.removeListeners(this._eventListeners);
|
|
+ for (const browserContextId of this._createdBrowserContextIds) {
|
|
+ const browserContext = this._contextManager.browserContextForId(browserContextId);
|
|
+ if (browserContext.options.removeOnDetach)
|
|
+ browserContext.destroy();
|
|
+ }
|
|
+ this._createdBrowserContextIds.clear();
|
|
+ }
|
|
+
|
|
+ _onTargetCreated(targetInfo) {
|
|
+ this._session.emitEvent('Target.targetCreated', targetInfo);
|
|
+ }
|
|
+
|
|
+ _onTargetChanged(targetInfo) {
|
|
+ this._session.emitEvent('Target.targetInfoChanged', targetInfo);
|
|
+ }
|
|
+
|
|
+ _onTargetDestroyed(targetInfo) {
|
|
+ this._session.emitEvent('Target.targetDestroyed', targetInfo);
|
|
+ }
|
|
+
|
|
+ _onPageTargetReady({sessionIds, targetInfo}) {
|
|
+ if (!this._createdBrowserContextIds.has(targetInfo.browserContextId))
|
|
+ return;
|
|
+ const sessionId = this._session.dispatcher().createSession(targetInfo.targetId, false /* shouldConnect */);
|
|
+ sessionIds.push(sessionId);
|
|
+ }
|
|
+
|
|
+ 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 7e10920c900670949c2bef73715bde0eb4f0ea23..8c0f31c01bce59e24fa83faeae69813b3ee60de7 100644
|
|
--- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
|
|
+++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
|
|
@@ -177,8 +177,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 299230cb3bde5ecd111454ed6f59d1f0504b67a1..3edcf62eefcfbee366566beea4c0899a7a4b59fc 100644
|
|
--- a/toolkit/toolkit.mozbuild
|
|
+++ b/toolkit/toolkit.mozbuild
|
|
@@ -168,6 +168,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 23df86cd223bccc335f7b62621f463ce62919134..372626c7e9e3d194b940befa1444d555d45a8801 100644
|
|
--- a/uriloader/base/nsDocLoader.cpp
|
|
+++ b/uriloader/base/nsDocLoader.cpp
|
|
@@ -758,6 +758,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 (!principal->IsSystemPrincipal())
|
|
+ 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
|
|
@@ -1365,6 +1372,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);
|
|
};
|