From 32c4c2e21067b53a88eda9ce077dcb0ea8d08740 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Wed, 14 Aug 2019 16:13:13 -0700 Subject: [PATCH 01/11] Bug 1561705: Part 1 - Add JSWindowActor.actorCreated callback. r=jdai The JSWindowActor constructor is called before the actor is fully initialized, which means it can't do things like send messages or access its content window. This patch adds a new callback which can do those things immediately after the actor is created. Differential Revision: https://phabricator.services.mozilla.com/D42178 --HG-- extra : rebase_source : ae5b99e690f7c1ebc38726a20bf0d4cf1334cfbe --- dom/chrome-webidl/JSWindowActor.webidl | 15 +++++++++------ dom/docs/Fission.rst | 5 +++++ dom/ipc/JSWindowActor.cpp | 16 ++++++++++------ dom/ipc/JSWindowActor.h | 4 ++-- dom/ipc/JSWindowActorChild.cpp | 2 ++ 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/dom/chrome-webidl/JSWindowActor.webidl b/dom/chrome-webidl/JSWindowActor.webidl index b84ca4b95419..67e0550717d1 100644 --- a/dom/chrome-webidl/JSWindowActor.webidl +++ b/dom/chrome-webidl/JSWindowActor.webidl @@ -73,11 +73,11 @@ callback interface MozObserverCallback { }; /** - * WebIDL callback interface calling the `willDestroy` and `didDestroy` - * method on JSWindowActors. + * WebIDL callback interface calling the `willDestroy`, `didDestroy`, and + * `actorCreated` methods on JSWindowActors. */ [MOZ_CAN_RUN_SCRIPT_BOUNDARY] -callback MozActorDestroyCallback = void(); +callback MozJSWindowActorCallback = void(); /** * The willDestroy method, if present, will be called at the last opportunity @@ -85,13 +85,16 @@ callback MozActorDestroyCallback = void(); * up and send final messages. * The didDestroy method, if present, will be called after the actor is no * longer able to receive any more messages. + * The actorCreated method, if present, will be called immediately after the + * actor has been created and initialized. * * NOTE: Messages may be received between willDestroy and didDestroy, but they * may not be sent. */ -dictionary MozActorDestroyCallbacks { - [ChromeOnly] MozActorDestroyCallback willDestroy; - [ChromeOnly] MozActorDestroyCallback didDestroy; +dictionary MozJSWindowActorCallbacks { + [ChromeOnly] MozJSWindowActorCallback willDestroy; + [ChromeOnly] MozJSWindowActorCallback didDestroy; + [ChromeOnly] MozJSWindowActorCallback actorCreated; }; /** diff --git a/dom/docs/Fission.rst b/dom/docs/Fission.rst index 3149013ba4e7..e70fd875c709 100644 --- a/dom/docs/Fission.rst +++ b/dom/docs/Fission.rst @@ -144,6 +144,11 @@ If you register your Actor to listen for ``nsIObserver`` notifications, implemen If you register your Actor to listen for content events, implement a ``handleEvent`` method with the above signature to handle the event. +``actorCreated`` +```````````````` + +This method is called immediately after a child actor is created and initialized. Unlike the actor's constructor, it is possible to do things like access the actor's content window and send messages from this callback. + ``willDestroy`` ``````````````` diff --git a/dom/ipc/JSWindowActor.cpp b/dom/ipc/JSWindowActor.cpp index c0557059359a..59360ef179a0 100644 --- a/dom/ipc/JSWindowActor.cpp +++ b/dom/ipc/JSWindowActor.cpp @@ -38,17 +38,17 @@ NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(JSWindowActor) JSWindowActor::JSWindowActor() : mNextQueryId(0) {} void JSWindowActor::StartDestroy() { - DestroyCallback(DestroyCallbackFunction::WillDestroy); + InvokeCallback(CallbackFunction::WillDestroy); } void JSWindowActor::AfterDestroy() { - DestroyCallback(DestroyCallbackFunction::DidDestroy); + InvokeCallback(CallbackFunction::DidDestroy); } -void JSWindowActor::DestroyCallback(DestroyCallbackFunction callback) { +void JSWindowActor::InvokeCallback(CallbackFunction callback) { AutoEntryScript aes(GetParentObject(), "JSWindowActor destroy callback"); JSContext* cx = aes.cx(); - MozActorDestroyCallbacks callbacksHolder; + MozJSWindowActorCallbacks callbacksHolder; NS_ENSURE_TRUE_VOID(GetWrapper()); JS::Rooted val(cx, JS::ObjectValue(*GetWrapper())); if (NS_WARN_IF(!callbacksHolder.Init(cx, val))) { @@ -56,14 +56,18 @@ void JSWindowActor::DestroyCallback(DestroyCallbackFunction callback) { } // Destroy callback is optional. - if (callback == DestroyCallbackFunction::WillDestroy) { + if (callback == CallbackFunction::WillDestroy) { if (callbacksHolder.mWillDestroy.WasPassed()) { callbacksHolder.mWillDestroy.Value()->Call(this); } - } else { + } else if (callback == CallbackFunction::DidDestroy) { if (callbacksHolder.mDidDestroy.WasPassed()) { callbacksHolder.mDidDestroy.Value()->Call(this); } + } else { + if (callbacksHolder.mActorCreated.WasPassed()) { + callbacksHolder.mActorCreated.Value()->Call(this); + } } } diff --git a/dom/ipc/JSWindowActor.h b/dom/ipc/JSWindowActor.h index 7f473c124971..cfff05d6e101 100644 --- a/dom/ipc/JSWindowActor.h +++ b/dom/ipc/JSWindowActor.h @@ -39,7 +39,7 @@ class JSWindowActor : public nsISupports, public nsWrapperCache { JSWindowActor(); enum class Type { Parent, Child }; - enum class DestroyCallbackFunction { WillDestroy, DidDestroy }; + enum class CallbackFunction { WillDestroy, DidDestroy, ActorCreated }; const nsString& Name() const { return mName; } @@ -76,7 +76,7 @@ class JSWindowActor : public nsISupports, public nsWrapperCache { void AfterDestroy(); - void DestroyCallback(DestroyCallbackFunction willDestroy); + void InvokeCallback(CallbackFunction willDestroy); private: void ReceiveMessageOrQuery(JSContext* aCx, diff --git a/dom/ipc/JSWindowActorChild.cpp b/dom/ipc/JSWindowActorChild.cpp index 403edfeacfba..5d53b76f1782 100644 --- a/dom/ipc/JSWindowActorChild.cpp +++ b/dom/ipc/JSWindowActorChild.cpp @@ -35,6 +35,8 @@ void JSWindowActorChild::Init(const nsAString& aName, MOZ_ASSERT(!mManager, "Cannot Init() a JSWindowActorChild twice!"); SetName(aName); mManager = aManager; + + InvokeCallback(CallbackFunction::ActorCreated); } namespace { From 9d3b4e9ce3cc93a85d55b6df83c2d8e942aa1f09 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Wed, 14 Aug 2019 16:14:02 -0700 Subject: [PATCH 02/11] Bug 1561705: Part 2 - Use regular SpecialPowers actor for browser chrome tests. r=mccr8 Differential Revision: https://phabricator.services.mozilla.com/D42179 --HG-- extra : rebase_source : 7f4d43232e1a32cb619b83716cf541bdbb1e246f --- ...browser_trackingUI_socialtracking_doorhanger.js | 14 +++++++------- browser/components/downloads/test/browser/head.js | 2 +- .../extensions/test/browser/browser-remote.ini | 6 ++++++ .../components/extensions/test/browser/browser.ini | 2 ++ .../browser/browser_ext_browserAction_context.js | 1 + ...xt_browserAction_pageAction_icon_permissions.js | 2 ++ .../test/browser/browser_ext_pageAction_popup.js | 2 ++ .../browser/browser_ext_pageAction_show_matches.js | 2 ++ .../test/browser/browser_ext_tabs_query.js | 6 +++--- .../test/browser/browser_ext_themes_validation.js | 2 ++ browser/components/extensions/test/browser/head.js | 14 -------------- .../browser/browser_copy_query_without_tree.js | 2 +- devtools/client/framework/test/browser.ini | 4 ++++ devtools/client/framework/test/head.js | 6 ------ .../client/jsonview/test/browser_jsonview_theme.js | 10 +++++----- dom/tests/browser/browser_frame_elements.js | 12 ++++++------ testing/mochitest/browser-test.js | 9 +++++---- testing/specialpowers/api.js | 1 + .../specialpowers/content/SpecialPowersChild.jsm | 5 +++++ 19 files changed, 55 insertions(+), 47 deletions(-) diff --git a/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js b/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js index 3e4349f567c2..0f17e87926bf 100644 --- a/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js +++ b/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js @@ -33,7 +33,7 @@ add_task(async function setup() { async function testPopup(hasPopup, buttonToClick) { let numPageLoaded = gProtectionsHandler._socialTrackingSessionPageLoad; - let numPopupShown = SpecialPowers.getIntPref( + let numPopupShown = Services.prefs.getIntPref( "privacy.socialtracking.notification.counter", 0 ); @@ -89,7 +89,7 @@ async function testPopup(hasPopup, buttonToClick) { // click on the button of the popup notification if (typeof buttonToClick === "string") { is( - SpecialPowers.getBoolPref( + Services.prefs.getBoolPref( "privacy.socialtracking.notification.enabled", false ), @@ -101,7 +101,7 @@ async function testPopup(hasPopup, buttonToClick) { EventUtils.synthesizeMouseAtCenter(notification[buttonToClick], {}); is( - SpecialPowers.getBoolPref( + Services.prefs.getBoolPref( "privacy.socialtracking.notification.enabled", true ), @@ -110,13 +110,13 @@ async function testPopup(hasPopup, buttonToClick) { ); } - let lastShown = SpecialPowers.getCharPref( + let lastShown = Services.prefs.getCharPref( "privacy.socialtracking.notification.lastShown", "0" ); ok(lastShown !== "0", "last shown timestamp updated"); is( - SpecialPowers.getIntPref( + Services.prefs.getIntPref( "privacy.socialtracking.notification.counter", 0 ), @@ -221,12 +221,12 @@ add_task(async function testSocialTrackingPopups() { for (let config of configs) { ok(config.description, config.description); - SpecialPowers.pushPrefEnv({ + await SpecialPowers.pushPrefEnv({ set: config.prefs, }); for (let result of config.results) { await testPopup(result, config.button); } - SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); } }); diff --git a/browser/components/downloads/test/browser/head.js b/browser/components/downloads/test/browser/head.js index 910c2ce59fa9..14210c3ddbdc 100644 --- a/browser/components/downloads/test/browser/head.js +++ b/browser/components/downloads/test/browser/head.js @@ -141,7 +141,7 @@ async function setDownloadDir() { await SpecialPowers.pushPrefEnv({ set: [ ["browser.download.folderList", 2], - ["browser.download.dir", tmpDir, Ci.nsIFile], + ["browser.download.dir", tmpDir.path], ], }); } diff --git a/browser/components/extensions/test/browser/browser-remote.ini b/browser/components/extensions/test/browser/browser-remote.ini index 9e7090f1cb77..c4e8a600a5cf 100644 --- a/browser/components/extensions/test/browser/browser-remote.ini +++ b/browser/components/extensions/test/browser/browser-remote.ini @@ -6,6 +6,12 @@ # in this manifest to the sub-directory "test-oop-extensions", and then check # whether we're running from that directory from head.js install-to-subdir = test-oop-extensions +prefs = + extensions.webextensions.remote=true + # We don't want to reset this at the end of the test, so that we don't have + # to spawn a new extension child process for each test unit. + dom.ipc.keepProcessesAlive.extension=1 + tags = webextensions remote-webextensions skip-if = !e10s support-files = diff --git a/browser/components/extensions/test/browser/browser.ini b/browser/components/extensions/test/browser/browser.ini index 29c36260931c..3d2daa718851 100644 --- a/browser/components/extensions/test/browser/browser.ini +++ b/browser/components/extensions/test/browser/browser.ini @@ -2,6 +2,8 @@ tags = webextensions in-process-webextensions support-files = head.js +prefs = + extensions.webextensions.remote=false [browser_ext_autocompletepopup.js] [browser_ext_windows_allowScriptsToClose.js] diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js index d27d6d2f6235..51f3b545ebcc 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js @@ -546,6 +546,7 @@ add_task(async function testBadgeColorPersistence() { add_task(async function testPropertyRemoval() { await runTests({ manifest: { + name: "Generated extension", browser_action: { default_icon: "default.png", default_popup: "default.html", diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js index b7cd3cc01cef..08e431d26fbb 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js @@ -2,6 +2,8 @@ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; +PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/); + // Test that an error is thrown when providing invalid icon sizes add_task(async function testInvalidIconSizes() { let extension = ExtensionTestUtils.loadExtension({ diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js index 411581c99a79..208eb24d10c5 100644 --- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js @@ -2,6 +2,8 @@ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; +PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/); + const { GlobalManager } = ChromeUtils.import( "resource://gre/modules/Extension.jsm", null diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js index b37ada26f59c..4e1ba1b251f9 100644 --- a/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js @@ -2,6 +2,8 @@ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; +PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/); + function getExtension(page_action) { return ExtensionTestUtils.loadExtension({ manifest: { diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_query.js b/browser/components/extensions/test/browser/browser_ext_tabs_query.js index 6268d89cf4d1..cac9a3eef98e 100644 --- a/browser/components/extensions/test/browser/browser_ext_tabs_query.js +++ b/browser/components/extensions/test/browser/browser_ext_tabs_query.js @@ -316,13 +316,13 @@ add_task(async function() { const RESOLUTION_PREF = "layout.css.devPixelsPerPx"; registerCleanupFunction(() => { - SpecialPowers.clearUserPref(RESOLUTION_PREF); + Services.prefs.clearUserPref(RESOLUTION_PREF); }); await Promise.all([extension.startup(), extension.awaitMessage("ready")]); for (let resolution of [2, 1]) { - SpecialPowers.setCharPref(RESOLUTION_PREF, String(resolution)); + Services.prefs.setCharPref(RESOLUTION_PREF, String(resolution)); is( window.devicePixelRatio, resolution, @@ -342,7 +342,7 @@ add_task(async function() { BrowserTestUtils.removeTab(tab1); BrowserTestUtils.removeTab(tab2); BrowserTestUtils.removeTab(tab3); - SpecialPowers.clearUserPref(RESOLUTION_PREF); + Services.prefs.clearUserPref(RESOLUTION_PREF); }); add_task(async function testQueryPermissions() { diff --git a/browser/components/extensions/test/browser/browser_ext_themes_validation.js b/browser/components/extensions/test/browser/browser_ext_themes_validation.js index 63f1793a5e34..9281fcc0e0de 100644 --- a/browser/components/extensions/test/browser/browser_ext_themes_validation.js +++ b/browser/components/extensions/test/browser/browser_ext_themes_validation.js @@ -1,5 +1,7 @@ "use strict"; +PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/); + /** * Helper function for testing a theme with invalid properties. * @param {object} invalidProps The invalid properties to load the theme with. diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js index 337d44160a13..b7d515645a08 100644 --- a/browser/components/extensions/test/browser/head.js +++ b/browser/components/extensions/test/browser/head.js @@ -68,20 +68,6 @@ function loadTestSubscript(filePath) { Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this); } -// We run tests under two different configurations, from browser.ini and -// browser-remote.ini. When running from browser-remote.ini, the tests are -// copied to the sub-directory "test-oop-extensions", which we detect here, and -// use to select our configuration. -let remote = gTestPath.includes("test-oop-extensions"); -SpecialPowers.pushPrefEnv({ - set: [["extensions.webextensions.remote", remote]], -}); -if (remote) { - // We don't want to reset this at the end of the test, so that we don't have - // to spawn a new extension child process for each test unit. - SpecialPowers.setIntPref("dom.ipc.keepProcessesAlive.extension", 1); -} - // Don't try to create screenshots of sites we load during tests. Services.prefs .getDefaultBranch("browser.newtabpage.activity-stream.") diff --git a/browser/components/places/tests/browser/browser_copy_query_without_tree.js b/browser/components/places/tests/browser/browser_copy_query_without_tree.js index 91c12b5bbd69..fbe36a8804a5 100644 --- a/browser/components/places/tests/browser/browser_copy_query_without_tree.js +++ b/browser/components/places/tests/browser/browser_copy_query_without_tree.js @@ -43,7 +43,7 @@ add_task(async function copy_toolbar_shortcut() { }); add_task(async function copy_mobile_shortcut() { - SpecialPowers.pushPrefEnv({ + await SpecialPowers.pushPrefEnv({ set: [["browser.bookmarks.showMobileBookmarks", true]], }); await promisePlacesInitComplete(); diff --git a/devtools/client/framework/test/browser.ini b/devtools/client/framework/test/browser.ini index 185430b9e81b..9d10b8fab2e5 100644 --- a/devtools/client/framework/test/browser.ini +++ b/devtools/client/framework/test/browser.ini @@ -53,6 +53,10 @@ support-files = !/devtools/client/shared/test/shared-head.js !/devtools/client/shared/test/shared-redux-head.js !/devtools/client/shared/test/telemetry-test-helpers.js +# This is far from ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1565279 +# covers removing this pref flip. +prefs = + security.allow_unsafe_parent_loads=true [browser_about-devtools-toolbox_load.js] [browser_about-devtools-toolbox_reload.js] diff --git a/devtools/client/framework/test/head.js b/devtools/client/framework/test/head.js index 680e0328265f..fa53d9b6fa60 100644 --- a/devtools/client/framework/test/head.js +++ b/devtools/client/framework/test/head.js @@ -12,12 +12,6 @@ Services.scriptloader.loadSubScript( const EventEmitter = require("devtools/shared/event-emitter"); -// This is far from ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1565279 -// covers removing this pref flip. -SpecialPowers.pushPrefEnv({ - set: [["security.allow_unsafe_parent_loads", true]], -}); - function toggleAllTools(state) { for (const [, tool] of gDevTools._tools) { if (!tool.visibilityswitch) { diff --git a/devtools/client/jsonview/test/browser_jsonview_theme.js b/devtools/client/jsonview/test/browser_jsonview_theme.js index 6dc43d0cb0cd..ab634ce073f7 100644 --- a/devtools/client/jsonview/test/browser_jsonview_theme.js +++ b/devtools/client/jsonview/test/browser_jsonview_theme.js @@ -8,20 +8,20 @@ const TEST_JSON_URL = URL_ROOT + "valid_json.json"; add_task(async function() { info("Test JSON theme started."); - const oldPref = SpecialPowers.getCharPref("devtools.theme"); - SpecialPowers.setCharPref("devtools.theme", "light"); + const oldPref = Services.prefs.getCharPref("devtools.theme"); + Services.prefs.setCharPref("devtools.theme", "light"); await addJsonViewTab(TEST_JSON_URL); is(await getTheme(), "theme-light", "The initial theme is light"); - SpecialPowers.setCharPref("devtools.theme", "dark"); + Services.prefs.setCharPref("devtools.theme", "dark"); is(await getTheme(), "theme-dark", "Theme changed to dark"); - SpecialPowers.setCharPref("devtools.theme", "light"); + Services.prefs.setCharPref("devtools.theme", "light"); is(await getTheme(), "theme-light", "Theme changed to light"); - SpecialPowers.setCharPref("devtools.theme", oldPref); + Services.prefs.setCharPref("devtools.theme", oldPref); }); function getTheme() { diff --git a/dom/tests/browser/browser_frame_elements.js b/dom/tests/browser/browser_frame_elements.js index ae4a6c89716a..08bd1df65130 100644 --- a/dom/tests/browser/browser_frame_elements.js +++ b/dom/tests/browser/browser_frame_elements.js @@ -81,9 +81,9 @@ function startTests() { async function mozBrowserTests(browser) { info("Granting special powers for mozbrowser"); - SpecialPowers.addPermission("browser", true, TEST_URI); - SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true); - SpecialPowers.setBoolPref("network.disable.ipc.security", true); + await SpecialPowers.addPermission("browser", true, TEST_URI); + Services.prefs.setBoolPref("dom.mozBrowserFramesEnabled", true); + Services.prefs.setBoolPref("network.disable.ipc.security", true); await ContentTask.spawn(browser, null, function() { info("Checking mozbrowser iframe"); @@ -103,7 +103,7 @@ async function mozBrowserTests(browser) { }); info("Revoking special powers for mozbrowser"); - SpecialPowers.clearUserPref("dom.mozBrowserFramesEnabled"); - SpecialPowers.clearUserPref("network.disable.ipc.security"); - SpecialPowers.removePermission("browser", TEST_URI); + Services.prefs.clearUserPref("dom.mozBrowserFramesEnabled"); + Services.prefs.clearUserPref("network.disable.ipc.security"); + await SpecialPowers.removePermission("browser", TEST_URI); } diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js index 62ecbe93ffd0..9d7c541020de 100644 --- a/testing/mochitest/browser-test.js +++ b/testing/mochitest/browser-test.js @@ -457,11 +457,12 @@ function Tester(aTests, structuredLogger, aCallback) { this.cpowEventUtils ); + // Make sure our SpecialPowers actor is instantiated, in case it was + // registered after our DOMWindowCreated event was fired (which it + // most likely was). + window.getWindowGlobalChild().getActor("SpecialPowers"); + var simpleTestScope = {}; - this._scriptLoader.loadSubScript( - "chrome://mochikit/content/tests/SimpleTest/ChromePowers.js", - simpleTestScope - ); this._scriptLoader.loadSubScript( "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", simpleTestScope diff --git a/testing/specialpowers/api.js b/testing/specialpowers/api.js index cd862b03e927..db7aee981476 100644 --- a/testing/specialpowers/api.js +++ b/testing/specialpowers/api.js @@ -35,6 +35,7 @@ this.specialpowers = class extends ExtensionAPI { ChromeUtils.registerWindowActor("SpecialPowers", { allFrames: true, + includeChrome: true, child: { moduleURI: "resource://specialpowers/SpecialPowersChild.jsm", events: { diff --git a/testing/specialpowers/content/SpecialPowersChild.jsm b/testing/specialpowers/content/SpecialPowersChild.jsm index 0d3064f6136a..8846bbc51dd6 100644 --- a/testing/specialpowers/content/SpecialPowersChild.jsm +++ b/testing/specialpowers/content/SpecialPowersChild.jsm @@ -46,6 +46,11 @@ class SpecialPowersChild extends SpecialPowersAPI { } handleEvent(aEvent) { + // We don't actually care much about the "DOMWindowCreated" event. + // We only listen to it to force creation of the actor. + } + + actorCreated() { this.attachToWindow(); } From 6e41908abd49c2bff1d032b6b0ac51aac7db7966 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Wed, 14 Aug 2019 16:18:32 -0700 Subject: [PATCH 03/11] Bug 1561705: Part 3 - Get rid of remaining ChromePowers.js consumers. r=mccr8 Differential Revision: https://phabricator.services.mozilla.com/D42180 --HG-- extra : rebase_source : aec54cf0a38daad4720ae1939da342e1576b7f04 --- docshell/test/chrome/bug293235_window.xul | 1 - docshell/test/chrome/bug396649_window.xul | 1 - docshell/test/chrome/bug89419_window.xul | 1 - testing/mochitest/moz.build | 1 - .../tests/SimpleTest/ChromePowers.js | 120 ------------------ .../content/SpecialPowersAPI.jsm | 3 - .../content/SpecialPowersChild.jsm | 6 +- 7 files changed, 1 insertion(+), 132 deletions(-) delete mode 100644 testing/mochitest/tests/SimpleTest/ChromePowers.js diff --git a/docshell/test/chrome/bug293235_window.xul b/docshell/test/chrome/bug293235_window.xul index a561e6d9ce2d..76c0033ef698 100644 --- a/docshell/test/chrome/bug293235_window.xul +++ b/docshell/test/chrome/bug293235_window.xul @@ -8,7 +8,6 @@ onload="setTimeout(runTests, 0);" title="bug 293235 test"> - diff --git a/docshell/test/chrome/bug396649_window.xul b/docshell/test/chrome/bug396649_window.xul index ad6a894b1c76..ef8151cad2f4 100755 --- a/docshell/test/chrome/bug396649_window.xul +++ b/docshell/test/chrome/bug396649_window.xul @@ -8,7 +8,6 @@ onload="setTimeout(nextTest, 0);" title="bug 396649 test"> - diff --git a/testing/mochitest/moz.build b/testing/mochitest/moz.build index b34a9478927c..a6dadf3e277e 100644 --- a/testing/mochitest/moz.build +++ b/testing/mochitest/moz.build @@ -47,7 +47,6 @@ FINAL_TARGET_FILES.content.tests.SimpleTest += [ '../../docshell/test/chrome/docshell_helpers.js', '../modules/StructuredLog.jsm', 'tests/SimpleTest/AsyncUtilsContent.js', - 'tests/SimpleTest/ChromePowers.js', 'tests/SimpleTest/EventUtils.js', 'tests/SimpleTest/ExtensionTestUtils.js', 'tests/SimpleTest/iframe-between-tests.html', diff --git a/testing/mochitest/tests/SimpleTest/ChromePowers.js b/testing/mochitest/tests/SimpleTest/ChromePowers.js deleted file mode 100644 index 12255615b16e..000000000000 --- a/testing/mochitest/tests/SimpleTest/ChromePowers.js +++ /dev/null @@ -1,120 +0,0 @@ -/* 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/. */ - -const {SpecialPowersAPI, bindDOMWindowUtils} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPI.jsm"); -const {SpecialPowersAPIParent} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPIParent.jsm"); - -class ChromePowers extends SpecialPowersAPI { - constructor(window) { - super(); - - this.window = Cu.getWeakReference(window); - - this.chromeWindow = window; - - this.DOMWindowUtils = bindDOMWindowUtils(window); - - this.parentActor = new SpecialPowersAPIParent(); - this.parentActor.sendAsyncMessage = this.sendReply.bind(this); - - this.listeners = new Map(); - } - - toString() { return "[ChromePowers]"; } - sanityCheck() { return "foo"; } - - get contentWindow() { - return window; - } - - get document() { - return window.document; - } - - get docShell() { - return window.docShell; - } - - sendReply(aType, aMsg) { - var msg = {name: aType, json: aMsg, data: aMsg}; - if (!this.listeners.has(aType)) { - throw new Error(`No listener for ${aType}`); - } - this.listeners.get(aType)(msg); - } - - sendAsyncMessage(aType, aMsg) { - var msg = {name: aType, json: aMsg, data: aMsg}; - this.receiveMessage(msg); - } - - async sendQuery(aType, aMsg) { - var msg = {name: aType, json: aMsg, data: aMsg}; - return this.receiveMessage(msg); - } - - _addMessageListener(aType, aCallback) { - if (this.listeners.has(aType)) { - throw new Error(`unable to handle multiple listeners for ${aType}`); - } - this.listeners.set(aType, aCallback); - } - _removeMessageListener(aType, aCallback) { - this.listeners.delete(aType); - } - - registerProcessCrashObservers() { - this._sendSyncMessage("SPProcessCrashService", { op: "register-observer" }); - } - - unregisterProcessCrashObservers() { - this._sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" }); - } - - receiveMessage(aMessage) { - switch (aMessage.name) { - case "SpecialPowers.Quit": - let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup); - appStartup.quit(Ci.nsIAppStartup.eForceQuit); - break; - case "SPProcessCrashService": - if (aMessage.json.op == "register-observer" || aMessage.json.op == "unregister-observer") { - // Hack out register/unregister specifically for browser-chrome leaks - break; - } - default: - // All calls go here, because we need to handle SPProcessCrashService calls as well - return this.parentActor.receiveMessage(aMessage); - } - return undefined; - } - - quit() { - // We come in here as SpecialPowers.quit, but SpecialPowers is really ChromePowers. - // For some reason this. resolves to TestRunner, so using SpecialPowers - // allows us to use the ChromePowers object which we defined below. - SpecialPowers._sendSyncMessage("SpecialPowers.Quit", {}); - } - - focus(aWindow) { - // We come in here as SpecialPowers.focus, but SpecialPowers is really ChromePowers. - // For some reason this. resolves to TestRunner, so using SpecialPowers - // allows us to use the ChromePowers object which we defined below. - if (aWindow) - aWindow.focus(); - } - - executeAfterFlushingMessageQueue(aCallback) { - aCallback(); - } -} - -if (window.parent.SpecialPowers && !window.SpecialPowers) { - window.SpecialPowers = window.parent.SpecialPowers; -} else { - ChromeUtils.import("resource://specialpowers/SpecialPowersAPIParent.jsm", this); - - window.SpecialPowers = new ChromePowers(window); -} - diff --git a/testing/specialpowers/content/SpecialPowersAPI.jsm b/testing/specialpowers/content/SpecialPowersAPI.jsm index b695a66e013c..ffa483ff5028 100644 --- a/testing/specialpowers/content/SpecialPowersAPI.jsm +++ b/testing/specialpowers/content/SpecialPowersAPI.jsm @@ -748,9 +748,6 @@ class SpecialPowersAPI extends JSWindowActorChild { * To get the expected data, you should modify * SpecialPowersObserver.prototype._registerObservers.observe. Or the message * you received from messageManager will only contain 'aData' from Service.obs. - * - * NOTICE: there is no implementation of _addMessageListener in - * ChromePowers.js */ registerObservers(topic) { var msg = { diff --git a/testing/specialpowers/content/SpecialPowersChild.jsm b/testing/specialpowers/content/SpecialPowersChild.jsm index 8846bbc51dd6..aa323e6295ed 100644 --- a/testing/specialpowers/content/SpecialPowersChild.jsm +++ b/testing/specialpowers/content/SpecialPowersChild.jsm @@ -7,11 +7,7 @@ var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -var EXPORTED_SYMBOLS = [ - "SpecialPowers", - "SpecialPowersChild", - "attachSpecialPowersToWindow", -]; +var EXPORTED_SYMBOLS = ["SpecialPowersChild"]; const { bindDOMWindowUtils, SpecialPowersAPI } = ChromeUtils.import( "resource://specialpowers/SpecialPowersAPI.jsm" From a0da8c1f6d71846243df4feb554d5616afcf98ce Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Thu, 15 Aug 2019 12:54:30 -0700 Subject: [PATCH 04/11] Bug 1561705: Part 4a - Remove SpecialPowersChild.jsm in preparation for merge with SpecialPowersAPI.jsm. r=mccr8 Differential Revision: https://phabricator.services.mozilla.com/D42183 --HG-- extra : rebase_source : 7ca65921bb6653309f7c0923782feb83ea22511e --- .../content/SpecialPowersChild.jsm | 215 ------------------ 1 file changed, 215 deletions(-) delete mode 100644 testing/specialpowers/content/SpecialPowersChild.jsm diff --git a/testing/specialpowers/content/SpecialPowersChild.jsm b/testing/specialpowers/content/SpecialPowersChild.jsm deleted file mode 100644 index aa323e6295ed..000000000000 --- a/testing/specialpowers/content/SpecialPowersChild.jsm +++ /dev/null @@ -1,215 +0,0 @@ -/* 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/. */ -/* This code is loaded in every child process that is started by mochitest in - * order to be used as a replacement for UniversalXPConnect - */ - -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - -var EXPORTED_SYMBOLS = ["SpecialPowersChild"]; - -const { bindDOMWindowUtils, SpecialPowersAPI } = ChromeUtils.import( - "resource://specialpowers/SpecialPowersAPI.jsm" -); -const { ExtensionUtils } = ChromeUtils.import( - "resource://gre/modules/ExtensionUtils.jsm" -); - -Cu.forcePermissiveCOWs(); - -class SpecialPowersChild extends SpecialPowersAPI { - constructor() { - super(); - - this._windowID = null; - this.DOMWindowUtils = null; - - this._encounteredCrashDumpFiles = []; - this._unexpectedCrashDumpFiles = {}; - this._crashDumpDir = null; - this._serviceWorkerRegistered = false; - this._serviceWorkerCleanUpRequests = new Map(); - Object.defineProperty(this, "Components", { - configurable: true, - enumerable: true, - value: this.getFullComponents(), - }); - this._createFilesOnError = null; - this._createFilesOnSuccess = null; - - this._messageListeners = new ExtensionUtils.DefaultMap(() => new Set()); - } - - handleEvent(aEvent) { - // We don't actually care much about the "DOMWindowCreated" event. - // We only listen to it to force creation of the actor. - } - - actorCreated() { - this.attachToWindow(); - } - - attachToWindow() { - let window = this.contentWindow; - if (!window.wrappedJSObject.SpecialPowers) { - this._windowID = window.windowUtils.currentInnerWindowID; - this.DOMWindowUtils = bindDOMWindowUtils(window); - - window.SpecialPowers = this; - window.wrappedJSObject.SpecialPowers = this; - if (this.IsInNestedFrame) { - this.addPermission("allowXULXBL", true, window.document); - } - } - } - - get window() { - return this.contentWindow; - } - - toString() { - return "[SpecialPowers]"; - } - sanityCheck() { - return "foo"; - } - - _addMessageListener(msgname, listener) { - this._messageListeners.get(msgname).add(listener); - } - - _removeMessageListener(msgname, listener) { - this._messageListeners.get(msgname).delete(listener); - } - - registerProcessCrashObservers() { - this.sendAsyncMessage("SPProcessCrashService", { op: "register-observer" }); - } - - unregisterProcessCrashObservers() { - this.sendAsyncMessage("SPProcessCrashService", { - op: "unregister-observer", - }); - } - - receiveMessage(aMessage) { - if (this._messageListeners.has(aMessage.name)) { - for (let listener of this._messageListeners.get(aMessage.name)) { - try { - if (typeof listener === "function") { - listener(aMessage); - } else { - listener.receiveMessage(aMessage); - } - } catch (e) { - Cu.reportError(e); - } - } - } - - switch (aMessage.name) { - case "SPProcessCrashService": - if (aMessage.json.type == "crash-observed") { - for (let e of aMessage.json.dumpIDs) { - this._encounteredCrashDumpFiles.push(e.id + "." + e.extension); - } - } - break; - - case "SPServiceWorkerRegistered": - this._serviceWorkerRegistered = aMessage.data.registered; - break; - - case "SpecialPowers.FilesCreated": - var createdHandler = this._createFilesOnSuccess; - this._createFilesOnSuccess = null; - this._createFilesOnError = null; - if (createdHandler) { - createdHandler(Cu.cloneInto(aMessage.data, this.contentWindow)); - } - break; - - case "SpecialPowers.FilesError": - var errorHandler = this._createFilesOnError; - this._createFilesOnSuccess = null; - this._createFilesOnError = null; - if (errorHandler) { - errorHandler(aMessage.data); - } - break; - - case "Spawn": - let { task, args, caller, taskId } = aMessage.data; - return this._spawnTask(task, args, caller, taskId); - - default: - return super.receiveMessage(aMessage); - } - - return true; - } - - quit() { - this.sendAsyncMessage("SpecialPowers.Quit", {}); - } - - // fileRequests is an array of file requests. Each file request is an object. - // A request must have a field |name|, which gives the base of the name of the - // file to be created in the profile directory. If the request has a |data| field - // then that data will be written to the file. - createFiles(fileRequests, onCreation, onError) { - return this.sendQuery("SpecialPowers.CreateFiles", fileRequests).then( - onCreation, - onError - ); - } - - // Remove the files that were created using |SpecialPowers.createFiles()|. - // This will be automatically called by |SimpleTest.finish()|. - removeFiles() { - this.sendAsyncMessage("SpecialPowers.RemoveFiles", {}); - } - - executeAfterFlushingMessageQueue(aCallback) { - return this.sendQuery("Ping").then(aCallback); - } - - async registeredServiceWorkers() { - // For the time being, if parent_intercept is false, we can assume that - // ServiceWorkers registered by the current test are all known to the SWM in - // this process. - if ( - !Services.prefs.getBoolPref("dom.serviceWorkers.parent_intercept", false) - ) { - let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( - Ci.nsIServiceWorkerManager - ); - let regs = swm.getAllRegistrations(); - - // XXX This is shared with SpecialPowersAPIParent.jsm - let workers = new Array(regs.length); - for (let i = 0; i < workers.length; ++i) { - let { scope, scriptSpec } = regs.queryElementAt( - i, - Ci.nsIServiceWorkerRegistrationInfo - ); - workers[i] = { scope, scriptSpec }; - } - - return workers; - } - - // Please see the comment in SpecialPowersObserver.jsm above - // this._serviceWorkerListener's assignment for what this returns. - if (this._serviceWorkerRegistered) { - // This test registered at least one service worker. Send a synchronous - // call to the parent to make sure that it called unregister on all of its - // service workers. - let { workers } = await this.sendQuery("SPCheckServiceWorkers"); - return workers; - } - - return []; - } -} From d4c6ef34b04e04ad460e07030c1df81535fba3d2 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Wed, 14 Aug 2019 16:32:59 -0700 Subject: [PATCH 05/11] Bug 1561705: Part 4b - Merge SpecialPowersAPI.jsm and SpecialPowersChild.jsm. r=mccr8 Differential Revision: https://phabricator.services.mozilla.com/D42181 --HG-- rename : testing/specialpowers/content/SpecialPowersAPI.jsm => testing/specialpowers/content/SpecialPowersChild.jsm extra : rebase_source : c25554eb016d08edb730a838401ae2ff8785ba96 --- .../performance/browser_startup_content.js | 6 +- ...alPowersAPI.jsm => SpecialPowersChild.jsm} | 214 ++++++++++++++++-- testing/specialpowers/moz.build | 1 - 3 files changed, 200 insertions(+), 21 deletions(-) rename testing/specialpowers/content/{SpecialPowersAPI.jsm => SpecialPowersChild.jsm} (90%) diff --git a/browser/base/content/test/performance/browser_startup_content.js b/browser/base/content/test/performance/browser_startup_content.js index 6d0ce67954b5..1df6116eaf95 100644 --- a/browser/base/content/test/performance/browser_startup_content.js +++ b/browser/base/content/test/performance/browser_startup_content.js @@ -20,9 +20,6 @@ const kDumpAllStacks = false; const whitelist = { modules: new Set([ "chrome://mochikit/content/ShutdownLeaksCollector.jsm", - "resource://specialpowers/SpecialPowersChild.jsm", - "resource://specialpowers/SpecialPowersAPI.jsm", - "resource://specialpowers/WrapPrivileged.jsm", "resource://gre/modules/ContentProcessSingleton.jsm", @@ -95,6 +92,9 @@ const intermittently_loaded_whitelist = { "resource://gre/modules/nsAsyncShutdown.jsm", "resource://gre/modules/sessionstore/Utils.jsm", + "resource://specialpowers/SpecialPowersChild.jsm", + "resource://specialpowers/WrapPrivileged.jsm", + // Webcompat about:config front-end. This is presently nightly-only and // part of a system add-on which may not load early enough for the test. "resource://webcompat/AboutCompat.jsm", diff --git a/testing/specialpowers/content/SpecialPowersAPI.jsm b/testing/specialpowers/content/SpecialPowersChild.jsm similarity index 90% rename from testing/specialpowers/content/SpecialPowersAPI.jsm rename to testing/specialpowers/content/SpecialPowersChild.jsm index ffa483ff5028..44b2791c79a0 100644 --- a/testing/specialpowers/content/SpecialPowersAPI.jsm +++ b/testing/specialpowers/content/SpecialPowersChild.jsm @@ -7,10 +7,14 @@ "use strict"; -var EXPORTED_SYMBOLS = ["SpecialPowersAPI", "bindDOMWindowUtils"]; +var EXPORTED_SYMBOLS = ["SpecialPowersChild"]; var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { ExtensionUtils } = ChromeUtils.import( + "resource://gre/modules/ExtensionUtils.jsm" +); + ChromeUtils.defineModuleGetter( this, "MockFilePicker", @@ -141,10 +145,28 @@ SPConsoleListener.prototype = { ]), }; -class SpecialPowersAPI extends JSWindowActorChild { +class SpecialPowersChild extends JSWindowActorChild { constructor() { super(); + this._windowID = null; + this.DOMWindowUtils = null; + + this._encounteredCrashDumpFiles = []; + this._unexpectedCrashDumpFiles = {}; + this._crashDumpDir = null; + this._serviceWorkerRegistered = false; + this._serviceWorkerCleanUpRequests = new Map(); + Object.defineProperty(this, "Components", { + configurable: true, + enumerable: true, + value: this.getFullComponents(), + }); + this._createFilesOnError = null; + this._createFilesOnSuccess = null; + + this._messageListeners = new ExtensionUtils.DefaultMap(() => new Set()); + this._consoleListeners = []; this._encounteredCrashDumpFiles = []; this._unexpectedCrashDumpFiles = {}; @@ -161,13 +183,103 @@ class SpecialPowersAPI extends JSWindowActorChild { this._extensionListeners = null; } + handleEvent(aEvent) { + // We don't actually care much about the "DOMWindowCreated" event. + // We only listen to it to force creation of the actor. + } + + actorCreated() { + this.attachToWindow(); + } + + attachToWindow() { + let window = this.contentWindow; + if (!window.wrappedJSObject.SpecialPowers) { + this._windowID = window.windowUtils.currentInnerWindowID; + this.DOMWindowUtils = bindDOMWindowUtils(window); + + window.SpecialPowers = this; + window.wrappedJSObject.SpecialPowers = this; + if (this.IsInNestedFrame) { + this.addPermission("allowXULXBL", true, window.document); + } + } + } + + get window() { + return this.contentWindow; + } + // Hack around devtools sometimes trying to JSON stringify us. toJSON() { return {}; } + toString() { + return "[SpecialPowers]"; + } + sanityCheck() { + return "foo"; + } + + _addMessageListener(msgname, listener) { + this._messageListeners.get(msgname).add(listener); + } + + _removeMessageListener(msgname, listener) { + this._messageListeners.get(msgname).delete(listener); + } + receiveMessage(message) { + if (this._messageListeners.has(message.name)) { + for (let listener of this._messageListeners.get(message.name)) { + try { + if (typeof listener === "function") { + listener(message); + } else { + listener.receiveMessage(message); + } + } catch (e) { + Cu.reportError(e); + } + } + } + switch (message.name) { + case "SPProcessCrashService": + if (message.json.type == "crash-observed") { + for (let e of message.json.dumpIDs) { + this._encounteredCrashDumpFiles.push(e.id + "." + e.extension); + } + } + break; + + case "SPServiceWorkerRegistered": + this._serviceWorkerRegistered = message.data.registered; + break; + + case "SpecialPowers.FilesCreated": + var createdHandler = this._createFilesOnSuccess; + this._createFilesOnSuccess = null; + this._createFilesOnError = null; + if (createdHandler) { + createdHandler(Cu.cloneInto(message.data, this.contentWindow)); + } + break; + + case "SpecialPowers.FilesError": + var errorHandler = this._createFilesOnError; + this._createFilesOnSuccess = null; + this._createFilesOnError = null; + if (errorHandler) { + errorHandler(message.data); + } + break; + + case "Spawn": + let { task, args, caller, taskId } = message.data; + return this._spawnTask(task, args, caller, taskId); + case "Assert": { // An assertion has been done in a mochitest chrome script @@ -193,6 +305,16 @@ class SpecialPowersAPI extends JSWindowActorChild { return undefined; } + registerProcessCrashObservers() { + this.sendAsyncMessage("SPProcessCrashService", { op: "register-observer" }); + } + + unregisterProcessCrashObservers() { + this.sendAsyncMessage("SPProcessCrashService", { + op: "unregister-observer", + }); + } + /* * Privileged object wrapping API * @@ -290,6 +412,69 @@ class SpecialPowersAPI extends JSWindowActorChild { return MockPermissionPrompt; } + quit() { + this.sendAsyncMessage("SpecialPowers.Quit", {}); + } + + // fileRequests is an array of file requests. Each file request is an object. + // A request must have a field |name|, which gives the base of the name of the + // file to be created in the profile directory. If the request has a |data| field + // then that data will be written to the file. + createFiles(fileRequests, onCreation, onError) { + return this.sendQuery("SpecialPowers.CreateFiles", fileRequests).then( + onCreation, + onError + ); + } + + // Remove the files that were created using |SpecialPowers.createFiles()|. + // This will be automatically called by |SimpleTest.finish()|. + removeFiles() { + this.sendAsyncMessage("SpecialPowers.RemoveFiles", {}); + } + + executeAfterFlushingMessageQueue(aCallback) { + return this.sendQuery("Ping").then(aCallback); + } + + async registeredServiceWorkers() { + // For the time being, if parent_intercept is false, we can assume that + // ServiceWorkers registered by the current test are all known to the SWM in + // this process. + if ( + !Services.prefs.getBoolPref("dom.serviceWorkers.parent_intercept", false) + ) { + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + let regs = swm.getAllRegistrations(); + + // XXX This is shared with SpecialPowersAPIParent.jsm + let workers = new Array(regs.length); + for (let i = 0; i < workers.length; ++i) { + let { scope, scriptSpec } = regs.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + workers[i] = { scope, scriptSpec }; + } + + return workers; + } + + // Please see the comment in SpecialPowersObserver.jsm above + // this._serviceWorkerListener's assignment for what this returns. + if (this._serviceWorkerRegistered) { + // This test registered at least one service worker. Send a synchronous + // call to the parent to make sure that it called unregister on all of its + // service workers. + let { workers } = await this.sendQuery("SPCheckServiceWorkers"); + return workers; + } + + return []; + } + /* * Load a privileged script that runs same-process. This is different from * |loadChromeScript|, which will run in the parent process in e10s mode. @@ -1604,10 +1789,8 @@ class SpecialPowersAPI extends JSWindowActorChild { } get isDebugBuild() { - delete SpecialPowersAPI.prototype.isDebugBuild; - - var debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); - return (SpecialPowersAPI.prototype.isDebugBuild = debug.isDebugBuild); + return Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2) + .isDebugBuild; } assertionCount() { var debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); @@ -2027,7 +2210,7 @@ class SpecialPowersAPI extends JSWindowActorChild { } } -SpecialPowersAPI.prototype._proxiedObservers = { +SpecialPowersChild.prototype._proxiedObservers = { "specialpowers-http-notify-request": function(aMessage) { let uri = aMessage.json.uri; Services.obs.notifyObservers( @@ -2042,10 +2225,10 @@ SpecialPowersAPI.prototype._proxiedObservers = { }, }; -SpecialPowersAPI.prototype.permissionObserverProxy = { +SpecialPowersChild.prototype.permissionObserverProxy = { // 'this' in permChangedObserverProxy is the permChangedObserverProxy - // object itself. The '_specialPowersAPI' will be set to the 'SpecialPowersAPI' - // object to call the member function in SpecialPowersAPI. + // object itself. The '_specialPowersAPI' will be set to the 'SpecialPowersChild' + // object to call the member function in SpecialPowersChild. _specialPowersAPI: null, observe(aSubject, aTopic, aData) { if (aTopic == "perm-changed") { @@ -2055,7 +2238,7 @@ SpecialPowersAPI.prototype.permissionObserverProxy = { }, }; -SpecialPowersAPI.prototype._permissionObserver = { +SpecialPowersChild.prototype._permissionObserver = { _self: null, _lastPermission: {}, _callBack: null, @@ -2102,17 +2285,14 @@ SpecialPowersAPI.prototype._permissionObserver = { }, }; -SpecialPowersAPI.prototype.EARLY_BETA_OR_EARLIER = +SpecialPowersChild.prototype.EARLY_BETA_OR_EARLIER = AppConstants.EARLY_BETA_OR_EARLIER; // Due to an unfortunate accident of history, when this API was -// subclassed using `Thing.prototype = new SpecialPowersAPI()`, existing +// subclassed using `Thing.prototype = new SpecialPowersChild()`, existing // code depends on all SpecialPowers instances using the same arrays for // these. -Object.assign(SpecialPowersAPI.prototype, { +Object.assign(SpecialPowersChild.prototype, { _permissionsUndoStack: [], _pendingPermissions: [], }); - -this.SpecialPowersAPI = SpecialPowersAPI; -this.bindDOMWindowUtils = bindDOMWindowUtils; diff --git a/testing/specialpowers/moz.build b/testing/specialpowers/moz.build index d6d58169ffe0..43bb83f2ce42 100644 --- a/testing/specialpowers/moz.build +++ b/testing/specialpowers/moz.build @@ -19,7 +19,6 @@ FINAL_TARGET_FILES.content += [ 'content/MockColorPicker.jsm', 'content/MockFilePicker.jsm', 'content/MockPermissionPrompt.jsm', - 'content/SpecialPowersAPI.jsm', 'content/SpecialPowersAPIParent.jsm', 'content/SpecialPowersChild.jsm', 'content/SpecialPowersParent.jsm', From 228a75258fbde4533d88f8525e01d4b99376813d Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Thu, 15 Aug 2019 12:55:27 -0700 Subject: [PATCH 06/11] Bug 1561705: Part 5a - Remove SpecialPowersParent.jsm in preparation for merge with SpecialPowersAPIParent.jsm. r=mccr8 Differential Revision: https://phabricator.services.mozilla.com/D42184 --HG-- extra : rebase_source : 4c06302a2905ba594a06ce5e80c7243a6d316f45 --- .../content/SpecialPowersParent.jsm | 222 ------------------ 1 file changed, 222 deletions(-) delete mode 100644 testing/specialpowers/content/SpecialPowersParent.jsm diff --git a/testing/specialpowers/content/SpecialPowersParent.jsm b/testing/specialpowers/content/SpecialPowersParent.jsm deleted file mode 100644 index 557f6e0b140d..000000000000 --- a/testing/specialpowers/content/SpecialPowersParent.jsm +++ /dev/null @@ -1,222 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -// Based on: -// https://bugzilla.mozilla.org/show_bug.cgi?id=549539 -// https://bug549539.bugzilla.mozilla.org/attachment.cgi?id=429661 -// https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_1.9.3 -// https://developer.mozilla.org/en/how_to_build_an_xpcom_component_in_javascript - -var EXPORTED_SYMBOLS = ["SpecialPowersParent"]; - -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - -const { SpecialPowersAPIParent } = ChromeUtils.import( - "resource://specialpowers/SpecialPowersAPIParent.jsm" -); - -class SpecialPowersParent extends SpecialPowersAPIParent { - constructor() { - super(); - this._messageManager = Services.mm; - this._serviceWorkerListener = null; - - this._observer = this.observe.bind(this); - - this.didDestroy = this.uninit.bind(this); - - this._registerObservers = { - _self: this, - _topics: [], - _add(topic) { - if (!this._topics.includes(topic)) { - this._topics.push(topic); - Services.obs.addObserver(this, topic); - } - }, - observe(aSubject, aTopic, aData) { - var msg = { aData }; - switch (aTopic) { - case "perm-changed": - var permission = aSubject.QueryInterface(Ci.nsIPermission); - - // specialPowersAPI will consume this value, and it is used as a - // fake permission, but only type will be used. - // - // We need to ensure that it looks the same as a real permission, - // so we fake these properties. - msg.permission = { - principal: { - originAttributes: {}, - }, - type: permission.type, - }; - // fall through - default: - this._self.sendAsyncMessage("specialpowers-" + aTopic, msg); - } - }, - }; - - this.init(); - } - - observe(aSubject, aTopic, aData) { - if (aTopic == "http-on-modify-request") { - if (aSubject instanceof Ci.nsIChannel) { - let uri = aSubject.URI.spec; - this.sendAsyncMessage("specialpowers-http-notify-request", { uri }); - } - } else { - this._observe(aSubject, aTopic, aData); - } - } - - init() { - Services.obs.addObserver(this._observer, "http-on-modify-request"); - - // We would like to check that tests don't leave service workers around - // after they finish, but that information lives in the parent process. - // Ideally, we'd be able to tell the child processes whenever service - // workers are registered or unregistered so they would know at all times, - // but service worker lifetimes are complicated enough to make that - // difficult. For the time being, let the child process know when a test - // registers a service worker so it can ask, synchronously, at the end if - // the service worker had unregister called on it. - let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( - Ci.nsIServiceWorkerManager - ); - let self = this; - this._serviceWorkerListener = { - onRegister() { - self.onRegister(); - }, - - onUnregister() { - // no-op - }, - }; - swm.addListener(this._serviceWorkerListener); - } - - uninit() { - var obs = Services.obs; - obs.removeObserver(this._observer, "http-on-modify-request"); - this._registerObservers._topics.splice(0).forEach(element => { - obs.removeObserver(this._registerObservers, element); - }); - this._removeProcessCrashObservers(); - - let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( - Ci.nsIServiceWorkerManager - ); - swm.removeListener(this._serviceWorkerListener); - } - - _addProcessCrashObservers() { - if (this._processCrashObserversRegistered) { - return; - } - - Services.obs.addObserver(this._observer, "plugin-crashed"); - Services.obs.addObserver(this._observer, "ipc:content-shutdown"); - this._processCrashObserversRegistered = true; - } - - _removeProcessCrashObservers() { - if (!this._processCrashObserversRegistered) { - return; - } - - Services.obs.removeObserver(this._observer, "plugin-crashed"); - Services.obs.removeObserver(this._observer, "ipc:content-shutdown"); - this._processCrashObserversRegistered = false; - } - - /** - * messageManager callback function - * This will get requests from our API in the window and process them in chrome for it - **/ - receiveMessage(aMessage) { - switch (aMessage.name) { - case "Ping": - return undefined; - case "SpecialPowers.Quit": - Services.startup.quit(Ci.nsIAppStartup.eForceQuit); - break; - case "SpecialPowers.Focus": - this.manager.rootFrameLoader.ownerElement.focus(); - break; - case "SpecialPowers.CreateFiles": - return (async () => { - let filePaths = []; - if (!this._createdFiles) { - this._createdFiles = []; - } - let createdFiles = this._createdFiles; - - let promises = []; - aMessage.data.forEach(function(request) { - const filePerms = 0o666; - let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile); - if (request.name) { - testFile.appendRelativePath(request.name); - } else { - testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms); - } - let outStream = Cc[ - "@mozilla.org/network/file-output-stream;1" - ].createInstance(Ci.nsIFileOutputStream); - outStream.init( - testFile, - 0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE - filePerms, - 0 - ); - if (request.data) { - outStream.write(request.data, request.data.length); - } - outStream.close(); - promises.push( - File.createFromFileName(testFile.path, request.options).then( - function(file) { - filePaths.push(file); - } - ) - ); - createdFiles.push(testFile); - }); - - await Promise.all(promises); - return filePaths; - })().catch(e => { - Cu.reportError(e); - return Promise.reject(String(e)); - }); - - case "SpecialPowers.RemoveFiles": - if (this._createdFiles) { - this._createdFiles.forEach(function(testFile) { - try { - testFile.remove(false); - } catch (e) {} - }); - this._createdFiles = null; - } - break; - - case "Wakeup": - break; - - default: - return super.receiveMessage(aMessage); - } - return undefined; - } - - onRegister() { - this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true }); - } -} From f18f60d4b4cb1f91311dfea8fb4e59f03a247f70 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Wed, 14 Aug 2019 16:41:41 -0700 Subject: [PATCH 07/11] Bug 1561705: Part 5b - Merge SpecialPowersAPIParent.jsm and SpecialPowersParent.jsm. r=mccr8 Differential Revision: https://phabricator.services.mozilla.com/D42182 --HG-- rename : testing/specialpowers/content/SpecialPowersAPIParent.jsm => testing/specialpowers/content/SpecialPowersParent.jsm extra : rebase_source : 654701db631c71c7839466fb48f5bfe75d29df72 --- ...sAPIParent.jsm => SpecialPowersParent.jsm} | 203 +++++++++++++++++- testing/specialpowers/moz.build | 1 - 2 files changed, 192 insertions(+), 12 deletions(-) rename testing/specialpowers/content/{SpecialPowersAPIParent.jsm => SpecialPowersParent.jsm} (82%) diff --git a/testing/specialpowers/content/SpecialPowersAPIParent.jsm b/testing/specialpowers/content/SpecialPowersParent.jsm similarity index 82% rename from testing/specialpowers/content/SpecialPowersAPIParent.jsm rename to testing/specialpowers/content/SpecialPowersParent.jsm index 8c0a5778649a..8431fe579684 100644 --- a/testing/specialpowers/content/SpecialPowersAPIParent.jsm +++ b/testing/specialpowers/content/SpecialPowersParent.jsm @@ -4,7 +4,7 @@ "use strict"; -var EXPORTED_SYMBOLS = ["SpecialPowersAPIParent", "SpecialPowersError"]; +var EXPORTED_SYMBOLS = ["SpecialPowersParent"]; var { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" @@ -119,9 +119,52 @@ function doPrefEnvOp(fn) { // used to bounce assertion messages back down to the correct child. let nextTaskID = 1; -class SpecialPowersAPIParent extends JSWindowActorParent { +class SpecialPowersParent extends JSWindowActorParent { constructor() { super(); + + this._messageManager = Services.mm; + this._serviceWorkerListener = null; + + this._observer = this.observe.bind(this); + + this.didDestroy = this.uninit.bind(this); + + this._registerObservers = { + _self: this, + _topics: [], + _add(topic) { + if (!this._topics.includes(topic)) { + this._topics.push(topic); + Services.obs.addObserver(this, topic); + } + }, + observe(aSubject, aTopic, aData) { + var msg = { aData }; + switch (aTopic) { + case "perm-changed": + var permission = aSubject.QueryInterface(Ci.nsIPermission); + + // specialPowersAPI will consume this value, and it is used as a + // fake permission, but only type will be used. + // + // We need to ensure that it looks the same as a real permission, + // so we fake these properties. + msg.permission = { + principal: { + originAttributes: {}, + }, + type: permission.type, + }; + // fall through + default: + this._self.sendAsyncMessage("specialpowers-" + aTopic, msg); + } + }, + }; + + this.init(); + this._crashDumpDir = null; this._processCrashObserversRegistered = false; this._chromeScriptListeners = []; @@ -129,7 +172,48 @@ class SpecialPowersAPIParent extends JSWindowActorParent { this._taskActors = new Map(); } - _observe(aSubject, aTopic, aData) { + init() { + Services.obs.addObserver(this._observer, "http-on-modify-request"); + + // We would like to check that tests don't leave service workers around + // after they finish, but that information lives in the parent process. + // Ideally, we'd be able to tell the child processes whenever service + // workers are registered or unregistered so they would know at all times, + // but service worker lifetimes are complicated enough to make that + // difficult. For the time being, let the child process know when a test + // registers a service worker so it can ask, synchronously, at the end if + // the service worker had unregister called on it. + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + let self = this; + this._serviceWorkerListener = { + onRegister() { + self.onRegister(); + }, + + onUnregister() { + // no-op + }, + }; + swm.addListener(this._serviceWorkerListener); + } + + uninit() { + var obs = Services.obs; + obs.removeObserver(this._observer, "http-on-modify-request"); + this._registerObservers._topics.splice(0).forEach(element => { + obs.removeObserver(this._registerObservers, element); + }); + this._removeProcessCrashObservers(); + + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + swm.removeListener(this._serviceWorkerListener); + } + + observe(aSubject, aTopic, aData) { function addDumpIDToMessage(propertyName) { try { var id = aSubject.getPropertyAsAString(propertyName); @@ -143,6 +227,13 @@ class SpecialPowersAPIParent extends JSWindowActorParent { } switch (aTopic) { + case "http-on-modify-request": + if (aSubject instanceof Ci.nsIChannel) { + let uri = aSubject.URI.spec; + this.sendAsyncMessage("specialpowers-http-notify-request", { uri }); + } + break; + case "plugin-crashed": case "ipc:content-shutdown": var message = { type: "crash-observed", dumpIDs: [] }; @@ -254,6 +345,30 @@ class SpecialPowersAPIParent extends JSWindowActorParent { return removed; } + _addProcessCrashObservers() { + if (this._processCrashObserversRegistered) { + return; + } + + Services.obs.addObserver(this._observer, "plugin-crashed"); + Services.obs.addObserver(this._observer, "ipc:content-shutdown"); + this._processCrashObserversRegistered = true; + } + + _removeProcessCrashObservers() { + if (!this._processCrashObserversRegistered) { + return; + } + + Services.obs.removeObserver(this._observer, "plugin-crashed"); + Services.obs.removeObserver(this._observer, "ipc:content-shutdown"); + this._processCrashObserversRegistered = false; + } + + onRegister() { + this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true }); + } + _getURI(url) { return Services.io.newURI(url); } @@ -459,6 +574,78 @@ class SpecialPowersAPIParent extends JSWindowActorParent { // doesn't trigger a flurry of warnings about "does not always return // a value". switch (aMessage.name) { + case "Ping": + return undefined; + + case "SpecialPowers.Quit": + Services.startup.quit(Ci.nsIAppStartup.eForceQuit); + return undefined; + + case "SpecialPowers.Focus": + this.manager.rootFrameLoader.ownerElement.focus(); + return undefined; + + case "SpecialPowers.CreateFiles": + return (async () => { + let filePaths = []; + if (!this._createdFiles) { + this._createdFiles = []; + } + let createdFiles = this._createdFiles; + + let promises = []; + aMessage.data.forEach(function(request) { + const filePerms = 0o666; + let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + if (request.name) { + testFile.appendRelativePath(request.name); + } else { + testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms); + } + let outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + testFile, + 0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE + filePerms, + 0 + ); + if (request.data) { + outStream.write(request.data, request.data.length); + } + outStream.close(); + promises.push( + File.createFromFileName(testFile.path, request.options).then( + function(file) { + filePaths.push(file); + } + ) + ); + createdFiles.push(testFile); + }); + + await Promise.all(promises); + return filePaths; + })().catch(e => { + Cu.reportError(e); + return Promise.reject(String(e)); + }); + + case "SpecialPowers.RemoveFiles": + if (this._createdFiles) { + this._createdFiles.forEach(function(testFile) { + try { + testFile.remove(false); + } catch (e) {} + }); + this._createdFiles = null; + } + return undefined; + + case "Wakeup": + return undefined; + case "PushPrefEnv": return this.pushPrefEnv(aMessage.data); @@ -898,18 +1085,12 @@ class SpecialPowersAPIParent extends JSWindowActorParent { return ServiceWorkerCleanUp.removeFromHost("example.com"); } - case "Wakeup": - return undefined; - default: throw new SpecialPowersError( `Unrecognized Special Powers API: ${aMessage.name}` ); } - - // We throw an exception before reaching this explicit return because - // we should never be arriving here anyway. - throw new SpecialPowersError("Unreached code"); // eslint-disable-line no-unreachable - return undefined; + // This should be unreachable. If it ever becomes reachable, ESLint + // will produce an error about inconsistent return values. } } diff --git a/testing/specialpowers/moz.build b/testing/specialpowers/moz.build index 43bb83f2ce42..05aa3ac22a40 100644 --- a/testing/specialpowers/moz.build +++ b/testing/specialpowers/moz.build @@ -19,7 +19,6 @@ FINAL_TARGET_FILES.content += [ 'content/MockColorPicker.jsm', 'content/MockFilePicker.jsm', 'content/MockPermissionPrompt.jsm', - 'content/SpecialPowersAPIParent.jsm', 'content/SpecialPowersChild.jsm', 'content/SpecialPowersParent.jsm', 'content/SpecialPowersSandbox.jsm', From bde858564ea4a02f2b5531eac6689f05c11d6aa7 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Thu, 15 Aug 2019 14:21:37 -0700 Subject: [PATCH 08/11] Bug 1574296: Support assertions in nested SpecialPowers.span calls. r=mccr8 Prior to this patch, assertions in SpecialPowers.spawn callbacks only work when the caller was in a window with a SimpleTest harness. This patch fixes that by registering a default assertion handler at the start of a test, and sending assertions from any task without its own harness to said default handler. MANUAL PUSH: Contains complex rename operations that I don't trust Lando to handle correctly. Differential Revision: https://phabricator.services.mozilla.com/D42210 --HG-- extra : rebase_source : d5bc493bdd4793cd2a42f6dea2073a9c93a5fbf5 --- testing/mochitest/browser-test.js | 3 +++ .../test_SpecialPowersSandbox.html | 25 +++++++++++++++++-- .../mochitest/tests/SimpleTest/SimpleTest.js | 2 ++ .../content/SpecialPowersChild.jsm | 20 ++++++++++++--- .../content/SpecialPowersParent.jsm | 22 +++++++++++++--- 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js index 9d7c541020de..351ef4138549 100644 --- a/testing/mochitest/browser-test.js +++ b/testing/mochitest/browser-test.js @@ -477,6 +477,9 @@ function Tester(aTests, structuredLogger, aCallback) { ); this.SimpleTest = simpleTestScope.SimpleTest; + window.SpecialPowers.SimpleTest = this.SimpleTest; + window.SpecialPowers.setAsDefaultAssertHandler(); + var extensionUtilsScope = { registerCleanupFunction: fn => { this.currentTest.scope.registerCleanupFunction(fn); diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html index 6208a35af4dc..36c8280b684f 100644 --- a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html @@ -37,8 +37,11 @@ async function interceptDiagnostics(func) { } add_task(async function() { + const frameSrc = "https://example.com/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html"; + const subframeSrc = "https://example.org/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html"; + let frame = document.getElementById("iframe"); - frame.src = "https://example.com/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html"; + frame.src = frameSrc; await new Promise(resolve => { frame.addEventListener("load", resolve, {once: true}); @@ -59,13 +62,31 @@ add_task(async function() { // just need to make sure that the general functionality works as expected. let tests = { "SpecialPowers.spawn": () => { - return SpecialPowers.spawn(frame, [], () => { + return SpecialPowers.spawn(frame, [], async () => { Assert.equal(1, 2, "Thing"); Assert.equal(1, 1, "Hmm"); Assert.ok(true, "Yay."); Assert.ok(false, "Boo!."); }); }, + "SpecialPowers.spawn-subframe": () => { + return SpecialPowers.spawn(frame, [subframeSrc], async src => { + let frame = this.content.document.createElement("iframe"); + frame.src = src; + this.content.document.body.appendChild(frame); + + await new Promise(resolve => { + frame.addEventListener("load", resolve, { once: true }); + }); + + await SpecialPowers.spawn(frame, [], () => { + Assert.equal(1, 2, "Thing"); + Assert.equal(1, 1, "Hmm"); + Assert.ok(true, "Yay."); + Assert.ok(false, "Boo!."); + }); + }); + }, "SpecialPowers.loadChromeScript": async () => { let script = SpecialPowers.loadChromeScript(() => { this.addMessageListener("ping", () => "pong"); diff --git a/testing/mochitest/tests/SimpleTest/SimpleTest.js b/testing/mochitest/tests/SimpleTest/SimpleTest.js index c59e6b237a04..05362cf1b19c 100644 --- a/testing/mochitest/tests/SimpleTest/SimpleTest.js +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js @@ -228,6 +228,8 @@ SimpleTest._inChaosMode = false; SimpleTest.expected = 'pass'; SimpleTest.num_failed = 0; +SpecialPowers.setAsDefaultAssertHandler(); + function usesFailurePatterns() { return Array.isArray(SimpleTest.expected); } diff --git a/testing/specialpowers/content/SpecialPowersChild.jsm b/testing/specialpowers/content/SpecialPowersChild.jsm index 44b2791c79a0..71f26cf95cba 100644 --- a/testing/specialpowers/content/SpecialPowersChild.jsm +++ b/testing/specialpowers/content/SpecialPowersChild.jsm @@ -285,9 +285,7 @@ class SpecialPowersChild extends JSWindowActorChild { // An assertion has been done in a mochitest chrome script let { name, passed, stack, diag } = message.data; - let SimpleTest = - this.contentWindow && this.contentWindow.wrappedJSObject.SimpleTest; - + let {SimpleTest} = this; if (SimpleTest) { SimpleTest.record( passed, @@ -1654,6 +1652,7 @@ class SpecialPowersChild extends JSWindowActorChild { args, task: String(task), caller: SpecialPowersSandbox.getCallerInfo(Components.stack.caller), + hasHarness: typeof this.SimpleTest === "object", }); } @@ -1685,6 +1684,21 @@ class SpecialPowersChild extends JSWindowActorChild { return sb.execute(task, args, caller); } + get SimpleTest() { + return this._SimpleTest || this.contentWindow.wrappedJSObject.SimpleTest; + } + set SimpleTest(val) { + this._SimpleTest = val; + } + + /** + * Sets this actor as the default assertion result handler for tasks + * which originate in a window without a test harness. + */ + setAsDefaultAssertHandler() { + this.sendAsyncMessage("SetAsDefaultAssertHandler"); + } + getFocusedElementForWindow(targetWindow, aDeep) { var outParam = {}; Services.focus.getFocusedElementForWindow(targetWindow, aDeep, outParam); diff --git a/testing/specialpowers/content/SpecialPowersParent.jsm b/testing/specialpowers/content/SpecialPowersParent.jsm index 8431fe579684..0fc15ca6c752 100644 --- a/testing/specialpowers/content/SpecialPowersParent.jsm +++ b/testing/specialpowers/content/SpecialPowersParent.jsm @@ -119,6 +119,10 @@ function doPrefEnvOp(fn) { // used to bounce assertion messages back down to the correct child. let nextTaskID = 1; +// The default actor to send assertions to if a task originated in a +// window without a test harness. +let defaultAssertHandler; + class SpecialPowersParent extends JSWindowActorParent { constructor() { super(); @@ -200,6 +204,10 @@ class SpecialPowersParent extends JSWindowActorParent { } uninit() { + if (defaultAssertHandler === this) { + defaultAssertHandler = null; + } + var obs = Services.obs; obs.removeObserver(this._observer, "http-on-modify-request"); this._registerObservers._topics.splice(0).forEach(element => { @@ -1030,15 +1038,22 @@ class SpecialPowersParent extends JSWindowActorParent { }); } + case "SetAsDefaultAssertHandler": { + defaultAssertHandler = this; + return undefined; + } + case "Spawn": { - let { browsingContext, task, args, caller } = aMessage.data; + let { browsingContext, task, args, caller, hasHarness } = aMessage.data; let spParent = browsingContext.currentWindowGlobal.getActor( "SpecialPowers" ); let taskId = nextTaskID++; - spParent._taskActors.set(taskId, this); + if (hasHarness) { + spParent._taskActors.set(taskId, this); + } return spParent .sendQuery("Spawn", { task, args, caller, taskId }) @@ -1071,9 +1086,10 @@ class SpecialPowersParent extends JSWindowActorParent { case "ProxiedAssert": { let { taskId, data } = aMessage.data; - let actor = this._taskActors.get(taskId); + let actor = this._taskActors.get(taskId) || defaultAssertHandler; actor.sendAsyncMessage("Assert", data); + return undefined; } From 98eafcceca8694a2dcaffdeb9ea7ea722e17be45 Mon Sep 17 00:00:00 2001 From: Coroiu Cristina Date: Tue, 20 Aug 2019 05:22:54 +0300 Subject: [PATCH 09/11] Bug 1561705 - Fix ESlint failure --- testing/specialpowers/content/SpecialPowersChild.jsm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/specialpowers/content/SpecialPowersChild.jsm b/testing/specialpowers/content/SpecialPowersChild.jsm index 71f26cf95cba..33d0bee81375 100644 --- a/testing/specialpowers/content/SpecialPowersChild.jsm +++ b/testing/specialpowers/content/SpecialPowersChild.jsm @@ -285,7 +285,7 @@ class SpecialPowersChild extends JSWindowActorChild { // An assertion has been done in a mochitest chrome script let { name, passed, stack, diag } = message.data; - let {SimpleTest} = this; + let { SimpleTest } = this; if (SimpleTest) { SimpleTest.record( passed, From b45bb1bc89428e9bafe925e001de16c9d6433f1a Mon Sep 17 00:00:00 2001 From: Oana Pop Rus Date: Tue, 20 Aug 2019 06:48:20 +0300 Subject: [PATCH 10/11] Backed out 9 changesets (bug 1574296, bug 1561705) for devtools failures in browser_dbg-breaking-from-console.js Backed out changeset 97fc86ccdbce (bug 1561705) Backed out changeset 41b6d03a870c (bug 1574296) Backed out changeset f9d136ac0e61 (bug 1561705) Backed out changeset 40a1794e0c0e (bug 1561705) Backed out changeset dad7319f1f6c (bug 1561705) Backed out changeset 7c7d41b7baca (bug 1561705) Backed out changeset cbe56a9fce52 (bug 1561705) Backed out changeset 080ff558e41a (bug 1561705) Backed out changeset ac3da20c6879 (bug 1561705) --HG-- rename : testing/specialpowers/content/SpecialPowersChild.jsm => testing/specialpowers/content/SpecialPowersAPI.jsm rename : testing/specialpowers/content/SpecialPowersParent.jsm => testing/specialpowers/content/SpecialPowersAPIParent.jsm --- .../performance/browser_startup_content.js | 6 +- ...er_trackingUI_socialtracking_doorhanger.js | 14 +- .../components/downloads/test/browser/head.js | 2 +- .../test/browser/browser-remote.ini | 6 - .../extensions/test/browser/browser.ini | 2 - .../browser_ext_browserAction_context.js | 1 - ...owserAction_pageAction_icon_permissions.js | 2 - .../browser/browser_ext_pageAction_popup.js | 2 - .../browser_ext_pageAction_show_matches.js | 2 - .../test/browser/browser_ext_tabs_query.js | 6 +- .../browser/browser_ext_themes_validation.js | 2 - .../extensions/test/browser/head.js | 14 + .../browser_copy_query_without_tree.js | 2 +- devtools/client/framework/test/browser.ini | 4 - devtools/client/framework/test/head.js | 6 + .../jsonview/test/browser_jsonview_theme.js | 10 +- docshell/test/chrome/bug293235_window.xul | 1 + docshell/test/chrome/bug396649_window.xul | 1 + docshell/test/chrome/bug89419_window.xul | 1 + dom/chrome-webidl/JSWindowActor.webidl | 15 +- dom/docs/Fission.rst | 5 - dom/ipc/JSWindowActor.cpp | 16 +- dom/ipc/JSWindowActor.h | 4 +- dom/ipc/JSWindowActorChild.cpp | 2 - dom/tests/browser/browser_frame_elements.js | 12 +- testing/mochitest/browser-test.js | 12 +- testing/mochitest/moz.build | 1 + .../test_SpecialPowersSandbox.html | 25 +- .../tests/SimpleTest/ChromePowers.js | 120 + .../mochitest/tests/SimpleTest/SimpleTest.js | 2 - testing/specialpowers/api.js | 1 - .../content/SpecialPowersAPI.jsm | 2121 ++++++++++++++++ .../content/SpecialPowersAPIParent.jsm | 915 +++++++ .../content/SpecialPowersChild.jsm | 2214 +---------------- .../content/SpecialPowersParent.jsm | 950 +------ testing/specialpowers/moz.build | 2 + 36 files changed, 3316 insertions(+), 3185 deletions(-) create mode 100644 testing/mochitest/tests/SimpleTest/ChromePowers.js create mode 100644 testing/specialpowers/content/SpecialPowersAPI.jsm create mode 100644 testing/specialpowers/content/SpecialPowersAPIParent.jsm diff --git a/browser/base/content/test/performance/browser_startup_content.js b/browser/base/content/test/performance/browser_startup_content.js index 1df6116eaf95..6d0ce67954b5 100644 --- a/browser/base/content/test/performance/browser_startup_content.js +++ b/browser/base/content/test/performance/browser_startup_content.js @@ -20,6 +20,9 @@ const kDumpAllStacks = false; const whitelist = { modules: new Set([ "chrome://mochikit/content/ShutdownLeaksCollector.jsm", + "resource://specialpowers/SpecialPowersChild.jsm", + "resource://specialpowers/SpecialPowersAPI.jsm", + "resource://specialpowers/WrapPrivileged.jsm", "resource://gre/modules/ContentProcessSingleton.jsm", @@ -92,9 +95,6 @@ const intermittently_loaded_whitelist = { "resource://gre/modules/nsAsyncShutdown.jsm", "resource://gre/modules/sessionstore/Utils.jsm", - "resource://specialpowers/SpecialPowersChild.jsm", - "resource://specialpowers/WrapPrivileged.jsm", - // Webcompat about:config front-end. This is presently nightly-only and // part of a system add-on which may not load early enough for the test. "resource://webcompat/AboutCompat.jsm", diff --git a/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js b/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js index 0f17e87926bf..3e4349f567c2 100644 --- a/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js +++ b/browser/base/content/test/trackingUI/browser_trackingUI_socialtracking_doorhanger.js @@ -33,7 +33,7 @@ add_task(async function setup() { async function testPopup(hasPopup, buttonToClick) { let numPageLoaded = gProtectionsHandler._socialTrackingSessionPageLoad; - let numPopupShown = Services.prefs.getIntPref( + let numPopupShown = SpecialPowers.getIntPref( "privacy.socialtracking.notification.counter", 0 ); @@ -89,7 +89,7 @@ async function testPopup(hasPopup, buttonToClick) { // click on the button of the popup notification if (typeof buttonToClick === "string") { is( - Services.prefs.getBoolPref( + SpecialPowers.getBoolPref( "privacy.socialtracking.notification.enabled", false ), @@ -101,7 +101,7 @@ async function testPopup(hasPopup, buttonToClick) { EventUtils.synthesizeMouseAtCenter(notification[buttonToClick], {}); is( - Services.prefs.getBoolPref( + SpecialPowers.getBoolPref( "privacy.socialtracking.notification.enabled", true ), @@ -110,13 +110,13 @@ async function testPopup(hasPopup, buttonToClick) { ); } - let lastShown = Services.prefs.getCharPref( + let lastShown = SpecialPowers.getCharPref( "privacy.socialtracking.notification.lastShown", "0" ); ok(lastShown !== "0", "last shown timestamp updated"); is( - Services.prefs.getIntPref( + SpecialPowers.getIntPref( "privacy.socialtracking.notification.counter", 0 ), @@ -221,12 +221,12 @@ add_task(async function testSocialTrackingPopups() { for (let config of configs) { ok(config.description, config.description); - await SpecialPowers.pushPrefEnv({ + SpecialPowers.pushPrefEnv({ set: config.prefs, }); for (let result of config.results) { await testPopup(result, config.button); } - await SpecialPowers.popPrefEnv(); + SpecialPowers.popPrefEnv(); } }); diff --git a/browser/components/downloads/test/browser/head.js b/browser/components/downloads/test/browser/head.js index 14210c3ddbdc..910c2ce59fa9 100644 --- a/browser/components/downloads/test/browser/head.js +++ b/browser/components/downloads/test/browser/head.js @@ -141,7 +141,7 @@ async function setDownloadDir() { await SpecialPowers.pushPrefEnv({ set: [ ["browser.download.folderList", 2], - ["browser.download.dir", tmpDir.path], + ["browser.download.dir", tmpDir, Ci.nsIFile], ], }); } diff --git a/browser/components/extensions/test/browser/browser-remote.ini b/browser/components/extensions/test/browser/browser-remote.ini index c4e8a600a5cf..9e7090f1cb77 100644 --- a/browser/components/extensions/test/browser/browser-remote.ini +++ b/browser/components/extensions/test/browser/browser-remote.ini @@ -6,12 +6,6 @@ # in this manifest to the sub-directory "test-oop-extensions", and then check # whether we're running from that directory from head.js install-to-subdir = test-oop-extensions -prefs = - extensions.webextensions.remote=true - # We don't want to reset this at the end of the test, so that we don't have - # to spawn a new extension child process for each test unit. - dom.ipc.keepProcessesAlive.extension=1 - tags = webextensions remote-webextensions skip-if = !e10s support-files = diff --git a/browser/components/extensions/test/browser/browser.ini b/browser/components/extensions/test/browser/browser.ini index 3d2daa718851..29c36260931c 100644 --- a/browser/components/extensions/test/browser/browser.ini +++ b/browser/components/extensions/test/browser/browser.ini @@ -2,8 +2,6 @@ tags = webextensions in-process-webextensions support-files = head.js -prefs = - extensions.webextensions.remote=false [browser_ext_autocompletepopup.js] [browser_ext_windows_allowScriptsToClose.js] diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js index 51f3b545ebcc..d27d6d2f6235 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js @@ -546,7 +546,6 @@ add_task(async function testBadgeColorPersistence() { add_task(async function testPropertyRemoval() { await runTests({ manifest: { - name: "Generated extension", browser_action: { default_icon: "default.png", default_popup: "default.html", diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js index 08e431d26fbb..b7cd3cc01cef 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js @@ -2,8 +2,6 @@ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; -PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/); - // Test that an error is thrown when providing invalid icon sizes add_task(async function testInvalidIconSizes() { let extension = ExtensionTestUtils.loadExtension({ diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js index 208eb24d10c5..411581c99a79 100644 --- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js @@ -2,8 +2,6 @@ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; -PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/); - const { GlobalManager } = ChromeUtils.import( "resource://gre/modules/Extension.jsm", null diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js index 4e1ba1b251f9..b37ada26f59c 100644 --- a/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js @@ -2,8 +2,6 @@ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; -PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/); - function getExtension(page_action) { return ExtensionTestUtils.loadExtension({ manifest: { diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_query.js b/browser/components/extensions/test/browser/browser_ext_tabs_query.js index cac9a3eef98e..6268d89cf4d1 100644 --- a/browser/components/extensions/test/browser/browser_ext_tabs_query.js +++ b/browser/components/extensions/test/browser/browser_ext_tabs_query.js @@ -316,13 +316,13 @@ add_task(async function() { const RESOLUTION_PREF = "layout.css.devPixelsPerPx"; registerCleanupFunction(() => { - Services.prefs.clearUserPref(RESOLUTION_PREF); + SpecialPowers.clearUserPref(RESOLUTION_PREF); }); await Promise.all([extension.startup(), extension.awaitMessage("ready")]); for (let resolution of [2, 1]) { - Services.prefs.setCharPref(RESOLUTION_PREF, String(resolution)); + SpecialPowers.setCharPref(RESOLUTION_PREF, String(resolution)); is( window.devicePixelRatio, resolution, @@ -342,7 +342,7 @@ add_task(async function() { BrowserTestUtils.removeTab(tab1); BrowserTestUtils.removeTab(tab2); BrowserTestUtils.removeTab(tab3); - Services.prefs.clearUserPref(RESOLUTION_PREF); + SpecialPowers.clearUserPref(RESOLUTION_PREF); }); add_task(async function testQueryPermissions() { diff --git a/browser/components/extensions/test/browser/browser_ext_themes_validation.js b/browser/components/extensions/test/browser/browser_ext_themes_validation.js index 9281fcc0e0de..63f1793a5e34 100644 --- a/browser/components/extensions/test/browser/browser_ext_themes_validation.js +++ b/browser/components/extensions/test/browser/browser_ext_themes_validation.js @@ -1,7 +1,5 @@ "use strict"; -PromiseTestUtils.whitelistRejectionsGlobally(/packaging errors/); - /** * Helper function for testing a theme with invalid properties. * @param {object} invalidProps The invalid properties to load the theme with. diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js index b7d515645a08..337d44160a13 100644 --- a/browser/components/extensions/test/browser/head.js +++ b/browser/components/extensions/test/browser/head.js @@ -68,6 +68,20 @@ function loadTestSubscript(filePath) { Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this); } +// We run tests under two different configurations, from browser.ini and +// browser-remote.ini. When running from browser-remote.ini, the tests are +// copied to the sub-directory "test-oop-extensions", which we detect here, and +// use to select our configuration. +let remote = gTestPath.includes("test-oop-extensions"); +SpecialPowers.pushPrefEnv({ + set: [["extensions.webextensions.remote", remote]], +}); +if (remote) { + // We don't want to reset this at the end of the test, so that we don't have + // to spawn a new extension child process for each test unit. + SpecialPowers.setIntPref("dom.ipc.keepProcessesAlive.extension", 1); +} + // Don't try to create screenshots of sites we load during tests. Services.prefs .getDefaultBranch("browser.newtabpage.activity-stream.") diff --git a/browser/components/places/tests/browser/browser_copy_query_without_tree.js b/browser/components/places/tests/browser/browser_copy_query_without_tree.js index fbe36a8804a5..91c12b5bbd69 100644 --- a/browser/components/places/tests/browser/browser_copy_query_without_tree.js +++ b/browser/components/places/tests/browser/browser_copy_query_without_tree.js @@ -43,7 +43,7 @@ add_task(async function copy_toolbar_shortcut() { }); add_task(async function copy_mobile_shortcut() { - await SpecialPowers.pushPrefEnv({ + SpecialPowers.pushPrefEnv({ set: [["browser.bookmarks.showMobileBookmarks", true]], }); await promisePlacesInitComplete(); diff --git a/devtools/client/framework/test/browser.ini b/devtools/client/framework/test/browser.ini index 9d10b8fab2e5..185430b9e81b 100644 --- a/devtools/client/framework/test/browser.ini +++ b/devtools/client/framework/test/browser.ini @@ -53,10 +53,6 @@ support-files = !/devtools/client/shared/test/shared-head.js !/devtools/client/shared/test/shared-redux-head.js !/devtools/client/shared/test/telemetry-test-helpers.js -# This is far from ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1565279 -# covers removing this pref flip. -prefs = - security.allow_unsafe_parent_loads=true [browser_about-devtools-toolbox_load.js] [browser_about-devtools-toolbox_reload.js] diff --git a/devtools/client/framework/test/head.js b/devtools/client/framework/test/head.js index fa53d9b6fa60..680e0328265f 100644 --- a/devtools/client/framework/test/head.js +++ b/devtools/client/framework/test/head.js @@ -12,6 +12,12 @@ Services.scriptloader.loadSubScript( const EventEmitter = require("devtools/shared/event-emitter"); +// This is far from ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1565279 +// covers removing this pref flip. +SpecialPowers.pushPrefEnv({ + set: [["security.allow_unsafe_parent_loads", true]], +}); + function toggleAllTools(state) { for (const [, tool] of gDevTools._tools) { if (!tool.visibilityswitch) { diff --git a/devtools/client/jsonview/test/browser_jsonview_theme.js b/devtools/client/jsonview/test/browser_jsonview_theme.js index ab634ce073f7..6dc43d0cb0cd 100644 --- a/devtools/client/jsonview/test/browser_jsonview_theme.js +++ b/devtools/client/jsonview/test/browser_jsonview_theme.js @@ -8,20 +8,20 @@ const TEST_JSON_URL = URL_ROOT + "valid_json.json"; add_task(async function() { info("Test JSON theme started."); - const oldPref = Services.prefs.getCharPref("devtools.theme"); - Services.prefs.setCharPref("devtools.theme", "light"); + const oldPref = SpecialPowers.getCharPref("devtools.theme"); + SpecialPowers.setCharPref("devtools.theme", "light"); await addJsonViewTab(TEST_JSON_URL); is(await getTheme(), "theme-light", "The initial theme is light"); - Services.prefs.setCharPref("devtools.theme", "dark"); + SpecialPowers.setCharPref("devtools.theme", "dark"); is(await getTheme(), "theme-dark", "Theme changed to dark"); - Services.prefs.setCharPref("devtools.theme", "light"); + SpecialPowers.setCharPref("devtools.theme", "light"); is(await getTheme(), "theme-light", "Theme changed to light"); - Services.prefs.setCharPref("devtools.theme", oldPref); + SpecialPowers.setCharPref("devtools.theme", oldPref); }); function getTheme() { diff --git a/docshell/test/chrome/bug293235_window.xul b/docshell/test/chrome/bug293235_window.xul index 76c0033ef698..a561e6d9ce2d 100644 --- a/docshell/test/chrome/bug293235_window.xul +++ b/docshell/test/chrome/bug293235_window.xul @@ -8,6 +8,7 @@ onload="setTimeout(runTests, 0);" title="bug 293235 test"> + diff --git a/docshell/test/chrome/bug396649_window.xul b/docshell/test/chrome/bug396649_window.xul index ef8151cad2f4..ad6a894b1c76 100755 --- a/docshell/test/chrome/bug396649_window.xul +++ b/docshell/test/chrome/bug396649_window.xul @@ -8,6 +8,7 @@ onload="setTimeout(nextTest, 0);" title="bug 396649 test"> + diff --git a/dom/chrome-webidl/JSWindowActor.webidl b/dom/chrome-webidl/JSWindowActor.webidl index 67e0550717d1..b84ca4b95419 100644 --- a/dom/chrome-webidl/JSWindowActor.webidl +++ b/dom/chrome-webidl/JSWindowActor.webidl @@ -73,11 +73,11 @@ callback interface MozObserverCallback { }; /** - * WebIDL callback interface calling the `willDestroy`, `didDestroy`, and - * `actorCreated` methods on JSWindowActors. + * WebIDL callback interface calling the `willDestroy` and `didDestroy` + * method on JSWindowActors. */ [MOZ_CAN_RUN_SCRIPT_BOUNDARY] -callback MozJSWindowActorCallback = void(); +callback MozActorDestroyCallback = void(); /** * The willDestroy method, if present, will be called at the last opportunity @@ -85,16 +85,13 @@ callback MozJSWindowActorCallback = void(); * up and send final messages. * The didDestroy method, if present, will be called after the actor is no * longer able to receive any more messages. - * The actorCreated method, if present, will be called immediately after the - * actor has been created and initialized. * * NOTE: Messages may be received between willDestroy and didDestroy, but they * may not be sent. */ -dictionary MozJSWindowActorCallbacks { - [ChromeOnly] MozJSWindowActorCallback willDestroy; - [ChromeOnly] MozJSWindowActorCallback didDestroy; - [ChromeOnly] MozJSWindowActorCallback actorCreated; +dictionary MozActorDestroyCallbacks { + [ChromeOnly] MozActorDestroyCallback willDestroy; + [ChromeOnly] MozActorDestroyCallback didDestroy; }; /** diff --git a/dom/docs/Fission.rst b/dom/docs/Fission.rst index e70fd875c709..3149013ba4e7 100644 --- a/dom/docs/Fission.rst +++ b/dom/docs/Fission.rst @@ -144,11 +144,6 @@ If you register your Actor to listen for ``nsIObserver`` notifications, implemen If you register your Actor to listen for content events, implement a ``handleEvent`` method with the above signature to handle the event. -``actorCreated`` -```````````````` - -This method is called immediately after a child actor is created and initialized. Unlike the actor's constructor, it is possible to do things like access the actor's content window and send messages from this callback. - ``willDestroy`` ``````````````` diff --git a/dom/ipc/JSWindowActor.cpp b/dom/ipc/JSWindowActor.cpp index 59360ef179a0..c0557059359a 100644 --- a/dom/ipc/JSWindowActor.cpp +++ b/dom/ipc/JSWindowActor.cpp @@ -38,17 +38,17 @@ NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(JSWindowActor) JSWindowActor::JSWindowActor() : mNextQueryId(0) {} void JSWindowActor::StartDestroy() { - InvokeCallback(CallbackFunction::WillDestroy); + DestroyCallback(DestroyCallbackFunction::WillDestroy); } void JSWindowActor::AfterDestroy() { - InvokeCallback(CallbackFunction::DidDestroy); + DestroyCallback(DestroyCallbackFunction::DidDestroy); } -void JSWindowActor::InvokeCallback(CallbackFunction callback) { +void JSWindowActor::DestroyCallback(DestroyCallbackFunction callback) { AutoEntryScript aes(GetParentObject(), "JSWindowActor destroy callback"); JSContext* cx = aes.cx(); - MozJSWindowActorCallbacks callbacksHolder; + MozActorDestroyCallbacks callbacksHolder; NS_ENSURE_TRUE_VOID(GetWrapper()); JS::Rooted val(cx, JS::ObjectValue(*GetWrapper())); if (NS_WARN_IF(!callbacksHolder.Init(cx, val))) { @@ -56,18 +56,14 @@ void JSWindowActor::InvokeCallback(CallbackFunction callback) { } // Destroy callback is optional. - if (callback == CallbackFunction::WillDestroy) { + if (callback == DestroyCallbackFunction::WillDestroy) { if (callbacksHolder.mWillDestroy.WasPassed()) { callbacksHolder.mWillDestroy.Value()->Call(this); } - } else if (callback == CallbackFunction::DidDestroy) { + } else { if (callbacksHolder.mDidDestroy.WasPassed()) { callbacksHolder.mDidDestroy.Value()->Call(this); } - } else { - if (callbacksHolder.mActorCreated.WasPassed()) { - callbacksHolder.mActorCreated.Value()->Call(this); - } } } diff --git a/dom/ipc/JSWindowActor.h b/dom/ipc/JSWindowActor.h index cfff05d6e101..7f473c124971 100644 --- a/dom/ipc/JSWindowActor.h +++ b/dom/ipc/JSWindowActor.h @@ -39,7 +39,7 @@ class JSWindowActor : public nsISupports, public nsWrapperCache { JSWindowActor(); enum class Type { Parent, Child }; - enum class CallbackFunction { WillDestroy, DidDestroy, ActorCreated }; + enum class DestroyCallbackFunction { WillDestroy, DidDestroy }; const nsString& Name() const { return mName; } @@ -76,7 +76,7 @@ class JSWindowActor : public nsISupports, public nsWrapperCache { void AfterDestroy(); - void InvokeCallback(CallbackFunction willDestroy); + void DestroyCallback(DestroyCallbackFunction willDestroy); private: void ReceiveMessageOrQuery(JSContext* aCx, diff --git a/dom/ipc/JSWindowActorChild.cpp b/dom/ipc/JSWindowActorChild.cpp index 5d53b76f1782..403edfeacfba 100644 --- a/dom/ipc/JSWindowActorChild.cpp +++ b/dom/ipc/JSWindowActorChild.cpp @@ -35,8 +35,6 @@ void JSWindowActorChild::Init(const nsAString& aName, MOZ_ASSERT(!mManager, "Cannot Init() a JSWindowActorChild twice!"); SetName(aName); mManager = aManager; - - InvokeCallback(CallbackFunction::ActorCreated); } namespace { diff --git a/dom/tests/browser/browser_frame_elements.js b/dom/tests/browser/browser_frame_elements.js index 08bd1df65130..ae4a6c89716a 100644 --- a/dom/tests/browser/browser_frame_elements.js +++ b/dom/tests/browser/browser_frame_elements.js @@ -81,9 +81,9 @@ function startTests() { async function mozBrowserTests(browser) { info("Granting special powers for mozbrowser"); - await SpecialPowers.addPermission("browser", true, TEST_URI); - Services.prefs.setBoolPref("dom.mozBrowserFramesEnabled", true); - Services.prefs.setBoolPref("network.disable.ipc.security", true); + SpecialPowers.addPermission("browser", true, TEST_URI); + SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true); + SpecialPowers.setBoolPref("network.disable.ipc.security", true); await ContentTask.spawn(browser, null, function() { info("Checking mozbrowser iframe"); @@ -103,7 +103,7 @@ async function mozBrowserTests(browser) { }); info("Revoking special powers for mozbrowser"); - Services.prefs.clearUserPref("dom.mozBrowserFramesEnabled"); - Services.prefs.clearUserPref("network.disable.ipc.security"); - await SpecialPowers.removePermission("browser", TEST_URI); + SpecialPowers.clearUserPref("dom.mozBrowserFramesEnabled"); + SpecialPowers.clearUserPref("network.disable.ipc.security"); + SpecialPowers.removePermission("browser", TEST_URI); } diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js index 351ef4138549..62ecbe93ffd0 100644 --- a/testing/mochitest/browser-test.js +++ b/testing/mochitest/browser-test.js @@ -457,12 +457,11 @@ function Tester(aTests, structuredLogger, aCallback) { this.cpowEventUtils ); - // Make sure our SpecialPowers actor is instantiated, in case it was - // registered after our DOMWindowCreated event was fired (which it - // most likely was). - window.getWindowGlobalChild().getActor("SpecialPowers"); - var simpleTestScope = {}; + this._scriptLoader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/ChromePowers.js", + simpleTestScope + ); this._scriptLoader.loadSubScript( "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", simpleTestScope @@ -477,9 +476,6 @@ function Tester(aTests, structuredLogger, aCallback) { ); this.SimpleTest = simpleTestScope.SimpleTest; - window.SpecialPowers.SimpleTest = this.SimpleTest; - window.SpecialPowers.setAsDefaultAssertHandler(); - var extensionUtilsScope = { registerCleanupFunction: fn => { this.currentTest.scope.registerCleanupFunction(fn); diff --git a/testing/mochitest/moz.build b/testing/mochitest/moz.build index a6dadf3e277e..b34a9478927c 100644 --- a/testing/mochitest/moz.build +++ b/testing/mochitest/moz.build @@ -47,6 +47,7 @@ FINAL_TARGET_FILES.content.tests.SimpleTest += [ '../../docshell/test/chrome/docshell_helpers.js', '../modules/StructuredLog.jsm', 'tests/SimpleTest/AsyncUtilsContent.js', + 'tests/SimpleTest/ChromePowers.js', 'tests/SimpleTest/EventUtils.js', 'tests/SimpleTest/ExtensionTestUtils.js', 'tests/SimpleTest/iframe-between-tests.html', diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html index 36c8280b684f..6208a35af4dc 100644 --- a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersSandbox.html @@ -37,11 +37,8 @@ async function interceptDiagnostics(func) { } add_task(async function() { - const frameSrc = "https://example.com/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html"; - const subframeSrc = "https://example.org/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html"; - let frame = document.getElementById("iframe"); - frame.src = frameSrc; + frame.src = "https://example.com/tests/testing/mochitest/tests/Harness_sanity/file_spawn.html"; await new Promise(resolve => { frame.addEventListener("load", resolve, {once: true}); @@ -62,31 +59,13 @@ add_task(async function() { // just need to make sure that the general functionality works as expected. let tests = { "SpecialPowers.spawn": () => { - return SpecialPowers.spawn(frame, [], async () => { + return SpecialPowers.spawn(frame, [], () => { Assert.equal(1, 2, "Thing"); Assert.equal(1, 1, "Hmm"); Assert.ok(true, "Yay."); Assert.ok(false, "Boo!."); }); }, - "SpecialPowers.spawn-subframe": () => { - return SpecialPowers.spawn(frame, [subframeSrc], async src => { - let frame = this.content.document.createElement("iframe"); - frame.src = src; - this.content.document.body.appendChild(frame); - - await new Promise(resolve => { - frame.addEventListener("load", resolve, { once: true }); - }); - - await SpecialPowers.spawn(frame, [], () => { - Assert.equal(1, 2, "Thing"); - Assert.equal(1, 1, "Hmm"); - Assert.ok(true, "Yay."); - Assert.ok(false, "Boo!."); - }); - }); - }, "SpecialPowers.loadChromeScript": async () => { let script = SpecialPowers.loadChromeScript(() => { this.addMessageListener("ping", () => "pong"); diff --git a/testing/mochitest/tests/SimpleTest/ChromePowers.js b/testing/mochitest/tests/SimpleTest/ChromePowers.js new file mode 100644 index 000000000000..12255615b16e --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/ChromePowers.js @@ -0,0 +1,120 @@ +/* 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/. */ + +const {SpecialPowersAPI, bindDOMWindowUtils} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPI.jsm"); +const {SpecialPowersAPIParent} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPIParent.jsm"); + +class ChromePowers extends SpecialPowersAPI { + constructor(window) { + super(); + + this.window = Cu.getWeakReference(window); + + this.chromeWindow = window; + + this.DOMWindowUtils = bindDOMWindowUtils(window); + + this.parentActor = new SpecialPowersAPIParent(); + this.parentActor.sendAsyncMessage = this.sendReply.bind(this); + + this.listeners = new Map(); + } + + toString() { return "[ChromePowers]"; } + sanityCheck() { return "foo"; } + + get contentWindow() { + return window; + } + + get document() { + return window.document; + } + + get docShell() { + return window.docShell; + } + + sendReply(aType, aMsg) { + var msg = {name: aType, json: aMsg, data: aMsg}; + if (!this.listeners.has(aType)) { + throw new Error(`No listener for ${aType}`); + } + this.listeners.get(aType)(msg); + } + + sendAsyncMessage(aType, aMsg) { + var msg = {name: aType, json: aMsg, data: aMsg}; + this.receiveMessage(msg); + } + + async sendQuery(aType, aMsg) { + var msg = {name: aType, json: aMsg, data: aMsg}; + return this.receiveMessage(msg); + } + + _addMessageListener(aType, aCallback) { + if (this.listeners.has(aType)) { + throw new Error(`unable to handle multiple listeners for ${aType}`); + } + this.listeners.set(aType, aCallback); + } + _removeMessageListener(aType, aCallback) { + this.listeners.delete(aType); + } + + registerProcessCrashObservers() { + this._sendSyncMessage("SPProcessCrashService", { op: "register-observer" }); + } + + unregisterProcessCrashObservers() { + this._sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" }); + } + + receiveMessage(aMessage) { + switch (aMessage.name) { + case "SpecialPowers.Quit": + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit); + break; + case "SPProcessCrashService": + if (aMessage.json.op == "register-observer" || aMessage.json.op == "unregister-observer") { + // Hack out register/unregister specifically for browser-chrome leaks + break; + } + default: + // All calls go here, because we need to handle SPProcessCrashService calls as well + return this.parentActor.receiveMessage(aMessage); + } + return undefined; + } + + quit() { + // We come in here as SpecialPowers.quit, but SpecialPowers is really ChromePowers. + // For some reason this. resolves to TestRunner, so using SpecialPowers + // allows us to use the ChromePowers object which we defined below. + SpecialPowers._sendSyncMessage("SpecialPowers.Quit", {}); + } + + focus(aWindow) { + // We come in here as SpecialPowers.focus, but SpecialPowers is really ChromePowers. + // For some reason this. resolves to TestRunner, so using SpecialPowers + // allows us to use the ChromePowers object which we defined below. + if (aWindow) + aWindow.focus(); + } + + executeAfterFlushingMessageQueue(aCallback) { + aCallback(); + } +} + +if (window.parent.SpecialPowers && !window.SpecialPowers) { + window.SpecialPowers = window.parent.SpecialPowers; +} else { + ChromeUtils.import("resource://specialpowers/SpecialPowersAPIParent.jsm", this); + + window.SpecialPowers = new ChromePowers(window); +} + diff --git a/testing/mochitest/tests/SimpleTest/SimpleTest.js b/testing/mochitest/tests/SimpleTest/SimpleTest.js index 05362cf1b19c..c59e6b237a04 100644 --- a/testing/mochitest/tests/SimpleTest/SimpleTest.js +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js @@ -228,8 +228,6 @@ SimpleTest._inChaosMode = false; SimpleTest.expected = 'pass'; SimpleTest.num_failed = 0; -SpecialPowers.setAsDefaultAssertHandler(); - function usesFailurePatterns() { return Array.isArray(SimpleTest.expected); } diff --git a/testing/specialpowers/api.js b/testing/specialpowers/api.js index db7aee981476..cd862b03e927 100644 --- a/testing/specialpowers/api.js +++ b/testing/specialpowers/api.js @@ -35,7 +35,6 @@ this.specialpowers = class extends ExtensionAPI { ChromeUtils.registerWindowActor("SpecialPowers", { allFrames: true, - includeChrome: true, child: { moduleURI: "resource://specialpowers/SpecialPowersChild.jsm", events: { diff --git a/testing/specialpowers/content/SpecialPowersAPI.jsm b/testing/specialpowers/content/SpecialPowersAPI.jsm new file mode 100644 index 000000000000..b695a66e013c --- /dev/null +++ b/testing/specialpowers/content/SpecialPowersAPI.jsm @@ -0,0 +1,2121 @@ +/* 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/. */ +/* This code is loaded in every child process that is started by mochitest in + * order to be used as a replacement for UniversalXPConnect + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["SpecialPowersAPI", "bindDOMWindowUtils"]; + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "MockFilePicker", + "resource://specialpowers/MockFilePicker.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "MockColorPicker", + "resource://specialpowers/MockColorPicker.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "MockPermissionPrompt", + "resource://specialpowers/MockPermissionPrompt.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "SpecialPowersSandbox", + "resource://specialpowers/SpecialPowersSandbox.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "WrapPrivileged", + "resource://specialpowers/WrapPrivileged.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "NetUtil", + "resource://gre/modules/NetUtil.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "AppConstants", + "resource://gre/modules/AppConstants.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "PerTestCoverageUtils", + "resource://testing-common/PerTestCoverageUtils.jsm" +); + +// Allow stuff from this scope to be accessed from non-privileged scopes. This +// would crash if used outside of automation. +Cu.forcePermissiveCOWs(); + +function bindDOMWindowUtils(aWindow) { + return aWindow && WrapPrivileged.wrap(aWindow.windowUtils); +} + +// SPConsoleListener reflects nsIConsoleMessage objects into JS in a +// tidy, XPCOM-hiding way. Messages that are nsIScriptError objects +// have their properties exposed in detail. It also auto-unregisters +// itself when it receives a "sentinel" message. +function SPConsoleListener(callback) { + this.callback = callback; +} + +SPConsoleListener.prototype = { + // Overload the observe method for both nsIConsoleListener and nsIObserver. + // The topic will be null for nsIConsoleListener. + observe(msg, topic) { + let m = { + message: msg.message, + errorMessage: null, + cssSelectors: null, + sourceName: null, + sourceLine: null, + lineNumber: null, + columnNumber: null, + category: null, + windowID: null, + isScriptError: false, + isConsoleEvent: false, + isWarning: false, + isException: false, + isStrict: false, + }; + if (msg instanceof Ci.nsIScriptError) { + m.errorMessage = msg.errorMessage; + m.cssSelectors = msg.cssSelectors; + m.sourceName = msg.sourceName; + m.sourceLine = msg.sourceLine; + m.lineNumber = msg.lineNumber; + m.columnNumber = msg.columnNumber; + m.category = msg.category; + m.windowID = msg.outerWindowID; + m.innerWindowID = msg.innerWindowID; + m.isScriptError = true; + m.isWarning = (msg.flags & Ci.nsIScriptError.warningFlag) === 1; + m.isException = (msg.flags & Ci.nsIScriptError.exceptionFlag) === 1; + m.isStrict = (msg.flags & Ci.nsIScriptError.strictFlag) === 1; + } else if (topic === "console-api-log-event") { + // This is a dom/console event. + let unwrapped = msg.wrappedJSObject; + m.errorMessage = unwrapped.arguments[0]; + m.sourceName = unwrapped.filename; + m.lineNumber = unwrapped.lineNumber; + m.columnNumber = unwrapped.columnNumber; + m.windowID = unwrapped.ID; + m.innerWindowID = unwrapped.innerID; + m.isConsoleEvent = true; + m.isWarning = unwrapped.level === "warning"; + } + + Object.freeze(m); + + // Run in a separate runnable since console listeners aren't + // supposed to touch content and this one might. + Services.tm.dispatchToMainThread(() => { + this.callback.call(undefined, m); + }); + + if (!m.isScriptError && !m.isConsoleEvent && m.message === "SENTINEL") { + Services.obs.removeObserver(this, "console-api-log-event"); + Services.console.unregisterListener(this); + } + }, + + QueryInterface: ChromeUtils.generateQI([ + Ci.nsIConsoleListener, + Ci.nsIObserver, + ]), +}; + +class SpecialPowersAPI extends JSWindowActorChild { + constructor() { + super(); + + this._consoleListeners = []; + this._encounteredCrashDumpFiles = []; + this._unexpectedCrashDumpFiles = {}; + this._crashDumpDir = null; + this._mfl = null; + this._applyingPermissions = false; + this._observingPermissions = false; + this._asyncObservers = new WeakMap(); + this._xpcomabi = null; + this._os = null; + this._pu = null; + + this._nextExtensionID = 0; + this._extensionListeners = null; + } + + // Hack around devtools sometimes trying to JSON stringify us. + toJSON() { + return {}; + } + + receiveMessage(message) { + switch (message.name) { + case "Assert": + { + // An assertion has been done in a mochitest chrome script + let { name, passed, stack, diag } = message.data; + + let SimpleTest = + this.contentWindow && this.contentWindow.wrappedJSObject.SimpleTest; + + if (SimpleTest) { + SimpleTest.record( + passed, + name, + diag, + stack && stack.formattedStack + ); + } else { + // Well, this is unexpected. + dump(name + "\n"); + } + } + break; + } + return undefined; + } + + /* + * Privileged object wrapping API + * + * Usage: + * var wrapper = SpecialPowers.wrap(obj); + * wrapper.privilegedMethod(); wrapper.privilegedProperty; + * obj === SpecialPowers.unwrap(wrapper); + * + * These functions provide transparent access to privileged objects using + * various pieces of deep SpiderMagic. Conceptually, a wrapper is just an + * object containing a reference to the underlying object, where all method + * calls and property accesses are transparently performed with the System + * Principal. Moreover, objects obtained from the wrapper (including properties + * and method return values) are wrapped automatically. Thus, after a single + * call to SpecialPowers.wrap(), the wrapper layer is transitively maintained. + * + * Known Issues: + * + * - The wrapping function does not preserve identity, so + * SpecialPowers.wrap(foo) !== SpecialPowers.wrap(foo). See bug 718543. + * + * - The wrapper cannot see expando properties on unprivileged DOM objects. + * That is to say, the wrapper uses Xray delegation. + * + * - The wrapper sometimes guesses certain ES5 attributes for returned + * properties. This is explained in a comment in the wrapper code above, + * and shouldn't be a problem. + */ + wrap(obj) { + return WrapPrivileged.wrap(obj); + } + unwrap(obj) { + return WrapPrivileged.unwrap(obj); + } + isWrapper(val) { + return WrapPrivileged.isWrapper(val); + } + + /* + * When content needs to pass a callback or a callback object to an API + * accessed over SpecialPowers, that API may sometimes receive arguments for + * whom it is forbidden to create a wrapper in content scopes. As such, we + * need a layer to wrap the values in SpecialPowers wrappers before they ever + * reach content. + */ + wrapCallback(func) { + return WrapPrivileged.wrapCallback(func); + } + wrapCallbackObject(obj) { + return WrapPrivileged.wrapCallbackObject(obj); + } + + /* + * Used for assigning a property to a SpecialPowers wrapper, without unwrapping + * the value that is assigned. + */ + setWrapped(obj, prop, val) { + if (!WrapPrivileged.isWrapper(obj)) { + throw new Error( + "You only need to use this for SpecialPowers wrapped objects" + ); + } + + obj = WrapPrivileged.unwrap(obj); + return Reflect.set(obj, prop, val); + } + + /* + * Create blank privileged objects to use as out-params for privileged functions. + */ + createBlankObject() { + return {}; + } + + /* + * Because SpecialPowers wrappers don't preserve identity, comparing with == + * can be hazardous. Sometimes we can just unwrap to compare, but sometimes + * wrapping the underlying object into a content scope is forbidden. This + * function strips any wrappers if they exist and compare the underlying + * values. + */ + compare(a, b) { + return WrapPrivileged.unwrap(a) === WrapPrivileged.unwrap(b); + } + + get MockFilePicker() { + return MockFilePicker; + } + + get MockColorPicker() { + return MockColorPicker; + } + + get MockPermissionPrompt() { + return MockPermissionPrompt; + } + + /* + * Load a privileged script that runs same-process. This is different from + * |loadChromeScript|, which will run in the parent process in e10s mode. + */ + loadPrivilegedScript(aFunction) { + var str = "(" + aFunction.toString() + ")();"; + let gGlobalObject = Cu.getGlobalForObject(this); + let sb = Cu.Sandbox(gGlobalObject); + var window = this.contentWindow; + var mc = new window.MessageChannel(); + sb.port = mc.port1; + try { + let blob = new Blob([str], { type: "application/javascript" }); + let blobUrl = URL.createObjectURL(blob); + Services.scriptloader.loadSubScript(blobUrl, sb); + } catch (e) { + throw WrapPrivileged.wrap(e); + } + + return mc.port2; + } + + _readUrlAsString(aUrl) { + // Fetch script content as we can't use scriptloader's loadSubScript + // to evaluate http:// urls... + var scriptableStream = Cc[ + "@mozilla.org/scriptableinputstream;1" + ].getService(Ci.nsIScriptableInputStream); + + var channel = NetUtil.newChannel({ + uri: aUrl, + loadUsingSystemPrincipal: true, + }); + var input = channel.open(); + scriptableStream.init(input); + + var str; + var buffer = []; + + while ((str = scriptableStream.read(4096))) { + buffer.push(str); + } + + var output = buffer.join(""); + + scriptableStream.close(); + input.close(); + + var status; + if (channel instanceof Ci.nsIHttpChannel) { + status = channel.responseStatus; + } + + if (status == 404) { + throw new Error( + `Error while executing chrome script '${aUrl}':\n` + + "The script doesn't exist. Ensure you have registered it in " + + "'support-files' in your mochitest.ini." + ); + } + + return output; + } + + loadChromeScript(urlOrFunction, sandboxOptions) { + // Create a unique id for this chrome script + let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService( + Ci.nsIUUIDGenerator + ); + let id = uuidGenerator.generateUUID().toString(); + + // Tells chrome code to evaluate this chrome script + let scriptArgs = { id, sandboxOptions }; + if (typeof urlOrFunction == "function") { + scriptArgs.function = { + body: "(" + urlOrFunction.toString() + ")();", + name: urlOrFunction.name, + }; + } else { + // Note: We need to do this in the child since, even though + // `_readUrlAsString` pretends to be synchronous, its channel + // winds up spinning the event loop when loading HTTP URLs. That + // leads to unexpected out-of-order operations if the child sends + // a message immediately after loading the script. + scriptArgs.function = { + body: this._readUrlAsString(urlOrFunction), + }; + scriptArgs.url = urlOrFunction; + } + this.sendAsyncMessage("SPLoadChromeScript", scriptArgs); + + // Returns a MessageManager like API in order to be + // able to communicate with this chrome script + let listeners = []; + let chromeScript = { + addMessageListener: (name, listener) => { + listeners.push({ name, listener }); + }, + + promiseOneMessage: name => + new Promise(resolve => { + chromeScript.addMessageListener(name, function listener(message) { + chromeScript.removeMessageListener(name, listener); + resolve(message); + }); + }), + + removeMessageListener: (name, listener) => { + listeners = listeners.filter( + o => o.name != name || o.listener != listener + ); + }, + + sendAsyncMessage: (name, message) => { + this.sendAsyncMessage("SPChromeScriptMessage", { id, name, message }); + }, + + sendQuery: (name, message) => { + return this.sendQuery("SPChromeScriptMessage", { id, name, message }); + }, + + destroy: () => { + listeners = []; + this._removeMessageListener("SPChromeScriptMessage", chromeScript); + }, + + receiveMessage: aMessage => { + let messageId = aMessage.json.id; + let name = aMessage.json.name; + let message = aMessage.json.message; + if (this.contentWindow) { + message = new StructuredCloneHolder(message).deserialize( + this.contentWindow + ); + } + // Ignore message from other chrome script + if (messageId != id) { + return null; + } + + let result; + if (aMessage.name == "SPChromeScriptMessage") { + for (let listener of listeners.filter(o => o.name == name)) { + result = listener.listener(message); + } + } + return result; + }, + }; + this._addMessageListener("SPChromeScriptMessage", chromeScript); + + return this.wrap(chromeScript); + } + + async importInMainProcess(importString) { + var message = await this.sendQuery("SPImportInMainProcess", importString); + if (message.hadError) { + throw new Error( + "SpecialPowers.importInMainProcess failed with error " + + message.errorMessage + ); + } + } + + get Services() { + return WrapPrivileged.wrap(Services); + } + + /* + * A getter for the privileged Components object we have. + */ + getFullComponents() { + return Components; + } + + /* + * Convenient shortcuts to the standard Components abbreviations. + */ + get Cc() { + return WrapPrivileged.wrap(this.getFullComponents().classes); + } + get Ci() { + return WrapPrivileged.wrap(this.getFullComponents().interfaces); + } + get Cu() { + return WrapPrivileged.wrap(this.getFullComponents().utils); + } + get Cr() { + return WrapPrivileged.wrap(this.getFullComponents().results); + } + + getDOMWindowUtils(aWindow) { + if (aWindow == this.contentWindow && this.DOMWindowUtils != null) { + return this.DOMWindowUtils; + } + + return bindDOMWindowUtils(aWindow); + } + + /* + * A method to get a DOMParser that can't parse XUL. + */ + getNoXULDOMParser() { + // If we create it with a system subject principal (so it gets a + // nullprincipal), it won't be able to parse XUL by default. + return WrapPrivileged.wrap(new DOMParser()); + } + + get InspectorUtils() { + return WrapPrivileged.wrap(InspectorUtils); + } + + get PromiseDebugging() { + return WrapPrivileged.wrap(PromiseDebugging); + } + + async waitForCrashes(aExpectingProcessCrash) { + if (!aExpectingProcessCrash) { + return; + } + + var crashIds = this._encounteredCrashDumpFiles + .filter(filename => { + return filename.length === 40 && filename.endsWith(".dmp"); + }) + .map(id => { + return id.slice(0, -4); // Strip the .dmp extension to get the ID + }); + + await this.sendQuery("SPProcessCrashManagerWait", { + crashIds, + }); + } + + async removeExpectedCrashDumpFiles(aExpectingProcessCrash) { + var success = true; + if (aExpectingProcessCrash) { + var message = { + op: "delete-crash-dump-files", + filenames: this._encounteredCrashDumpFiles, + }; + if (!(await this.sendQuery("SPProcessCrashService", message))) { + success = false; + } + } + this._encounteredCrashDumpFiles.length = 0; + return success; + } + + async findUnexpectedCrashDumpFiles() { + var self = this; + var message = { + op: "find-crash-dump-files", + crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles, + }; + var crashDumpFiles = await this.sendQuery("SPProcessCrashService", message); + crashDumpFiles.forEach(function(aFilename) { + self._unexpectedCrashDumpFiles[aFilename] = true; + }); + return crashDumpFiles; + } + + removePendingCrashDumpFiles() { + var message = { + op: "delete-pending-crash-dump-files", + }; + return this.sendQuery("SPProcessCrashService", message); + } + + _setTimeout(callback) { + // for mochitest-browser + if (typeof this.chromeWindow != "undefined") { + this.chromeWindow.setTimeout(callback, 0); + } + // for mochitest-plain + else { + this.contentWindow.setTimeout(callback, 0); + } + } + + promiseTimeout(delay) { + return new Promise(resolve => { + this._setTimeout(resolve, delay); + }); + } + + _delayCallbackTwice(callback) { + let delayedCallback = () => { + let delayAgain = aCallback => { + // Using this._setTimeout doesn't work here + // It causes failures in mochtests that use + // multiple pushPrefEnv calls + // For chrome/browser-chrome mochitests + this._setTimeout(aCallback); + }; + delayAgain(delayAgain.bind(this, callback)); + }; + return delayedCallback; + } + + /* apply permissions to the system and when the test case is finished (SimpleTest.finish()) + we will revert the permission back to the original. + + inPermissions is an array of objects where each object has a type, action, context, ex: + [{'type': 'SystemXHR', 'allow': 1, 'context': document}, + {'type': 'SystemXHR', 'allow': Ci.nsIPermissionManager.PROMPT_ACTION, 'context': document}] + + Allow can be a boolean value of true/false or ALLOW_ACTION/DENY_ACTION/PROMPT_ACTION/UNKNOWN_ACTION + */ + async pushPermissions(inPermissions, callback) { + inPermissions = Cu.waiveXrays(inPermissions); + var pendingPermissions = []; + var cleanupPermissions = []; + + for (var p in inPermissions) { + var permission = inPermissions[p]; + var originalValue = Ci.nsIPermissionManager.UNKNOWN_ACTION; + var context = Cu.unwaiveXrays(permission.context); // Sometimes |context| is a DOM object on which we expect + // to be able to access .nodePrincipal, so we need to unwaive. + if ( + await this.testPermission( + permission.type, + Ci.nsIPermissionManager.ALLOW_ACTION, + context + ) + ) { + originalValue = Ci.nsIPermissionManager.ALLOW_ACTION; + } else if ( + await this.testPermission( + permission.type, + Ci.nsIPermissionManager.DENY_ACTION, + context + ) + ) { + originalValue = Ci.nsIPermissionManager.DENY_ACTION; + } else if ( + await this.testPermission( + permission.type, + Ci.nsIPermissionManager.PROMPT_ACTION, + context + ) + ) { + originalValue = Ci.nsIPermissionManager.PROMPT_ACTION; + } else if ( + await this.testPermission( + permission.type, + Ci.nsICookiePermission.ACCESS_SESSION, + context + ) + ) { + originalValue = Ci.nsICookiePermission.ACCESS_SESSION; + } + + let principal = this._getPrincipalFromArg(context); + if (principal.isSystemPrincipal) { + continue; + } + + let perm; + if (typeof permission.allow !== "boolean") { + perm = permission.allow; + } else { + perm = permission.allow + ? Ci.nsIPermissionManager.ALLOW_ACTION + : Ci.nsIPermissionManager.DENY_ACTION; + } + + if (permission.remove) { + perm = Ci.nsIPermissionManager.UNKNOWN_ACTION; + } + + if (originalValue == perm) { + continue; + } + + var todo = { + op: "add", + type: permission.type, + permission: perm, + value: perm, + principal, + expireType: + typeof permission.expireType === "number" ? permission.expireType : 0, // default: EXPIRE_NEVER + expireTime: + typeof permission.expireTime === "number" ? permission.expireTime : 0, + }; + + var cleanupTodo = Object.assign({}, todo); + + if (permission.remove) { + todo.op = "remove"; + } + + pendingPermissions.push(todo); + + /* Push original permissions value or clear into cleanup array */ + if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) { + cleanupTodo.op = "remove"; + } else { + cleanupTodo.value = originalValue; + cleanupTodo.permission = originalValue; + } + cleanupPermissions.push(cleanupTodo); + } + + if (pendingPermissions.length > 0) { + // The callback needs to be delayed twice. One delay is because the pref + // service doesn't guarantee the order it calls its observers in, so it + // may notify the observer holding the callback before the other + // observers have been notified and given a chance to make the changes + // that the callback checks for. The second delay is because pref + // observers often defer making their changes by posting an event to the + // event loop. + if (!this._observingPermissions) { + this._observingPermissions = true; + // If specialpowers is in main-process, then we can add a observer + // to get all 'perm-changed' signals. Otherwise, it can't receive + // all signals, so we register a observer in specialpowersobserver(in + // main-process) and get signals from it. + if (this.isMainProcess()) { + this.permissionObserverProxy._specialPowersAPI = this; + Services.obs.addObserver( + this.permissionObserverProxy, + "perm-changed" + ); + } else { + this.registerObservers("perm-changed"); + // bind() is used to set 'this' to SpecialPowersAPI itself. + this._addMessageListener( + "specialpowers-perm-changed", + this.permChangedProxy.bind(this) + ); + } + } + this._permissionsUndoStack.push(cleanupPermissions); + this._pendingPermissions.push([ + pendingPermissions, + this._delayCallbackTwice(callback), + ]); + this._applyPermissions(); + } else { + this._setTimeout(callback); + } + } + + /* + * This function should be used when specialpowers is in content process but + * it want to get the notification from chrome space. + * + * This function will call Services.obs.addObserver in SpecialPowersObserver + * (that is in chrome process) and forward the data received to SpecialPowers + * via messageManager. + * You can use this._addMessageListener("specialpowers-YOUR_TOPIC") to fire + * the callback. + * + * To get the expected data, you should modify + * SpecialPowersObserver.prototype._registerObservers.observe. Or the message + * you received from messageManager will only contain 'aData' from Service.obs. + * + * NOTICE: there is no implementation of _addMessageListener in + * ChromePowers.js + */ + registerObservers(topic) { + var msg = { + op: "add", + observerTopic: topic, + }; + return this.sendQuery("SPObserverService", msg); + } + + permChangedProxy(aMessage) { + let permission = aMessage.json.permission; + let aData = aMessage.json.aData; + this._permissionObserver.observe(permission, aData); + } + + popPermissions(callback) { + let promise = new Promise(resolve => { + if (this._permissionsUndoStack.length > 0) { + // See pushPermissions comment regarding delay. + let cb = this._delayCallbackTwice(resolve); + /* Each pop from the stack will yield an object {op/type/permission/value/url/appid/isInIsolatedMozBrowserElement} or null */ + this._pendingPermissions.push([this._permissionsUndoStack.pop(), cb]); + this._applyPermissions(); + } else { + if (this._observingPermissions) { + this._observingPermissions = false; + this._removeMessageListener( + "specialpowers-perm-changed", + this.permChangedProxy.bind(this) + ); + } + this._setTimeout(resolve); + } + }); + if (callback) { + promise.then(callback); + } + return promise; + } + + flushPermissions(callback) { + while (this._permissionsUndoStack.length > 1) { + this.popPermissions(null); + } + + return this.popPermissions(callback); + } + + setTestPluginEnabledState(newEnabledState, pluginName) { + return this.sendQuery("SPSetTestPluginEnabledState", { + newEnabledState, + pluginName, + }); + } + + /* + Iterate through one atomic set of permissions actions and perform allow/deny as appropriate. + All actions performed must modify the relevant permission. + */ + _applyPermissions() { + if (this._applyingPermissions || this._pendingPermissions.length <= 0) { + return; + } + + /* Set lock and get prefs from the _pendingPrefs queue */ + this._applyingPermissions = true; + var transaction = this._pendingPermissions.shift(); + var pendingActions = transaction[0]; + var callback = transaction[1]; + var lastPermission = pendingActions[pendingActions.length - 1]; + + var self = this; + this._permissionObserver._self = self; + this._permissionObserver._lastPermission = lastPermission; + this._permissionObserver._callback = callback; + this._permissionObserver._nextCallback = function() { + self._applyingPermissions = false; + // Now apply any permissions that may have been queued while we were applying + self._applyPermissions(); + }; + + for (var idx in pendingActions) { + var perm = pendingActions[idx]; + this.sendAsyncMessage("SPPermissionManager", perm); + } + } + + async pushPrefEnv(inPrefs, callback = null) { + await this.sendQuery("PushPrefEnv", inPrefs).then(callback); + await this.promiseTimeout(0); + } + + async popPrefEnv(callback = null) { + await this.sendQuery("PopPrefEnv").then(callback); + await this.promiseTimeout(0); + } + + async flushPrefEnv(callback = null) { + await this.sendQuery("FlushPrefEnv").then(callback); + await this.promiseTimeout(0); + } + + _addObserverProxy(notification) { + if (notification in this._proxiedObservers) { + this._addMessageListener( + notification, + this._proxiedObservers[notification] + ); + } + } + _removeObserverProxy(notification) { + if (notification in this._proxiedObservers) { + this._removeMessageListener( + notification, + this._proxiedObservers[notification] + ); + } + } + + addObserver(obs, notification, weak) { + // Make sure the parent side exists, or we won't get any notifications. + this.sendAsyncMessage("Wakeup"); + + this._addObserverProxy(notification); + obs = Cu.waiveXrays(obs); + if ( + typeof obs == "object" && + obs.observe.name != "SpecialPowersCallbackWrapper" + ) { + obs.observe = WrapPrivileged.wrapCallback(obs.observe); + } + Services.obs.addObserver(obs, notification, weak); + } + removeObserver(obs, notification) { + this._removeObserverProxy(notification); + Services.obs.removeObserver(Cu.waiveXrays(obs), notification); + } + notifyObservers(subject, topic, data) { + Services.obs.notifyObservers(subject, topic, data); + } + + /** + * An async observer is useful if you're listening for a + * notification that normally is only used by C++ code or chrome + * code (so it runs in the SystemGroup), but we need to know about + * it for a test (which runs as web content). If we used + * addObserver, we would assert when trying to enter web content + * from a runnabled labeled by the SystemGroup. An async observer + * avoids this problem. + */ + addAsyncObserver(obs, notification, weak) { + obs = Cu.waiveXrays(obs); + if ( + typeof obs == "object" && + obs.observe.name != "SpecialPowersCallbackWrapper" + ) { + obs.observe = WrapPrivileged.wrapCallback(obs.observe); + } + let asyncObs = (...args) => { + Services.tm.dispatchToMainThread(() => { + if (typeof obs == "function") { + obs(...args); + } else { + obs.observe.call(undefined, ...args); + } + }); + }; + this._asyncObservers.set(obs, asyncObs); + Services.obs.addObserver(asyncObs, notification, weak); + } + removeAsyncObserver(obs, notification) { + let asyncObs = this._asyncObservers.get(Cu.waiveXrays(obs)); + Services.obs.removeObserver(asyncObs, notification); + } + + can_QI(obj) { + return obj.QueryInterface !== undefined; + } + do_QueryInterface(obj, iface) { + return obj.QueryInterface(Ci[iface]); + } + + call_Instanceof(obj1, obj2) { + obj1 = WrapPrivileged.unwrap(obj1); + obj2 = WrapPrivileged.unwrap(obj2); + return obj1 instanceof obj2; + } + + // Returns a privileged getter from an object. GetOwnPropertyDescriptor does + // not work here because xray wrappers don't properly implement it. + // + // This terribleness is used by dom/base/test/test_object.html because + // and tags will spawn plugins if their prototype is touched, + // so we need to get and cache the getter of |hasRunningPlugin| if we want to + // call it without paradoxically spawning the plugin. + do_lookupGetter(obj, name) { + return Object.prototype.__lookupGetter__.call(obj, name); + } + + // Mimic the get*Pref API + getBoolPref(...args) { + return Services.prefs.getBoolPref(...args); + } + getIntPref(...args) { + return Services.prefs.getIntPref(...args); + } + getCharPref(...args) { + return Services.prefs.getCharPref(...args); + } + getComplexValue(prefName, iid) { + return Services.prefs.getComplexValue(prefName, iid); + } + + getParentBoolPref(prefName, defaultValue) { + return this._getParentPref(prefName, "BOOL", { defaultValue }); + } + getParentIntPref(prefName, defaultValue) { + return this._getParentPref(prefName, "INT", { defaultValue }); + } + getParentCharPref(prefName, defaultValue) { + return this._getParentPref(prefName, "CHAR", { defaultValue }); + } + + // Mimic the set*Pref API + setBoolPref(prefName, value) { + return this._setPref(prefName, "BOOL", value); + } + setIntPref(prefName, value) { + return this._setPref(prefName, "INT", value); + } + setCharPref(prefName, value) { + return this._setPref(prefName, "CHAR", value); + } + setComplexValue(prefName, iid, value) { + return this._setPref(prefName, "COMPLEX", value, iid); + } + + // Mimic the clearUserPref API + clearUserPref(prefName) { + let msg = { + op: "clear", + prefName, + prefType: "", + }; + return this.sendQuery("SPPrefService", msg); + } + + // Private pref functions to communicate to chrome + async _getParentPref(prefName, prefType, { defaultValue, iid }) { + let msg = { + op: "get", + prefName, + prefType, + iid, // Only used with complex prefs + defaultValue, // Optional default value + }; + let val = await this.sendQuery("SPPrefService", msg); + if (val == null) { + throw new Error(`Error getting pref '${prefName}'`); + } + return val; + } + _getPref(prefName, prefType, { defaultValue }) { + switch (prefType) { + case "BOOL": + return Services.prefs.getBoolPref(prefName); + case "INT": + return Services.prefs.getIntPref(prefName); + case "CHAR": + return Services.prefs.getCharPref(prefName); + } + return undefined; + } + _setPref(prefName, prefType, prefValue, iid) { + let msg = { + op: "set", + prefName, + prefType, + iid, // Only used with complex prefs + prefValue, + }; + return this.sendQuery("SPPrefService", msg); + } + + _getMUDV(window) { + return window.docShell.contentViewer; + } + // XXX: these APIs really ought to be removed, they're not e10s-safe. + // (also they're pretty Firefox-specific) + _getTopChromeWindow(window) { + return window.docShell.rootTreeItem.domWindow; + } + _getAutoCompletePopup(window) { + return this._getTopChromeWindow(window).document.getElementById( + "PopupAutoComplete" + ); + } + addAutoCompletePopupEventListener(window, eventname, listener) { + this._getAutoCompletePopup(window).addEventListener(eventname, listener); + } + removeAutoCompletePopupEventListener(window, eventname, listener) { + this._getAutoCompletePopup(window).removeEventListener(eventname, listener); + } + get formHistory() { + let tmp = {}; + ChromeUtils.import("resource://gre/modules/FormHistory.jsm", tmp); + return WrapPrivileged.wrap(tmp.FormHistory); + } + getFormFillController(window) { + return Cc["@mozilla.org/satchel/form-fill-controller;1"].getService( + Ci.nsIFormFillController + ); + } + attachFormFillControllerTo(window) { + this.getFormFillController().attachPopupElementToBrowser( + window.docShell, + this._getAutoCompletePopup(window) + ); + } + detachFormFillControllerFrom(window) { + this.getFormFillController().detachFromBrowser(window.docShell); + } + isBackButtonEnabled(window) { + return !this._getTopChromeWindow(window) + .document.getElementById("Browser:Back") + .hasAttribute("disabled"); + } + // XXX end of problematic APIs + + addChromeEventListener(type, listener, capture, allowUntrusted) { + this.docShell.chromeEventHandler.addEventListener( + type, + listener, + capture, + allowUntrusted + ); + } + removeChromeEventListener(type, listener, capture) { + this.docShell.chromeEventHandler.removeEventListener( + type, + listener, + capture + ); + } + + // Note: each call to registerConsoleListener MUST be paired with a + // call to postConsoleSentinel; when the callback receives the + // sentinel it will unregister itself (_after_ calling the + // callback). SimpleTest.expectConsoleMessages does this for you. + // If you register more than one console listener, a call to + // postConsoleSentinel will zap all of them. + registerConsoleListener(callback) { + let listener = new SPConsoleListener(callback); + Services.console.registerListener(listener); + + // listen for dom/console events as well + Services.obs.addObserver(listener, "console-api-log-event"); + } + postConsoleSentinel() { + Services.console.logStringMessage("SENTINEL"); + } + resetConsole() { + Services.console.reset(); + } + + getFullZoom(window) { + return this._getMUDV(window).fullZoom; + } + getDeviceFullZoom(window) { + return this._getMUDV(window).deviceFullZoom; + } + setFullZoom(window, zoom) { + this._getMUDV(window).fullZoom = zoom; + } + getTextZoom(window) { + return this._getMUDV(window).textZoom; + } + setTextZoom(window, zoom) { + this._getMUDV(window).textZoom = zoom; + } + + getOverrideDPPX(window) { + return this._getMUDV(window).overrideDPPX; + } + setOverrideDPPX(window, dppx) { + this._getMUDV(window).overrideDPPX = dppx; + } + + emulateMedium(window, mediaType) { + this._getMUDV(window).emulateMedium(mediaType); + } + stopEmulatingMedium(window) { + this._getMUDV(window).stopEmulatingMedium(); + } + + // Takes a snapshot of the given window and returns a + // containing the image. When the window is same-process, the canvas + // is returned synchronously. When it is out-of-process (or when a + // BrowsingContext or FrameLoaderOwner is passed instead of a Window), + // a promise which resolves to such a canvas is returned instead. + snapshotWindowWithOptions(content, rect, bgcolor, options) { + function getImageData(rect, bgcolor, options) { + let el = content.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + if (rect === undefined) { + rect = { + top: content.scrollY, + left: content.scrollX, + width: content.innerWidth, + height: content.innerHeight, + }; + } + if (bgcolor === undefined) { + bgcolor = "rgb(255,255,255)"; + } + if (options === undefined) { + options = {}; + } + + el.width = rect.width; + el.height = rect.height; + let ctx = el.getContext("2d"); + + let flags = 0; + for (let option in options) { + flags |= options[option] && ctx[option]; + } + + ctx.drawWindow( + content, + rect.left, + rect.top, + rect.width, + rect.height, + bgcolor, + flags + ); + + return ctx.getImageData(0, 0, el.width, el.height); + } + + let toCanvas = imageData => { + let el = this.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + el.width = imageData.width; + el.height = imageData.height; + + if (ImageData.isInstance(imageData)) { + let ctx = el.getContext("2d"); + ctx.putImageData(imageData, 0, 0); + } + + return el; + }; + + if (Window.isInstance(content)) { + // Hack around tests that try to snapshot 0 width or height + // elements. + if (rect && !(rect.width && rect.height)) { + return toCanvas(rect); + } + + // This is an in-process window. Snapshot it synchronously. + return toCanvas(getImageData(rect, bgcolor, options)); + } + + // This is a remote window or frame. Snapshot it asynchronously and + // return a promise for the result. Alas, consumers expect us to + // return a element rather than an ImageData object, so we + // need to convert the result from the remote snapshot to a local + // canvas. + return this.spawn(content, [rect, bgcolor, options], getImageData).then( + toCanvas + ); + } + + snapshotWindow(win, withCaret, rect, bgcolor) { + return this.snapshotWindowWithOptions(win, rect, bgcolor, { + DRAWWINDOW_DRAW_CARET: withCaret, + }); + } + + snapshotRect(win, rect, bgcolor) { + return this.snapshotWindowWithOptions(win, rect, bgcolor); + } + + gc() { + this.DOMWindowUtils.garbageCollect(); + } + + forceGC() { + Cu.forceGC(); + } + + forceShrinkingGC() { + Cu.forceShrinkingGC(); + } + + forceCC() { + Cu.forceCC(); + } + + finishCC() { + Cu.finishCC(); + } + + ccSlice(budget) { + Cu.ccSlice(budget); + } + + // Due to various dependencies between JS objects and C++ objects, an ordinary + // forceGC doesn't necessarily clear all unused objects, thus the GC and CC + // needs to run several times and when no other JS is running. + // The current number of iterations has been determined according to massive + // cross platform testing. + exactGC(callback) { + let count = 0; + + function genGCCallback(cb) { + return function() { + Cu.forceCC(); + if (++count < 3) { + Cu.schedulePreciseGC(genGCCallback(cb)); + } else if (cb) { + cb(); + } + }; + } + + Cu.schedulePreciseGC(genGCCallback(callback)); + } + + nondeterministicGetWeakMapKeys(m) { + return ChromeUtils.nondeterministicGetWeakMapKeys(m); + } + + getMemoryReports() { + try { + Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager) + .getReports(() => {}, null, () => {}, null, false); + } catch (e) {} + } + + setGCZeal(zeal) { + Cu.setGCZeal(zeal); + } + + isMainProcess() { + try { + return ( + Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); + } catch (e) {} + return true; + } + + get XPCOMABI() { + if (this._xpcomabi != null) { + return this._xpcomabi; + } + + var xulRuntime = Services.appinfo.QueryInterface(Ci.nsIXULRuntime); + + this._xpcomabi = xulRuntime.XPCOMABI; + return this._xpcomabi; + } + + // The optional aWin parameter allows the caller to specify a given window in + // whose scope the runnable should be dispatched. If aFun throws, the + // exception will be reported to aWin. + executeSoon(aFun, aWin) { + // Create the runnable in the scope of aWin to avoid running into COWs. + var runnable = {}; + if (aWin) { + runnable = Cu.createObjectIn(aWin); + } + runnable.run = aFun; + Cu.dispatch(runnable, aWin); + } + + get OS() { + if (this._os != null) { + return this._os; + } + + this._os = Services.appinfo.OS; + return this._os; + } + + get useRemoteSubframes() { + return this.docShell.nsILoadContext.useRemoteSubframes; + } + + addSystemEventListener(target, type, listener, useCapture) { + Services.els.addSystemEventListener(target, type, listener, useCapture); + } + removeSystemEventListener(target, type, listener, useCapture) { + Services.els.removeSystemEventListener(target, type, listener, useCapture); + } + + // helper method to check if the event is consumed by either default group's + // event listener or system group's event listener. + defaultPreventedInAnyGroup(event) { + // FYI: Event.defaultPrevented returns false in content context if the + // event is consumed only by system group's event listeners. + return event.defaultPrevented; + } + + getDOMRequestService() { + var serv = Services.DOMRequest; + var res = {}; + var props = [ + "createRequest", + "createCursor", + "fireError", + "fireSuccess", + "fireDone", + "fireDetailedError", + ]; + for (var i in props) { + let prop = props[i]; + res[prop] = function() { + return serv[prop].apply(serv, arguments); + }; + } + return res; + } + + addCategoryEntry(category, entry, value, persists, replace) { + Services.catMan.addCategoryEntry(category, entry, value, persists, replace); + } + + deleteCategoryEntry(category, entry, persists) { + Services.catMan.deleteCategoryEntry(category, entry, persists); + } + openDialog(win, args) { + return win.openDialog.apply(win, args); + } + // This is a blocking call which creates and spins a native event loop + spinEventLoop(win) { + // simply do a sync XHR back to our windows location. + var syncXHR = new win.XMLHttpRequest(); + syncXHR.open("GET", win.location, false); + syncXHR.send(); + } + + // :jdm gets credit for this. ex: getPrivilegedProps(window, 'location.href'); + getPrivilegedProps(obj, props) { + var parts = props.split("."); + for (var i = 0; i < parts.length; i++) { + var p = parts[i]; + if (obj[p] != undefined) { + obj = obj[p]; + } else { + return null; + } + } + return obj; + } + + _browsingContextForTarget(target) { + if (BrowsingContext.isInstance(target)) { + return target; + } + if (Element.isInstance(target)) { + return target.browsingContext; + } + + return BrowsingContext.getFromWindow(target); + } + + /** + * Runs a task in the context of the given frame, and returns a + * promise which resolves to the return value of that task. + * + * The given frame may be in-process or out-of-process. Either way, + * the task will run asynchronously, in a sandbox with access to the + * frame's content window via its `content` global. Any arguments + * passed will be copied via structured clone, as will its return + * value. + * + * The sandbox also has access to an Assert object, as provided by + * Assert.jsm. Any assertion methods called before the task resolves + * will be relayed back to the test environment of the caller. + * + * @param {BrowsingContext or FrameLoaderOwner or WindowProxy} target + * The target in which to run the task. This may be any element + * which implements the FrameLoaderOwner interface (including + * HTML