summaryrefslogtreecommitdiffstats
path: root/testing/specialpowers/content
diff options
context:
space:
mode:
Diffstat (limited to 'testing/specialpowers/content')
-rw-r--r--testing/specialpowers/content/AppTestDelegate.sys.mjs52
-rw-r--r--testing/specialpowers/content/AppTestDelegateChild.sys.mjs18
-rw-r--r--testing/specialpowers/content/AppTestDelegateParent.sys.mjs85
-rw-r--r--testing/specialpowers/content/MockColorPicker.sys.mjs123
-rw-r--r--testing/specialpowers/content/MockFilePicker.sys.mjs315
-rw-r--r--testing/specialpowers/content/MockPermissionPrompt.sys.mjs75
-rw-r--r--testing/specialpowers/content/SpecialPowersChild.sys.mjs2329
-rw-r--r--testing/specialpowers/content/SpecialPowersEventUtils.sys.mjs35
-rw-r--r--testing/specialpowers/content/SpecialPowersParent.sys.mjs1481
-rw-r--r--testing/specialpowers/content/SpecialPowersSandbox.sys.mjs141
-rw-r--r--testing/specialpowers/content/WrapPrivileged.sys.mjs385
11 files changed, 5039 insertions, 0 deletions
diff --git a/testing/specialpowers/content/AppTestDelegate.sys.mjs b/testing/specialpowers/content/AppTestDelegate.sys.mjs
new file mode 100644
index 0000000000..97fe60341b
--- /dev/null
+++ b/testing/specialpowers/content/AppTestDelegate.sys.mjs
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+class Delegate {
+ actor(window) {
+ return window.windowGlobalChild.getActor("AppTestDelegate");
+ }
+
+ clickPageAction(window, extension) {
+ return this.actor(window).sendQuery("clickPageAction", {
+ extensionId: extension.id,
+ });
+ }
+
+ clickBrowserAction(window, extension) {
+ return this.actor(window).sendQuery("clickBrowserAction", {
+ extensionId: extension.id,
+ });
+ }
+
+ closeBrowserAction(window, extension) {
+ return this.actor(window).sendQuery("closeBrowserAction", {
+ extensionId: extension.id,
+ });
+ }
+
+ closePageAction(window, extension) {
+ return this.actor(window).sendQuery("closePageAction", {
+ extensionId: extension.id,
+ });
+ }
+
+ awaitExtensionPanel(window, extension) {
+ return this.actor(window).sendQuery("awaitExtensionPanel", {
+ extensionId: extension.id,
+ });
+ }
+
+ async openNewForegroundTab(window, url, waitForLoad = true) {
+ const tabId = await this.actor(window).sendQuery("openNewForegroundTab", {
+ url,
+ waitForLoad,
+ });
+ return { id: tabId };
+ }
+
+ removeTab(window, tab) {
+ return this.actor(window).sendQuery("removeTab", { tabId: tab.id });
+ }
+}
+
+export var AppTestDelegate = new Delegate();
diff --git a/testing/specialpowers/content/AppTestDelegateChild.sys.mjs b/testing/specialpowers/content/AppTestDelegateChild.sys.mjs
new file mode 100644
index 0000000000..1d78d41f5d
--- /dev/null
+++ b/testing/specialpowers/content/AppTestDelegateChild.sys.mjs
@@ -0,0 +1,18 @@
+/* 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/. */
+
+export class AppTestDelegateChild extends JSWindowActorChild {
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMContentLoaded":
+ case "load": {
+ this.sendAsyncMessage(event.type, {
+ internalURL: event.target.documentURI,
+ visibleURL: event.target.location?.href,
+ });
+ break;
+ }
+ }
+ }
+}
diff --git a/testing/specialpowers/content/AppTestDelegateParent.sys.mjs b/testing/specialpowers/content/AppTestDelegateParent.sys.mjs
new file mode 100644
index 0000000000..4935f3245a
--- /dev/null
+++ b/testing/specialpowers/content/AppTestDelegateParent.sys.mjs
@@ -0,0 +1,85 @@
+/* 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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ // Each app needs to implement this
+ AppUiTestDelegate: "resource://testing-common/AppUiTestDelegate.sys.mjs",
+});
+
+export class AppTestDelegateParent extends JSWindowActorParent {
+ constructor() {
+ super();
+ this._tabs = new Map();
+ }
+
+ get browser() {
+ return this.browsingContext.top.embedderElement;
+ }
+
+ get window() {
+ return this.browser.ownerGlobal;
+ }
+
+ async receiveMessage(message) {
+ const { extensionId, url, waitForLoad, tabId } = message.data;
+ switch (message.name) {
+ case "DOMContentLoaded":
+ case "load": {
+ return this.browser?.dispatchEvent(
+ new CustomEvent(`AppTestDelegate:${message.name}`, {
+ detail: {
+ browsingContext: this.browsingContext,
+ ...message.data,
+ },
+ })
+ );
+ }
+ case "clickPageAction":
+ return lazy.AppUiTestDelegate.clickPageAction(this.window, extensionId);
+ case "clickBrowserAction":
+ return lazy.AppUiTestDelegate.clickBrowserAction(
+ this.window,
+ extensionId
+ );
+ case "closePageAction":
+ return lazy.AppUiTestDelegate.closePageAction(this.window, extensionId);
+ case "closeBrowserAction":
+ return lazy.AppUiTestDelegate.closeBrowserAction(
+ this.window,
+ extensionId
+ );
+ case "awaitExtensionPanel":
+ // The desktop delegate returns a <browser>, but that cannot be sent
+ // over IPC, so just ignore it. The promise resolves when the panel and
+ // its content is fully loaded.
+ await lazy.AppUiTestDelegate.awaitExtensionPanel(
+ this.window,
+ extensionId
+ );
+ return null;
+ case "openNewForegroundTab": {
+ // We cannot send the tab object across process so let's store it with
+ // a unique ID here.
+ const uuid = Services.uuid.generateUUID().toString();
+ const tab = await lazy.AppUiTestDelegate.openNewForegroundTab(
+ this.window,
+ url,
+ waitForLoad
+ );
+ this._tabs.set(uuid, tab);
+ return uuid;
+ }
+ case "removeTab": {
+ const tab = this._tabs.get(tabId);
+ this._tabs.delete(tabId);
+ return lazy.AppUiTestDelegate.removeTab(tab);
+ }
+
+ default:
+ throw new Error(`Unknown Test API: ${message.name}.`);
+ }
+ }
+}
diff --git a/testing/specialpowers/content/MockColorPicker.sys.mjs b/testing/specialpowers/content/MockColorPicker.sys.mjs
new file mode 100644
index 0000000000..1b4c4d3b3e
--- /dev/null
+++ b/testing/specialpowers/content/MockColorPicker.sys.mjs
@@ -0,0 +1,123 @@
+/* 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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ WrapPrivileged: "resource://testing-common/WrapPrivileged.sys.mjs",
+});
+
+const Cm = Components.manager;
+
+const CONTRACT_ID = "@mozilla.org/colorpicker;1";
+
+Cu.crashIfNotInAutomation();
+
+var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+var oldClassID = "";
+var newClassID = Services.uuid.generateUUID();
+var newFactory = function (window) {
+ return {
+ createInstance(aIID) {
+ return new MockColorPickerInstance(window).QueryInterface(aIID);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
+ };
+};
+
+export var MockColorPicker = {
+ init(window) {
+ this.reset();
+ this.factory = newFactory(window);
+ if (!registrar.isCIDRegistered(newClassID)) {
+ try {
+ oldClassID = registrar.contractIDToCID(CONTRACT_ID);
+ } catch (ex) {
+ oldClassID = "";
+ dump(
+ "TEST-INFO | can't get colorpicker registered component, " +
+ "assuming there is none"
+ );
+ }
+ registrar.registerFactory(newClassID, "", CONTRACT_ID, this.factory);
+ }
+ },
+
+ reset() {
+ this.returnColor = "";
+ this.showCallback = null;
+ this.shown = false;
+ this.showing = false;
+ },
+
+ cleanup() {
+ var previousFactory = this.factory;
+ this.reset();
+ this.factory = null;
+
+ registrar.unregisterFactory(newClassID, previousFactory);
+ if (oldClassID != "") {
+ registrar.registerFactory(oldClassID, "", CONTRACT_ID, null);
+ }
+ },
+};
+
+function MockColorPickerInstance(window) {
+ this.window = window;
+ this.showCallback = null;
+ this.showCallbackWrapped = null;
+}
+MockColorPickerInstance.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIColorPicker"]),
+ init(aParent, aTitle, aInitialColor, aDefaultColors) {
+ this.parent = aParent;
+ this.initialColor = aInitialColor;
+ this.defaultColors = aDefaultColors;
+ },
+ initialColor: "",
+ parent: null,
+ open(aColorPickerShownCallback) {
+ MockColorPicker.showing = true;
+ MockColorPicker.shown = true;
+
+ this.window.setTimeout(() => {
+ let result = "";
+ try {
+ if (typeof MockColorPicker.showCallback == "function") {
+ if (MockColorPicker.showCallback != this.showCallback) {
+ this.showCallback = MockColorPicker.showCallback;
+ if (Cu.isXrayWrapper(this.window)) {
+ this.showCallbackWrapped = lazy.WrapPrivileged.wrapCallback(
+ MockColorPicker.showCallback,
+ this.window
+ );
+ } else {
+ this.showCallbackWrapped = this.showCallback;
+ }
+ }
+ var updateCb = function (color) {
+ result = color;
+ aColorPickerShownCallback.update(color);
+ };
+ let returnColor = this.showCallbackWrapped(this, updateCb);
+ if (typeof returnColor === "string") {
+ result = returnColor;
+ }
+ } else if (typeof MockColorPicker.returnColor === "string") {
+ result = MockColorPicker.returnColor;
+ }
+ } catch (ex) {
+ dump(
+ "TEST-UNEXPECTED-FAIL | Exception in MockColorPicker.sys.mjs open() " +
+ "method: " +
+ ex +
+ "\n"
+ );
+ }
+ if (aColorPickerShownCallback) {
+ aColorPickerShownCallback.done(result);
+ }
+ }, 0);
+ },
+};
diff --git a/testing/specialpowers/content/MockFilePicker.sys.mjs b/testing/specialpowers/content/MockFilePicker.sys.mjs
new file mode 100644
index 0000000000..9a7dab7c17
--- /dev/null
+++ b/testing/specialpowers/content/MockFilePicker.sys.mjs
@@ -0,0 +1,315 @@
+/* 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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ WrapPrivileged: "resource://testing-common/WrapPrivileged.sys.mjs",
+});
+
+const Cm = Components.manager;
+
+const CONTRACT_ID = "@mozilla.org/filepicker;1";
+
+if (import.meta.url.includes("specialpowers")) {
+ Cu.crashIfNotInAutomation();
+}
+
+var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+var oldClassID;
+var newClassID = Services.uuid.generateUUID();
+var newFactory = function (window) {
+ return {
+ createInstance(aIID) {
+ return new MockFilePickerInstance(window).QueryInterface(aIID);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
+ };
+};
+
+export var MockFilePicker = {
+ returnOK: Ci.nsIFilePicker.returnOK,
+ returnCancel: Ci.nsIFilePicker.returnCancel,
+ returnReplace: Ci.nsIFilePicker.returnReplace,
+
+ filterAll: Ci.nsIFilePicker.filterAll,
+ filterHTML: Ci.nsIFilePicker.filterHTML,
+ filterText: Ci.nsIFilePicker.filterText,
+ filterImages: Ci.nsIFilePicker.filterImages,
+ filterXML: Ci.nsIFilePicker.filterXML,
+ filterXUL: Ci.nsIFilePicker.filterXUL,
+ filterApps: Ci.nsIFilePicker.filterApps,
+ filterAllowURLs: Ci.nsIFilePicker.filterAllowURLs,
+ filterAudio: Ci.nsIFilePicker.filterAudio,
+ filterVideo: Ci.nsIFilePicker.filterVideo,
+
+ window: null,
+ pendingPromises: [],
+
+ init(window) {
+ this.window = window;
+
+ this.reset();
+ this.factory = newFactory(window);
+ if (!registrar.isCIDRegistered(newClassID)) {
+ oldClassID = registrar.contractIDToCID(CONTRACT_ID);
+ registrar.registerFactory(newClassID, "", CONTRACT_ID, this.factory);
+ }
+ },
+
+ reset() {
+ this.appendFilterCallback = null;
+ this.appendFiltersCallback = null;
+ this.displayDirectory = null;
+ this.displaySpecialDirectory = "";
+ this.filterIndex = 0;
+ this.mode = null;
+ this.returnData = [];
+ this.returnValue = null;
+ this.showCallback = null;
+ this.afterOpenCallback = null;
+ this.shown = false;
+ this.showing = false;
+ },
+
+ cleanup() {
+ var previousFactory = this.factory;
+ this.reset();
+ this.factory = null;
+ if (oldClassID) {
+ registrar.unregisterFactory(newClassID, previousFactory);
+ registrar.registerFactory(oldClassID, "", CONTRACT_ID, null);
+ }
+ },
+
+ internalFileData(obj) {
+ return {
+ nsIFile: "nsIFile" in obj ? obj.nsIFile : null,
+ domFile: "domFile" in obj ? obj.domFile : null,
+ domDirectory: "domDirectory" in obj ? obj.domDirectory : null,
+ };
+ },
+
+ useAnyFile() {
+ var file = lazy.FileUtils.getDir("TmpD", [], false);
+ file.append("testfile");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ let promise = this.window.File.createFromNsIFile(file)
+ .then(
+ domFile => domFile,
+ () => null
+ )
+ // domFile can be null.
+ .then(domFile => {
+ this.returnData = [this.internalFileData({ nsIFile: file, domFile })];
+ })
+ .then(() => file);
+
+ this.pendingPromises = [promise];
+
+ // We return a promise in order to support some existing mochitests.
+ return promise;
+ },
+
+ useBlobFile() {
+ var blob = new this.window.Blob([]);
+ var file = new this.window.File([blob], "helloworld.txt", {
+ type: "plain/text",
+ });
+ this.returnData = [this.internalFileData({ domFile: file })];
+ this.pendingPromises = [];
+ },
+
+ useDirectory(aPath) {
+ var directory = new this.window.Directory(aPath);
+ this.returnData = [this.internalFileData({ domDirectory: directory })];
+ this.pendingPromises = [];
+ },
+
+ setFiles(files) {
+ this.returnData = [];
+ this.pendingPromises = [];
+
+ for (let file of files) {
+ if (this.window.File.isInstance(file)) {
+ this.returnData.push(this.internalFileData({ domFile: file }));
+ } else {
+ let promise = this.window.File.createFromNsIFile(file, {
+ existenceCheck: false,
+ });
+
+ promise.then(domFile => {
+ this.returnData.push(
+ this.internalFileData({ nsIFile: file, domFile })
+ );
+ });
+ this.pendingPromises.push(promise);
+ }
+ }
+ },
+
+ getNsIFile() {
+ if (this.returnData.length >= 1) {
+ return this.returnData[0].nsIFile;
+ }
+ return null;
+ },
+};
+
+function MockFilePickerInstance(window) {
+ this.window = window;
+ this.showCallback = null;
+ this.showCallbackWrapped = null;
+}
+MockFilePickerInstance.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFilePicker"]),
+ init(aParent, aTitle, aMode) {
+ this.mode = aMode;
+ this.filterIndex = MockFilePicker.filterIndex;
+ this.parent = aParent;
+ },
+ appendFilter(aTitle, aFilter) {
+ if (typeof MockFilePicker.appendFilterCallback == "function") {
+ MockFilePicker.appendFilterCallback(this, aTitle, aFilter);
+ }
+ },
+ appendFilters(aFilterMask) {
+ if (typeof MockFilePicker.appendFiltersCallback == "function") {
+ MockFilePicker.appendFiltersCallback(this, aFilterMask);
+ }
+ },
+ defaultString: "",
+ defaultExtension: "",
+ parent: null,
+ filterIndex: 0,
+ displayDirectory: null,
+ displaySpecialDirectory: "",
+ get file() {
+ if (MockFilePicker.returnData.length >= 1) {
+ return MockFilePicker.returnData[0].nsIFile;
+ }
+
+ return null;
+ },
+
+ // We don't support directories here.
+ get domFileOrDirectory() {
+ if (MockFilePicker.returnData.length < 1) {
+ return null;
+ }
+
+ if (MockFilePicker.returnData[0].domFile) {
+ return MockFilePicker.returnData[0].domFile;
+ }
+
+ if (MockFilePicker.returnData[0].domDirectory) {
+ return MockFilePicker.returnData[0].domDirectory;
+ }
+
+ return null;
+ },
+ get fileURL() {
+ if (
+ MockFilePicker.returnData.length >= 1 &&
+ MockFilePicker.returnData[0].nsIFile
+ ) {
+ return Services.io.newFileURI(MockFilePicker.returnData[0].nsIFile);
+ }
+
+ return null;
+ },
+ *getFiles(asDOM) {
+ for (let d of MockFilePicker.returnData) {
+ if (asDOM) {
+ yield d.domFile || d.domDirectory;
+ } else if (d.nsIFile) {
+ yield d.nsIFile;
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ }
+ },
+ get files() {
+ return this.getFiles(false);
+ },
+ get domFileOrDirectoryEnumerator() {
+ return this.getFiles(true);
+ },
+ open(aFilePickerShownCallback) {
+ MockFilePicker.showing = true;
+ Services.tm.dispatchToMainThread(() => {
+ // Maybe all the pending promises are already resolved, but we want to be sure.
+ Promise.all(MockFilePicker.pendingPromises)
+ .then(
+ () => {
+ return Ci.nsIFilePicker.returnOK;
+ },
+ () => {
+ return Ci.nsIFilePicker.returnCancel;
+ }
+ )
+ .then(result => {
+ // Nothing else has to be done.
+ MockFilePicker.pendingPromises = [];
+
+ if (result == Ci.nsIFilePicker.returnCancel) {
+ return result;
+ }
+
+ MockFilePicker.displayDirectory = this.displayDirectory;
+ MockFilePicker.displaySpecialDirectory = this.displaySpecialDirectory;
+ MockFilePicker.shown = true;
+ if (typeof MockFilePicker.showCallback == "function") {
+ if (MockFilePicker.showCallback != this.showCallback) {
+ this.showCallback = MockFilePicker.showCallback;
+ if (Cu.isXrayWrapper(this.window)) {
+ this.showCallbackWrapped = lazy.WrapPrivileged.wrapCallback(
+ MockFilePicker.showCallback,
+ this.window
+ );
+ } else {
+ this.showCallbackWrapped = this.showCallback;
+ }
+ }
+ try {
+ var returnValue = this.showCallbackWrapped(this);
+ if (typeof returnValue != "undefined") {
+ return returnValue;
+ }
+ } catch (ex) {
+ return Ci.nsIFilePicker.returnCancel;
+ }
+ }
+
+ return MockFilePicker.returnValue;
+ })
+ .then(result => {
+ // Some additional result file can be set by the callback. Let's
+ // resolve the pending promises again.
+ return Promise.all(MockFilePicker.pendingPromises).then(
+ () => {
+ return result;
+ },
+ () => {
+ return Ci.nsIFilePicker.returnCancel;
+ }
+ );
+ })
+ .then(result => {
+ MockFilePicker.pendingPromises = [];
+
+ if (aFilePickerShownCallback) {
+ aFilePickerShownCallback.done(result);
+ }
+
+ if (typeof MockFilePicker.afterOpenCallback == "function") {
+ Services.tm.dispatchToMainThread(() => {
+ MockFilePicker.afterOpenCallback(this);
+ });
+ }
+ });
+ });
+ },
+};
diff --git a/testing/specialpowers/content/MockPermissionPrompt.sys.mjs b/testing/specialpowers/content/MockPermissionPrompt.sys.mjs
new file mode 100644
index 0000000000..615c786f2c
--- /dev/null
+++ b/testing/specialpowers/content/MockPermissionPrompt.sys.mjs
@@ -0,0 +1,75 @@
+/* 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 Cm = Components.manager;
+
+const CONTRACT_ID = "@mozilla.org/content-permission/prompt;1";
+
+var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+var oldClassID, oldFactory;
+var newClassID = Services.uuid.generateUUID();
+var newFactory = {
+ createInstance(aIID) {
+ return new MockPermissionPromptInstance().QueryInterface(aIID);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
+};
+
+export var MockPermissionPrompt = {
+ init() {
+ this.reset();
+ if (!registrar.isCIDRegistered(newClassID)) {
+ try {
+ oldClassID = registrar.contractIDToCID(CONTRACT_ID);
+ oldFactory = Cm.getClassObject(Cc[CONTRACT_ID], Ci.nsIFactory);
+ } catch (ex) {
+ oldClassID = "";
+ oldFactory = null;
+ dump(
+ "TEST-INFO | can't get permission prompt registered component, " +
+ "assuming there is none"
+ );
+ }
+ if (oldFactory) {
+ registrar.unregisterFactory(oldClassID, oldFactory);
+ }
+ registrar.registerFactory(newClassID, "", CONTRACT_ID, newFactory);
+ }
+ },
+
+ reset() {},
+
+ cleanup() {
+ this.reset();
+ if (oldFactory) {
+ registrar.unregisterFactory(newClassID, newFactory);
+ registrar.registerFactory(oldClassID, "", CONTRACT_ID, oldFactory);
+ }
+ },
+};
+
+function MockPermissionPromptInstance() {}
+MockPermissionPromptInstance.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionPrompt"]),
+
+ promptResult: Ci.nsIPermissionManager.UNKNOWN_ACTION,
+
+ prompt(request) {
+ let perms = request.types.QueryInterface(Ci.nsIArray);
+ for (let idx = 0; idx < perms.length; idx++) {
+ let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType);
+ if (
+ Services.perms.testExactPermissionFromPrincipal(
+ request.principal,
+ perm.type
+ ) != Ci.nsIPermissionManager.ALLOW_ACTION
+ ) {
+ request.cancel();
+ return;
+ }
+ }
+
+ request.allow();
+ },
+};
diff --git a/testing/specialpowers/content/SpecialPowersChild.sys.mjs b/testing/specialpowers/content/SpecialPowersChild.sys.mjs
new file mode 100644
index 0000000000..7b40aab198
--- /dev/null
+++ b/testing/specialpowers/content/SpecialPowersChild.sys.mjs
@@ -0,0 +1,2329 @@
+/* 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.
+ */
+
+import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ContentTaskUtils: "resource://testing-common/ContentTaskUtils.sys.mjs",
+ MockColorPicker: "resource://testing-common/MockColorPicker.sys.mjs",
+ MockFilePicker: "resource://testing-common/MockFilePicker.sys.mjs",
+ MockPermissionPrompt:
+ "resource://testing-common/MockPermissionPrompt.sys.mjs",
+ PerTestCoverageUtils:
+ "resource://testing-common/PerTestCoverageUtils.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ SpecialPowersSandbox:
+ "resource://testing-common/SpecialPowersSandbox.sys.mjs",
+ WrapPrivileged: "resource://testing-common/WrapPrivileged.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "NetUtil",
+ "resource://gre/modules/NetUtil.jsm"
+);
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+Cu.crashIfNotInAutomation();
+
+function bindDOMWindowUtils(aWindow) {
+ return aWindow && lazy.WrapPrivileged.wrap(aWindow.windowUtils, aWindow);
+}
+
+function defineSpecialPowers(sp) {
+ let window = sp.contentWindow;
+ window.SpecialPowers = sp;
+ if (window === window.wrappedJSObject) {
+ return;
+ }
+ // We can't use a generic |defineLazyGetter| because it does not
+ // allow customizing the re-definition behavior.
+ Object.defineProperty(window.wrappedJSObject, "SpecialPowers", {
+ get() {
+ let value = lazy.WrapPrivileged.wrap(sp, window);
+ // If we bind |window.wrappedJSObject| when defining the getter
+ // and use it here, it might become a dead wrapper.
+ // We have to retrieve |wrappedJSObject| again.
+ Object.defineProperty(window.wrappedJSObject, "SpecialPowers", {
+ configurable: true,
+ enumerable: true,
+ value,
+ writable: true,
+ });
+ return value;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+}
+
+// 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, contentWindow) {
+ this.callback = callback;
+ this.contentWindow = contentWindow;
+}
+
+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,
+ };
+ 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;
+ }
+
+ 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, Cu.cloneInto(m, this.contentWindow));
+ });
+
+ if (!m.isScriptError && !m.isConsoleEvent && m.message === "SENTINEL") {
+ Services.console.unregisterListener(this);
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener", "nsIObserver"]),
+};
+
+export class SpecialPowersChild extends JSWindowActorChild {
+ constructor() {
+ super();
+
+ this._windowID = null;
+
+ this._encounteredCrashDumpFiles = [];
+ this._unexpectedCrashDumpFiles = {};
+ this._crashDumpDir = null;
+ this._serviceWorkerRegistered = false;
+ this._serviceWorkerCleanUpRequests = new Map();
+ Object.defineProperty(this, "Components", {
+ configurable: true,
+ enumerable: true,
+ value: Components,
+ });
+ this._createFilesOnError = null;
+ this._createFilesOnSuccess = null;
+
+ this._messageListeners = new ExtensionUtils.DefaultMap(() => new Set());
+
+ this._consoleListeners = [];
+ this._spawnTaskImports = {};
+ this._encounteredCrashDumpFiles = [];
+ this._unexpectedCrashDumpFiles = {};
+ this._crashDumpDir = null;
+ this._mfl = null;
+ this._asyncObservers = new WeakMap();
+ this._xpcomabi = null;
+ this._os = null;
+ this._pu = null;
+
+ this._nextExtensionID = 0;
+ this._extensionListeners = null;
+
+ lazy.WrapPrivileged.disableAutoWrap(
+ this.unwrap,
+ this.isWrapper,
+ this.wrapCallback,
+ this.wrapCallbackObject,
+ this.setWrapped,
+ this.nondeterministicGetWeakMapKeys,
+ this.snapshotWindowWithOptions,
+ this.snapshotWindow,
+ this.snapshotRect,
+ this.getDOMRequestService
+ );
+ }
+
+ observe(aSubject, aTopic, aData) {
+ // Ignore the "{chrome/content}-document-global-created" event. It
+ // is only observed to force creation of the actor.
+ }
+
+ actorCreated() {
+ this.attachToWindow();
+ }
+
+ attachToWindow() {
+ let window = this.contentWindow;
+ // We should not invoke the getter.
+ if (!("SpecialPowers" in window.wrappedJSObject)) {
+ this._windowID = window.windowGlobalChild.innerWindowId;
+
+ defineSpecialPowers(this);
+ }
+ }
+
+ 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) {
+ console.error(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, imports } = message.data;
+ return this._spawnTask(task, args, caller, taskId, imports);
+
+ case "EnsureFocus":
+ // Ensure that the focus is in this child document. Returns a browsing
+ // context of a child frame if a subframe should be focused or undefined
+ // otherwise.
+
+ // If a subframe node is focused, then the focus will actually
+ // be within that subframe's document. If blurSubframe is true,
+ // then blur the subframe so that this parent document is focused
+ // instead. If blurSubframe is false, then return the browsing
+ // context for that subframe. The parent process will then call back
+ // into this same code but in the process for that subframe.
+ let focusedNode = this.document.activeElement;
+ let subframeFocused =
+ ChromeUtils.getClassName(focusedNode) == "HTMLIFrameElement" ||
+ ChromeUtils.getClassName(focusedNode) == "HTMLFrameElement" ||
+ ChromeUtils.getClassName(focusedNode) == "XULFrameElement";
+ if (subframeFocused) {
+ if (message.data.blurSubframe) {
+ Services.focus.clearFocus(this.contentWindow);
+ } else {
+ if (!this.document.hasFocus()) {
+ this.contentWindow.focus();
+ }
+ return Promise.resolve(focusedNode.browsingContext);
+ }
+ }
+
+ // A subframe is not focused, so if this document is
+ // not focused, focus it and wait for the focus event.
+ if (!this.document.hasFocus()) {
+ return new Promise(resolve => {
+ this.document.addEventListener(
+ "focus",
+ () => {
+ resolve();
+ },
+ {
+ capture: true,
+ once: true,
+ }
+ );
+ this.contentWindow.focus();
+ });
+ }
+ break;
+
+ case "Assert":
+ {
+ if ("info" in message.data) {
+ (this.xpcshellScope || this.SimpleTest).info(message.data.info);
+ break;
+ }
+
+ // An assertion has been done in a mochitest chrome script
+ let { name, passed, stack, diag, expectFail } = message.data;
+
+ let { SimpleTest } = this;
+ if (SimpleTest) {
+ let expected = expectFail ? "fail" : "pass";
+ SimpleTest.record(passed, name, diag, stack, expected);
+ } else if (this.xpcshellScope) {
+ this.xpcshellScope.do_report_result(passed, name, stack);
+ } else {
+ // Well, this is unexpected.
+ dump(name + "\n");
+ }
+ }
+ break;
+ }
+ return undefined;
+ }
+
+ registerProcessCrashObservers() {
+ this.sendAsyncMessage("SPProcessCrashService", { op: "register-observer" });
+ }
+
+ unregisterProcessCrashObservers() {
+ this.sendAsyncMessage("SPProcessCrashService", {
+ op: "unregister-observer",
+ });
+ }
+
+ /*
+ * 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 obj;
+ }
+ unwrap(obj) {
+ return lazy.WrapPrivileged.unwrap(obj);
+ }
+ isWrapper(val) {
+ return lazy.WrapPrivileged.isWrapper(val);
+ }
+
+ unwrapIfWrapped(obj) {
+ return lazy.WrapPrivileged.isWrapper(obj)
+ ? lazy.WrapPrivileged.unwrap(obj)
+ : obj;
+ }
+
+ /*
+ * Wrap objects on a specified global.
+ */
+ wrapFor(obj, win) {
+ return lazy.WrapPrivileged.wrap(obj, win);
+ }
+
+ /*
+ * 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 lazy.WrapPrivileged.wrapCallback(func, this.contentWindow);
+ }
+ wrapCallbackObject(obj) {
+ return lazy.WrapPrivileged.wrapCallbackObject(obj, this.contentWindow);
+ }
+
+ /*
+ * Used for assigning a property to a SpecialPowers wrapper, without unwrapping
+ * the value that is assigned.
+ */
+ setWrapped(obj, prop, val) {
+ if (!lazy.WrapPrivileged.isWrapper(obj)) {
+ throw new Error(
+ "You only need to use this for SpecialPowers wrapped objects"
+ );
+ }
+
+ obj = lazy.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 lazy.WrapPrivileged.unwrap(a) === lazy.WrapPrivileged.unwrap(b);
+ }
+
+ get MockFilePicker() {
+ return lazy.MockFilePicker;
+ }
+
+ get MockColorPicker() {
+ return lazy.MockColorPicker;
+ }
+
+ get MockPermissionPrompt() {
+ return lazy.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(
+ files => onCreation(Cu.cloneInto(files, this.contentWindow)),
+ 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() {
+ // 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.
+ */
+ 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;
+ let blob = new Blob([str], { type: "application/javascript" });
+ let blobUrl = URL.createObjectURL(blob);
+ Services.scriptloader.loadSubScript(blobUrl, sb);
+
+ 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 = lazy.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 id = Services.uuid.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(
+ `SpecialPowers/receiveMessage/${name}`,
+ null,
+ 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 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 Services;
+ }
+
+ /*
+ * Convenient shortcuts to the standard Components abbreviations.
+ */
+ get Cc() {
+ return Cc;
+ }
+ get Ci() {
+ return Ci;
+ }
+ get Cu() {
+ return Cu;
+ }
+ get Cr() {
+ return Cr;
+ }
+
+ get ChromeUtils() {
+ return ChromeUtils;
+ }
+
+ get isHeadless() {
+ return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless;
+ }
+
+ get addProfilerMarker() {
+ return ChromeUtils.addProfilerMarker;
+ }
+
+ get DOMWindowUtils() {
+ return this.contentWindow.windowUtils;
+ }
+
+ getDOMWindowUtils(aWindow) {
+ if (aWindow == this.contentWindow) {
+ return aWindow.windowUtils;
+ }
+
+ return bindDOMWindowUtils(Cu.unwaiveXrays(aWindow));
+ }
+
+ async toggleMuteState(aMuted, aWindow) {
+ let actor = aWindow
+ ? aWindow.windowGlobalChild.getActor("SpecialPowers")
+ : this;
+ return actor.sendQuery("SPToggleMuteAudio", { mute: aMuted });
+ }
+
+ /*
+ * 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 new DOMParser();
+ }
+
+ get InspectorUtils() {
+ return InspectorUtils;
+ }
+
+ get PromiseDebugging() {
+ return 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, delay = 0) {
+ // for mochitest-browser
+ if (typeof this.chromeWindow != "undefined") {
+ this.chromeWindow.setTimeout(callback, delay);
+ }
+ // for mochitest-plain
+ else {
+ this.contentWindow.setTimeout(callback, delay);
+ }
+ }
+
+ 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) {
+ let permissions = [];
+ for (let perm of inPermissions) {
+ let principal = this._getPrincipalFromArg(perm.context);
+ permissions.push({
+ ...perm,
+ context: null,
+ principal,
+ });
+ }
+
+ await this.sendQuery("PushPermissions", permissions).then(callback);
+ await this.promiseTimeout(0);
+ }
+
+ async popPermissions(callback = null) {
+ await this.sendQuery("PopPermissions").then(callback);
+ await this.promiseTimeout(0);
+ }
+
+ async flushPermissions(callback = null) {
+ await this.sendQuery("FlushPermissions").then(callback);
+ await this.promiseTimeout(0);
+ }
+
+ /*
+ * 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.
+ */
+ registerObservers(topic) {
+ var msg = {
+ op: "add",
+ observerTopic: topic,
+ };
+ return this.sendQuery("SPObserverService", msg);
+ }
+
+ async pushPrefEnv(inPrefs, callback = null) {
+ let { requiresRefresh } = await this.sendQuery("PushPrefEnv", inPrefs);
+ if (callback) {
+ await callback();
+ }
+ if (requiresRefresh) {
+ await this._promiseEarlyRefresh();
+ }
+ }
+
+ async popPrefEnv(callback = null) {
+ let { popped, requiresRefresh } = await this.sendQuery("PopPrefEnv");
+ if (callback) {
+ await callback(popped);
+ }
+ if (requiresRefresh) {
+ await this._promiseEarlyRefresh();
+ }
+ }
+
+ async flushPrefEnv(callback = null) {
+ let { requiresRefresh } = await this.sendQuery("FlushPrefEnv");
+ if (callback) {
+ await callback();
+ }
+ if (requiresRefresh) {
+ await this._promiseEarlyRefresh();
+ }
+ }
+
+ /*
+ Collect a snapshot of all preferences in Firefox (i.e. about:prefs).
+ From this, store the results within specialpowers for later reference.
+ */
+ async getBaselinePrefs(callback = null) {
+ await this.sendQuery("getBaselinePrefs");
+ if (callback) {
+ await callback();
+ }
+ }
+
+ /*
+ This uses the stored prefs from getBaselinePrefs, collects a new snapshot
+ of preferences, then compares the new vs the baseline. If there are differences
+ they are recorded and returned as an array of preferences, in addition
+ all the changed preferences are reset to the value found in the baseline.
+
+ ignorePrefs: array of strings which are preferences. If they end in *,
+ we do a partial match
+ */
+ async comparePrefsToBaseline(ignorePrefs, callback = null) {
+ let retVal = await this.sendQuery("comparePrefsToBaseline", ignorePrefs);
+ if (callback) {
+ callback(retVal);
+ }
+ return retVal;
+ }
+
+ _promiseEarlyRefresh() {
+ return new Promise(r => {
+ // for mochitest-browser
+ if (typeof this.chromeWindow != "undefined") {
+ this.chromeWindow.requestAnimationFrame(r);
+ }
+ // for mochitest-plain
+ else {
+ this.contentWindow.requestAnimationFrame(r);
+ }
+ });
+ }
+
+ _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 = lazy.WrapPrivileged.wrapCallback(
+ Cu.unwaiveXrays(obs.observe),
+ this.contentWindow
+ );
+ }
+ 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 = lazy.WrapPrivileged.wrapCallback(
+ Cu.unwaiveXrays(obs.observe),
+ this.contentWindow
+ );
+ }
+ 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 = lazy.WrapPrivileged.unwrap(obj1);
+ obj2 = lazy.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
+ // <object> and <embed> 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);
+ }
+ getStringPref(...args) {
+ return Services.prefs.getStringPref(...args);
+ }
+
+ 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 });
+ }
+ getParentStringPref(prefName, defaultValue) {
+ return this._getParentPref(prefName, "STRING", { 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);
+ }
+ setStringPref(prefName, value) {
+ return this._setPref(prefName, "STRING", value);
+ }
+
+ // 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);
+ case "STRING":
+ return Services.prefs.getStringPref(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.browsingContext.topChromeWindow;
+ }
+ _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);
+ }
+ getFormFillController(window) {
+ return Cc["@mozilla.org/satchel/form-fill-controller;1"].getService(
+ Ci.nsIFormFillController
+ );
+ }
+ attachFormFillControllerTo(window) {
+ this.getFormFillController().attachPopupElementToDocument(
+ window.document,
+ this._getAutoCompletePopup(window)
+ );
+ }
+ detachFormFillControllerFrom(window) {
+ this.getFormFillController().detachFromDocument(window.document);
+ }
+ 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
+ );
+ }
+
+ async generateMediaControlKeyTestEvent(event) {
+ await this.sendQuery("SPGenerateMediaControlKeyTestEvent", { event });
+ }
+
+ // 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, this.contentWindow);
+ Services.console.registerListener(listener);
+ }
+ postConsoleSentinel() {
+ Services.console.logStringMessage("SENTINEL");
+ }
+ resetConsole() {
+ Services.console.reset();
+ }
+
+ getFullZoom(window) {
+ return BrowsingContext.getFromWindow(window).fullZoom;
+ }
+
+ getDeviceFullZoom(window) {
+ return this._getMUDV(window).deviceFullZoomForTest;
+ }
+ setFullZoom(window, zoom) {
+ BrowsingContext.getFromWindow(window).fullZoom = zoom;
+ }
+ getTextZoom(window) {
+ return BrowsingContext.getFromWindow(window).textZoom;
+ }
+ setTextZoom(window, zoom) {
+ BrowsingContext.getFromWindow(window).textZoom = zoom;
+ }
+
+ emulateMedium(window, mediaType) {
+ BrowsingContext.getFromWindow(window).top.mediumOverride = mediaType;
+ }
+
+ stopEmulatingMedium(window) {
+ BrowsingContext.getFromWindow(window).top.mediumOverride = "";
+ }
+
+ // Takes a snapshot of the given window and returns a <canvas>
+ // 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 (!Cu.isRemoteProxy(content) && 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 <canvas> element rather than an ImageData object, so we
+ // need to convert the result from the remote snapshot to a local
+ // canvas.
+ let promise = this.spawn(
+ content,
+ [rect, bgcolor, options],
+ getImageData
+ ).then(toCanvas);
+ if (Cu.isXrayWrapper(this.contentWindow)) {
+ return new this.contentWindow.Promise((resolve, reject) => {
+ promise.then(resolve, reject);
+ });
+ }
+ return promise;
+ }
+
+ 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.contentWindow.windowUtils.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) {
+ let keys = ChromeUtils.nondeterministicGetWeakMapKeys(m);
+ if (!keys) {
+ return undefined;
+ }
+ return this.contentWindow.Array.from(keys);
+ }
+
+ 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 Cu.cloneInto(res, this.contentWindow, { cloneFunctions: true });
+ }
+
+ 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);
+ }
+
+ getBrowsingContextID(target) {
+ return this._browsingContextForTarget(target).id;
+ }
+
+ *getGroupTopLevelWindows(target) {
+ let { group } = this._browsingContextForTarget(target);
+ for (let bc of group.getToplevels()) {
+ yield bc.window;
+ }
+ }
+
+ /**
+ * 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.sys.mjs. 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 <iframe> elements and XUL <browser> elements) or a
+ * WindowProxy (either in-process or remote).
+ * @param {Array<any>} args
+ * An array of arguments to pass to the task. All arguments
+ * must be structured clone compatible, and will be cloned
+ * before being passed to the task.
+ * @param {function} task
+ * The function to run in the context of the target. The
+ * function will be stringified and re-evaluated in the context
+ * of the target's content window. It may return any structured
+ * clone compatible value, or a Promise which resolves to the
+ * same, which will be returned to the caller.
+ *
+ * @returns {Promise<any>}
+ * A promise which resolves to the return value of the task, or
+ * which rejects if the task raises an exception. As this is
+ * being written, the rejection value will always be undefined
+ * in the cases where the task throws an error, though that may
+ * change in the future.
+ */
+ spawn(target, args, task) {
+ let browsingContext = this._browsingContextForTarget(target);
+
+ return this.sendQuery("Spawn", {
+ browsingContext,
+ args,
+ task: String(task),
+ caller: Cu.getFunctionSourceLocation(task),
+ hasHarness:
+ typeof this.SimpleTest === "object" ||
+ typeof this.xpcshellScope === "object",
+ imports: this._spawnTaskImports,
+ });
+ }
+
+ /**
+ * Like `spawn`, but spawns a chrome task in the parent process,
+ * instead. The task additionally has access to `windowGlobalParent`
+ * and `browsingContext` globals corresponding to the window from
+ * which the task was spawned.
+ */
+ spawnChrome(args, task) {
+ return this.sendQuery("SpawnChrome", {
+ args,
+ task: String(task),
+ caller: Cu.getFunctionSourceLocation(task),
+ imports: this._spawnTaskImports,
+ });
+ }
+
+ snapshotContext(target, rect, background, resetScrollPosition = false) {
+ let browsingContext = this._browsingContextForTarget(target);
+
+ return this.sendQuery("Snapshot", {
+ browsingContext,
+ rect,
+ background,
+ resetScrollPosition,
+ }).then(imageData => {
+ return this.contentWindow.createImageBitmap(imageData);
+ });
+ }
+
+ getSecurityState(target) {
+ let browsingContext = this._browsingContextForTarget(target);
+
+ return this.sendQuery("SecurityState", {
+ browsingContext,
+ });
+ }
+
+ _spawnTask(task, args, caller, taskId, imports) {
+ let sb = new lazy.SpecialPowersSandbox(
+ null,
+ data => {
+ this.sendAsyncMessage("ProxiedAssert", { taskId, data });
+ },
+ { imports }
+ );
+
+ sb.sandbox.SpecialPowers = this;
+ sb.sandbox.ContentTaskUtils = lazy.ContentTaskUtils;
+ for (let [global, prop] of Object.entries({
+ content: "contentWindow",
+ docShell: "docShell",
+ })) {
+ Object.defineProperty(sb.sandbox, global, {
+ get: () => {
+ return this[prop];
+ },
+ enumerable: true,
+ });
+ }
+
+ return sb.execute(task, args, caller);
+ }
+
+ /**
+ * Automatically imports the given symbol from the given sys.mjs for any
+ * task spawned by this SpecialPowers instance.
+ */
+ addTaskImport(symbol, url) {
+ this._spawnTaskImports[symbol] = url;
+ }
+
+ get SimpleTest() {
+ return this._SimpleTest || this.contentWindow.wrappedJSObject.SimpleTest;
+ }
+ set SimpleTest(val) {
+ this._SimpleTest = val;
+ }
+
+ get xpcshellScope() {
+ return this._xpcshellScope;
+ }
+ set xpcshellScope(val) {
+ this._xpcshellScope = val;
+ }
+
+ async evictAllContentViewers() {
+ if (Services.appinfo.sessionHistoryInParent) {
+ await this.sendQuery("EvictAllContentViewers");
+ } else {
+ this.browsingContext.top.childSessionHistory.legacySHistory.evictAllContentViewers();
+ }
+ }
+
+ /**
+ * 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);
+ return outParam.value;
+ }
+
+ get focusManager() {
+ return Services.focus;
+ }
+
+ activeWindow() {
+ return Services.focus.activeWindow;
+ }
+
+ focusedWindow() {
+ return Services.focus.focusedWindow;
+ }
+
+ clearFocus(aWindow) {
+ Services.focus.clearFocus(aWindow);
+ }
+
+ focus(aWindow) {
+ // This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests
+ // With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching
+ if (aWindow) {
+ aWindow.focus();
+ }
+
+ try {
+ let actor = aWindow
+ ? aWindow.windowGlobalChild.getActor("SpecialPowers")
+ : this;
+ actor.sendAsyncMessage("SpecialPowers.Focus", {});
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ ensureFocus(aBrowsingContext, aBlurSubframe) {
+ return this.sendQuery("EnsureFocus", {
+ browsingContext: aBrowsingContext,
+ blurSubframe: aBlurSubframe,
+ });
+ }
+
+ getClipboardData(flavor, whichClipboard) {
+ if (whichClipboard === undefined) {
+ whichClipboard = Services.clipboard.kGlobalClipboard;
+ }
+
+ var xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ xferable.init(this.docShell);
+ xferable.addDataFlavor(flavor);
+ Services.clipboard.getData(xferable, whichClipboard);
+ var data = {};
+ try {
+ xferable.getTransferData(flavor, data);
+ } catch (e) {}
+ data = data.value || null;
+ if (data == null) {
+ return "";
+ }
+
+ return data.QueryInterface(Ci.nsISupportsString).data;
+ }
+
+ clipboardCopyString(str) {
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(str);
+ }
+
+ supportsSelectionClipboard() {
+ return Services.clipboard.isClipboardTypeSupported(
+ Services.clipboard.kSelectionClipboard
+ );
+ }
+
+ swapFactoryRegistration(cid, contractID, newFactory) {
+ newFactory = Cu.waiveXrays(newFactory);
+
+ var componentRegistrar = Components.manager.QueryInterface(
+ Ci.nsIComponentRegistrar
+ );
+
+ var currentCID = componentRegistrar.contractIDToCID(contractID);
+ var currentFactory = Components.manager.getClassObject(
+ Cc[contractID],
+ Ci.nsIFactory
+ );
+ if (cid) {
+ componentRegistrar.unregisterFactory(currentCID, currentFactory);
+ } else {
+ cid = Services.uuid.generateUUID();
+ }
+
+ // Restore the original factory.
+ componentRegistrar.registerFactory(cid, "", contractID, newFactory);
+ return { originalCID: currentCID };
+ }
+
+ _getElement(aWindow, id) {
+ return typeof id == "string" ? aWindow.document.getElementById(id) : id;
+ }
+
+ dispatchEvent(aWindow, target, event) {
+ var el = this._getElement(aWindow, target);
+ return el.dispatchEvent(event);
+ }
+
+ get isDebugBuild() {
+ return Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2)
+ .isDebugBuild;
+ }
+ assertionCount() {
+ var debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
+ return debugsvc.assertionCount;
+ }
+
+ /**
+ * @param arg one of the following:
+ * - A URI string.
+ * - A document node.
+ * - A dictionary including a URL (`url`) and origin attributes (`attr`).
+ */
+ _getPrincipalFromArg(arg) {
+ arg = lazy.WrapPrivileged.unwrap(Cu.unwaiveXrays(arg));
+
+ if (arg.nodePrincipal) {
+ // It's a document.
+ return arg.nodePrincipal;
+ }
+
+ let secMan = Services.scriptSecurityManager;
+ if (typeof arg == "string") {
+ // It's a URL.
+ let uri = Services.io.newURI(arg);
+ return secMan.createContentPrincipal(uri, {});
+ }
+
+ let uri = Services.io.newURI(arg.url);
+ let attrs = arg.originAttributes || {};
+ return secMan.createContentPrincipal(uri, attrs);
+ }
+
+ async addPermission(type, allow, arg, expireType, expireTime) {
+ let principal = this._getPrincipalFromArg(arg);
+ if (principal.isSystemPrincipal) {
+ return; // nothing to do
+ }
+
+ let permission = allow;
+ if (typeof permission === "boolean") {
+ permission =
+ Ci.nsIPermissionManager[allow ? "ALLOW_ACTION" : "DENY_ACTION"];
+ }
+
+ var msg = {
+ op: "add",
+ type,
+ permission,
+ principal,
+ expireType: typeof expireType === "number" ? expireType : 0,
+ expireTime: typeof expireTime === "number" ? expireTime : 0,
+ };
+
+ await this.sendQuery("SPPermissionManager", msg);
+ }
+
+ /**
+ * @param type see nsIPermissionsManager::testPermissionFromPrincipal.
+ * @param arg one of the following:
+ * - A URI string.
+ * - A document node.
+ * - A dictionary including a URL (`url`) and origin attributes (`attr`).
+ */
+ async removePermission(type, arg) {
+ let principal = this._getPrincipalFromArg(arg);
+ if (principal.isSystemPrincipal) {
+ return; // nothing to do
+ }
+
+ var msg = {
+ op: "remove",
+ type,
+ principal,
+ };
+
+ await this.sendQuery("SPPermissionManager", msg);
+ }
+
+ async hasPermission(type, arg) {
+ let principal = this._getPrincipalFromArg(arg);
+ if (principal.isSystemPrincipal) {
+ return true; // system principals have all permissions
+ }
+
+ var msg = {
+ op: "has",
+ type,
+ principal,
+ };
+
+ return this.sendQuery("SPPermissionManager", msg);
+ }
+
+ async testPermission(type, value, arg) {
+ let principal = this._getPrincipalFromArg(arg);
+ if (principal.isSystemPrincipal) {
+ return true; // system principals have all permissions
+ }
+
+ var msg = {
+ op: "test",
+ type,
+ value,
+ principal,
+ };
+ return this.sendQuery("SPPermissionManager", msg);
+ }
+
+ isContentWindowPrivate(win) {
+ return lazy.PrivateBrowsingUtils.isContentWindowPrivate(win);
+ }
+
+ async notifyObserversInParentProcess(subject, topic, data) {
+ if (subject) {
+ throw new Error("Can't send subject to another process!");
+ }
+ if (this.isMainProcess()) {
+ this.notifyObservers(subject, topic, data);
+ return;
+ }
+ var msg = {
+ op: "notify",
+ observerTopic: topic,
+ observerData: data,
+ };
+ await this.sendQuery("SPObserverService", msg);
+ }
+
+ removeAllServiceWorkerData() {
+ return this.sendQuery("SPRemoveAllServiceWorkers", {});
+ }
+
+ removeServiceWorkerDataForExampleDomain() {
+ return this.sendQuery("SPRemoveServiceWorkerDataForExampleDomain", {});
+ }
+
+ cleanUpSTSData(origin, flags) {
+ return this.sendQuery("SPCleanUpSTSData", { origin });
+ }
+
+ async requestDumpCoverageCounters(cb) {
+ // We want to avoid a roundtrip between child and parent.
+ if (!lazy.PerTestCoverageUtils.enabled) {
+ return;
+ }
+
+ await this.sendQuery("SPRequestDumpCoverageCounters", {});
+ }
+
+ async requestResetCoverageCounters(cb) {
+ // We want to avoid a roundtrip between child and parent.
+ if (!lazy.PerTestCoverageUtils.enabled) {
+ return;
+ }
+ await this.sendQuery("SPRequestResetCoverageCounters", {});
+ }
+
+ loadExtension(ext, handler) {
+ if (this._extensionListeners == null) {
+ this._extensionListeners = new Set();
+
+ this._addMessageListener("SPExtensionMessage", msg => {
+ for (let listener of this._extensionListeners) {
+ try {
+ listener(msg);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ });
+ }
+
+ // Note, this is not the addon is as used by the AddonManager etc,
+ // this is just an identifier used for specialpowers messaging
+ // between this content process and the chrome process.
+ let id = this._nextExtensionID++;
+
+ handler = Cu.waiveXrays(handler);
+ ext = Cu.waiveXrays(ext);
+
+ let sp = this;
+ let state = "uninitialized";
+ let extension = {
+ get state() {
+ return state;
+ },
+
+ startup() {
+ state = "pending";
+ return sp.sendQuery("SPStartupExtension", { id }).then(
+ () => {
+ state = "running";
+ },
+ () => {
+ state = "failed";
+ sp._extensionListeners.delete(listener);
+ return Promise.reject("startup failed");
+ }
+ );
+ },
+
+ unload() {
+ state = "unloading";
+ return sp.sendQuery("SPUnloadExtension", { id }).finally(() => {
+ sp._extensionListeners.delete(listener);
+ state = "unloaded";
+ });
+ },
+
+ sendMessage(...args) {
+ sp.sendAsyncMessage("SPExtensionMessage", { id, args });
+ },
+
+ grantActiveTab(tabId) {
+ sp.sendAsyncMessage("SPExtensionGrantActiveTab", { id, tabId });
+ },
+
+ terminateBackground(...args) {
+ return sp.sendQuery("SPExtensionTerminateBackground", { id, args });
+ },
+
+ wakeupBackground() {
+ return sp.sendQuery("SPExtensionWakeupBackground", { id });
+ },
+ };
+
+ this.sendAsyncMessage("SPLoadExtension", { ext, id });
+
+ let listener = msg => {
+ if (msg.data.id == id) {
+ if (msg.data.type == "extensionSetId") {
+ extension.id = msg.data.args[0];
+ extension.uuid = msg.data.args[1];
+ } else if (msg.data.type in handler) {
+ handler[msg.data.type](
+ ...Cu.cloneInto(msg.data.args, this.contentWindow)
+ );
+ } else {
+ dump(`Unexpected: ${msg.data.type}\n`);
+ }
+ }
+ };
+
+ this._extensionListeners.add(listener);
+ return extension;
+ }
+
+ invalidateExtensionStorageCache() {
+ this.notifyObserversInParentProcess(
+ null,
+ "extension-invalidate-storage-cache",
+ ""
+ );
+ }
+
+ allowMedia(window, enable) {
+ window.docShell.allowMedia = enable;
+ }
+
+ createChromeCache(name, url) {
+ let principal = this._getPrincipalFromArg(url);
+ return new this.contentWindow.CacheStorage(name, principal);
+ }
+
+ loadChannelAndReturnStatus(url, loadUsingSystemPrincipal) {
+ const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+ );
+
+ return new Promise(function (resolve) {
+ let listener = {
+ httpStatus: 0,
+
+ onStartRequest(request) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+ this.httpStatus = request.responseStatus;
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ new BinaryInputStream(stream).readByteArray(count);
+ },
+
+ onStopRequest(request, status) {
+ /* testing here that the redirect was not followed. If it was followed
+ we would see a http status of 200 and status of NS_OK */
+
+ let httpStatus = this.httpStatus;
+ resolve({ status, httpStatus });
+ },
+ };
+ let uri = lazy.NetUtil.newURI(url);
+ let channel = lazy.NetUtil.newChannel({ uri, loadUsingSystemPrincipal });
+
+ channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
+ channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ channel.documentURI = uri;
+ channel.asyncOpen(listener);
+ });
+ }
+
+ get ParserUtils() {
+ if (this._pu != null) {
+ return this._pu;
+ }
+
+ let pu = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
+ // We need to create and return our own wrapper.
+ this._pu = {
+ sanitize(src, flags) {
+ return pu.sanitize(src, flags);
+ },
+ convertToPlainText(src, flags, wrapCol) {
+ return pu.convertToPlainText(src, flags, wrapCol);
+ },
+ parseFragment(fragment, flags, isXML, baseURL, element) {
+ let baseURI = baseURL ? lazy.NetUtil.newURI(baseURL) : null;
+ return pu.parseFragment(
+ lazy.WrapPrivileged.unwrap(fragment),
+ flags,
+ isXML,
+ baseURI,
+ lazy.WrapPrivileged.unwrap(element)
+ );
+ },
+ };
+ return this._pu;
+ }
+
+ createDOMWalker(node, showAnonymousContent) {
+ node = lazy.WrapPrivileged.unwrap(node);
+ let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance(
+ Ci.inIDeepTreeWalker
+ );
+ walker.showAnonymousContent = showAnonymousContent;
+ walker.init(node.ownerDocument, NodeFilter.SHOW_ALL);
+ walker.currentNode = node;
+ let contentWindow = this.contentWindow;
+ return {
+ get firstChild() {
+ return lazy.WrapPrivileged.wrap(walker.firstChild(), contentWindow);
+ },
+ get lastChild() {
+ return lazy.WrapPrivileged.wrap(walker.lastChild(), contentWindow);
+ },
+ };
+ }
+
+ /**
+ * Which commands are available can be determined by checking which commands
+ * are registered. See \ref
+ * nsIControllerCommandTable.registerCommand(in String, in nsIControllerCommand).
+ */
+ doCommand(window, cmd, param) {
+ switch (cmd) {
+ case "cmd_align":
+ case "cmd_backgroundColor":
+ case "cmd_fontColor":
+ case "cmd_fontFace":
+ case "cmd_fontSize":
+ case "cmd_highlight":
+ case "cmd_insertImageNoUI":
+ case "cmd_insertLinkNoUI":
+ case "cmd_paragraphState": {
+ const params = Cu.createCommandParams();
+ params.setStringValue("state_attribute", param);
+ return window.docShell.doCommandWithParams(cmd, params);
+ }
+ case "cmd_pasteTransferable": {
+ const params = Cu.createCommandParams();
+ params.setISupportsValue("transferable", param);
+ return window.docShell.doCommandWithParams(cmd, params);
+ }
+ default:
+ return window.docShell.doCommand(cmd);
+ }
+ }
+
+ isCommandEnabled(window, cmd) {
+ return window.docShell.isCommandEnabled(cmd);
+ }
+
+ /**
+ * See \ref nsIContentViewerEdit.setCommandNode(in Node).
+ */
+ setCommandNode(window, node) {
+ return window.docShell.contentViewer
+ .QueryInterface(Ci.nsIContentViewerEdit)
+ .setCommandNode(node);
+ }
+
+ /* Bug 1339006 Runnables of nsIURIClassifier.classify may be labeled by
+ * SystemGroup, but some test cases may run as web content. That would assert
+ * when trying to enter web content from a runnable labeled by the
+ * SystemGroup. To avoid that, we run classify from SpecialPowers which is
+ * chrome-privileged and allowed to run inside SystemGroup
+ */
+
+ doUrlClassify(principal, callback) {
+ let classifierService = Cc[
+ "@mozilla.org/url-classifier/dbservice;1"
+ ].getService(Ci.nsIURIClassifier);
+
+ let wrapCallback = (...args) => {
+ Services.tm.dispatchToMainThread(() => {
+ if (typeof callback == "function") {
+ callback(...args);
+ } else {
+ callback.onClassifyComplete.call(undefined, ...args);
+ }
+ });
+ };
+
+ return classifierService.classify(
+ lazy.WrapPrivileged.unwrap(principal),
+ wrapCallback
+ );
+ }
+
+ // TODO: Bug 1353701 - Supports custom event target for labelling.
+ doUrlClassifyLocal(uri, tables, callback) {
+ let classifierService = Cc[
+ "@mozilla.org/url-classifier/dbservice;1"
+ ].getService(Ci.nsIURIClassifier);
+
+ let wrapCallback = results => {
+ Services.tm.dispatchToMainThread(() => {
+ if (typeof callback == "function") {
+ callback(lazy.WrapPrivileged.wrap(results, this.contentWindow));
+ } else {
+ callback.onClassifyComplete.call(
+ undefined,
+ lazy.WrapPrivileged.wrap(results, this.contentWindow)
+ );
+ }
+ });
+ };
+
+ let feature = classifierService.createFeatureWithTables(
+ "test",
+ tables.split(","),
+ []
+ );
+ return classifierService.asyncClassifyLocalWithFeatures(
+ lazy.WrapPrivileged.unwrap(uri),
+ [feature],
+ Ci.nsIUrlClassifierFeature.blocklist,
+ wrapCallback
+ );
+ }
+
+ /* Content processes asynchronously receive child-to-parent transformations
+ * when they are launched. Until they are received, screen coordinates
+ * reported to JS are wrong. This is generally ok. It behaves as if the
+ * user repositioned the window. But if we want to test screen coordinates,
+ * we need to wait for the updated data.
+ */
+ contentTransformsReceived(win) {
+ while (win) {
+ try {
+ return win.docShell.browserChild.contentTransformsReceived();
+ } catch (ex) {
+ // browserChild getter throws on non-e10s rather than returning null.
+ }
+ if (win == win.parent) {
+ break;
+ }
+ win = win.parent;
+ }
+ return Promise.resolve();
+ }
+}
+
+SpecialPowersChild.prototype._proxiedObservers = {
+ "specialpowers-http-notify-request": function (aMessage) {
+ let uri = aMessage.json.uri;
+ Services.obs.notifyObservers(
+ null,
+ "specialpowers-http-notify-request",
+ uri
+ );
+ },
+
+ "specialpowers-service-worker-shutdown": function (aMessage) {
+ Services.obs.notifyObservers(null, "specialpowers-service-worker-shutdown");
+ },
+
+ "specialpowers-csp-on-violate-policy": function (aMessage) {
+ let subject = null;
+
+ try {
+ subject = Services.io.newURI(aMessage.data.subject);
+ } catch (ex) {
+ // if it's not a valid URI it must be an nsISupportsCString
+ subject = Cc["@mozilla.org/supports-cstring;1"].createInstance(
+ Ci.nsISupportsCString
+ );
+ subject.data = aMessage.data.subject;
+ }
+ Services.obs.notifyObservers(
+ subject,
+ "specialpowers-csp-on-violate-policy",
+ aMessage.data.data
+ );
+ },
+
+ "specialpowers-xfo-on-violate-policy": function (aMessage) {
+ let subject = Services.io.newURI(aMessage.data.subject);
+ Services.obs.notifyObservers(
+ subject,
+ "specialpowers-xfo-on-violate-policy",
+ aMessage.data.data
+ );
+ },
+};
+
+SpecialPowersChild.prototype.EARLY_BETA_OR_EARLIER =
+ AppConstants.EARLY_BETA_OR_EARLIER;
diff --git a/testing/specialpowers/content/SpecialPowersEventUtils.sys.mjs b/testing/specialpowers/content/SpecialPowersEventUtils.sys.mjs
new file mode 100644
index 0000000000..68cec2b6e3
--- /dev/null
+++ b/testing/specialpowers/content/SpecialPowersEventUtils.sys.mjs
@@ -0,0 +1,35 @@
+/* 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/. */
+
+/**
+ * Loads a stub copy of EventUtils.js which can be used by things like
+ * content tasks without holding any direct references to windows.
+ */
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+export let EventUtils = { setTimeout, window: {}, _EU_Ci: Ci, _EU_Cc: Cc };
+
+EventUtils.parent = EventUtils.window;
+
+EventUtils.synthesizeClick = element =>
+ new Promise(resolve => {
+ element.addEventListener("click", () => resolve(), { once: true });
+
+ EventUtils.synthesizeMouseAtCenter(
+ element,
+ { type: "mousedown", isSynthesized: false },
+ element.ownerGlobal
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ element,
+ { type: "mouseup", isSynthesized: false },
+ element.ownerGlobal
+ );
+ });
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+);
diff --git a/testing/specialpowers/content/SpecialPowersParent.sys.mjs b/testing/specialpowers/content/SpecialPowersParent.sys.mjs
new file mode 100644
index 0000000000..2da4760c68
--- /dev/null
+++ b/testing/specialpowers/content/SpecialPowersParent.sys.mjs
@@ -0,0 +1,1481 @@
+/* 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/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ExtensionData: "resource://gre/modules/Extension.sys.mjs",
+ ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs",
+ HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs",
+ PerTestCoverageUtils:
+ "resource://testing-common/PerTestCoverageUtils.sys.mjs",
+ ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs",
+ SpecialPowersSandbox:
+ "resource://testing-common/SpecialPowersSandbox.sys.mjs",
+});
+
+class SpecialPowersError extends Error {
+ get name() {
+ return "SpecialPowersError";
+ }
+}
+
+const PREF_TYPES = {
+ [Ci.nsIPrefBranch.PREF_INVALID]: "INVALID",
+ [Ci.nsIPrefBranch.PREF_INT]: "INT",
+ [Ci.nsIPrefBranch.PREF_BOOL]: "BOOL",
+ [Ci.nsIPrefBranch.PREF_STRING]: "STRING",
+ number: "INT",
+ boolean: "BOOL",
+ string: "STRING",
+};
+
+// We share a single preference environment stack between all
+// SpecialPowers instances, across all processes.
+let prefUndoStack = [];
+let inPrefEnvOp = false;
+
+let permissionUndoStack = [];
+
+function doPrefEnvOp(fn) {
+ if (inPrefEnvOp) {
+ throw new Error(
+ "Reentrant preference environment operations not supported"
+ );
+ }
+ inPrefEnvOp = true;
+ try {
+ return fn();
+ } finally {
+ inPrefEnvOp = false;
+ }
+}
+
+async function createWindowlessBrowser({ isPrivate = false } = {}) {
+ const { promiseDocumentLoaded, promiseEvent, promiseObserved } =
+ ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionUtils.sys.mjs"
+ ).ExtensionUtils;
+
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
+
+ if (isPrivate) {
+ let loadContext = windowlessBrowser.docShell.QueryInterface(
+ Ci.nsILoadContext
+ );
+ loadContext.usePrivateBrowsing = true;
+ }
+
+ let chromeShell = windowlessBrowser.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ );
+
+ const system = Services.scriptSecurityManager.getSystemPrincipal();
+ chromeShell.createAboutBlankContentViewer(system, system);
+ windowlessBrowser.browsingContext.useGlobalHistory = false;
+ chromeShell.loadURI(
+ Services.io.newURI("chrome://extensions/content/dummy.xhtml"),
+ {
+ triggeringPrincipal: system,
+ }
+ );
+
+ await promiseObserved(
+ "chrome-document-global-created",
+ win => win.document == chromeShell.document
+ );
+
+ let chromeDoc = await promiseDocumentLoaded(chromeShell.document);
+
+ let browser = chromeDoc.createXULElement("browser");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("disableglobalhistory", "true");
+ browser.setAttribute("remote", "true");
+
+ let promise = promiseEvent(browser, "XULFrameLoaderCreated");
+ chromeDoc.documentElement.appendChild(browser);
+
+ await promise;
+
+ return { windowlessBrowser, browser };
+}
+
+// Supplies the unique IDs for tasks created by SpecialPowers.spawn(),
+// 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;
+
+export 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 "csp-on-violate-policy":
+ // the subject is either an nsIURI or an nsISupportsCString
+ let subject = null;
+ if (aSubject instanceof Ci.nsIURI) {
+ subject = aSubject.asciiSpec;
+ } else if (aSubject instanceof Ci.nsISupportsCString) {
+ subject = aSubject.data;
+ } else {
+ throw new Error("Subject must be nsIURI or nsISupportsCString");
+ }
+ msg = {
+ subject,
+ data: aData,
+ };
+ this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
+ return;
+ case "xfo-on-violate-policy":
+ let uriSpec = null;
+ if (aSubject instanceof Ci.nsIURI) {
+ uriSpec = aSubject.asciiSpec;
+ } else {
+ throw new Error("Subject must be nsIURI");
+ }
+ msg = {
+ subject: uriSpec,
+ data: aData,
+ };
+ this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
+ return;
+ default:
+ this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
+ }
+ },
+ };
+
+ this._basePrefs = null;
+ this.init();
+
+ this._crashDumpDir = null;
+ this._processCrashObserversRegistered = false;
+ this._chromeScriptListeners = [];
+ this._extensions = new Map();
+ this._taskActors = new Map();
+ }
+
+ static registerActor() {
+ ChromeUtils.registerWindowActor("SpecialPowers", {
+ allFrames: true,
+ includeChrome: true,
+ child: {
+ esModuleURI: "resource://testing-common/SpecialPowersChild.sys.mjs",
+ observers: [
+ "chrome-document-global-created",
+ "content-document-global-created",
+ ],
+ },
+ parent: {
+ esModuleURI: "resource://testing-common/SpecialPowersParent.sys.mjs",
+ },
+ });
+ }
+
+ static unregisterActor() {
+ ChromeUtils.unregisterWindowActor("SpecialPowers");
+ }
+
+ 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);
+
+ this.getBaselinePrefs();
+ }
+
+ 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 => {
+ 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);
+ } catch (ex) {
+ id = null;
+ }
+ if (id) {
+ message.dumpIDs.push({ id, extension: "dmp" });
+ message.dumpIDs.push({ id, extension: "extra" });
+ }
+ }
+
+ 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 "ipc:content-shutdown":
+ aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2);
+ if (!aSubject.hasKey("abnormal")) {
+ return; // This is a normal shutdown, ignore it
+ }
+
+ var message = { type: "crash-observed", dumpIDs: [] };
+ addDumpIDToMessage("dumpID");
+ this.sendAsyncMessage("SPProcessCrashService", message);
+ break;
+ }
+ }
+
+ _getCrashDumpDir() {
+ if (!this._crashDumpDir) {
+ this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ this._crashDumpDir.append("minidumps");
+ }
+ return this._crashDumpDir;
+ }
+
+ _getPendingCrashDumpDir() {
+ if (!this._pendingCrashDumpDir) {
+ this._pendingCrashDumpDir = Services.dirsvc.get("UAppData", Ci.nsIFile);
+ this._pendingCrashDumpDir.append("Crash Reports");
+ this._pendingCrashDumpDir.append("pending");
+ }
+ return this._pendingCrashDumpDir;
+ }
+
+ _deleteCrashDumpFiles(aFilenames) {
+ var crashDumpDir = this._getCrashDumpDir();
+ if (!crashDumpDir.exists()) {
+ return false;
+ }
+
+ var success = !!aFilenames.length;
+ aFilenames.forEach(function (crashFilename) {
+ var file = crashDumpDir.clone();
+ file.append(crashFilename);
+ if (file.exists()) {
+ file.remove(false);
+ } else {
+ success = false;
+ }
+ });
+ return success;
+ }
+
+ _findCrashDumpFiles(aToIgnore) {
+ var crashDumpDir = this._getCrashDumpDir();
+ var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries;
+ if (!entries) {
+ return [];
+ }
+
+ var crashDumpFiles = [];
+ while (entries.hasMoreElements()) {
+ var file = entries.nextFile;
+ var path = String(file.path);
+ if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) {
+ crashDumpFiles.push(path);
+ }
+ }
+ return crashDumpFiles.concat();
+ }
+
+ _deletePendingCrashDumpFiles() {
+ var crashDumpDir = this._getPendingCrashDumpDir();
+ var removed = false;
+ if (crashDumpDir.exists()) {
+ let entries = crashDumpDir.directoryEntries;
+ while (entries.hasMoreElements()) {
+ let file = entries.nextFile;
+ if (file.isFile()) {
+ file.remove(false);
+ removed = true;
+ }
+ }
+ }
+ return removed;
+ }
+
+ _addProcessCrashObservers() {
+ if (this._processCrashObserversRegistered) {
+ return;
+ }
+
+ Services.obs.addObserver(this._observer, "ipc:content-shutdown");
+ this._processCrashObserversRegistered = true;
+ }
+
+ _removeProcessCrashObservers() {
+ if (!this._processCrashObserversRegistered) {
+ return;
+ }
+
+ Services.obs.removeObserver(this._observer, "ipc:content-shutdown");
+ this._processCrashObserversRegistered = false;
+ }
+
+ onRegister() {
+ this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true });
+ }
+
+ _getURI(url) {
+ return Services.io.newURI(url);
+ }
+ _notifyCategoryAndObservers(subject, topic, data) {
+ const serviceMarker = "service,";
+
+ // First create observers from the category manager.
+
+ let observers = [];
+
+ for (let { value: contractID } of Services.catMan.enumerateCategory(
+ topic
+ )) {
+ let factoryFunction;
+ if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
+ contractID = contractID.substring(serviceMarker.length);
+ factoryFunction = "getService";
+ } else {
+ factoryFunction = "createInstance";
+ }
+
+ try {
+ let handler = Cc[contractID][factoryFunction]();
+ if (handler) {
+ let observer = handler.QueryInterface(Ci.nsIObserver);
+ observers.push(observer);
+ }
+ } catch (e) {}
+ }
+
+ // Next enumerate the registered observers.
+ for (let observer of Services.obs.enumerateObservers(topic)) {
+ if (observer instanceof Ci.nsIObserver && !observers.includes(observer)) {
+ observers.push(observer);
+ }
+ }
+
+ observers.forEach(function (observer) {
+ try {
+ observer.observe(subject, topic, data);
+ } catch (e) {}
+ });
+ }
+
+ /*
+ Iterate through one atomic set of pref actions and perform sets/clears as appropriate.
+ All actions performed must modify the relevant pref.
+
+ Returns whether we need to wait for a refresh driver tick for the pref to
+ have effect. This is only needed for ui. and font. prefs, which affect the
+ look and feel code and have some change-coalescing going on.
+ */
+ _applyPrefs(actions) {
+ let requiresRefresh = false;
+ for (let pref of actions) {
+ // This logic should match PrefRequiresRefresh in reftest.jsm
+ requiresRefresh =
+ requiresRefresh ||
+ pref.name == "layout.css.prefers-color-scheme.content-override" ||
+ pref.name.startsWith("ui.") ||
+ pref.name.startsWith("browser.display.") ||
+ pref.name.startsWith("font.");
+ if (pref.action == "set") {
+ this._setPref(pref.name, pref.type, pref.value, pref.iid);
+ } else if (pref.action == "clear") {
+ Services.prefs.clearUserPref(pref.name);
+ }
+ }
+ return requiresRefresh;
+ }
+
+ /**
+ * Take in a list of pref changes to make, pushes their current values
+ * onto the restore stack, and makes the changes. When the test
+ * finishes, these changes are reverted.
+ *
+ * |inPrefs| must be an object with up to two properties: "set" and "clear".
+ * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset
+ * the prefs indicated in |inPrefs.clear|.
+ *
+ * For example, you might pass |inPrefs| as:
+ *
+ * inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']],
+ * 'clear': [['clear.this'], ['also.this']] };
+ *
+ * Notice that |set| and |clear| are both an array of arrays. In |set|, each
+ * of the inner arrays must have the form [pref_name, value] or [pref_name,
+ * value, iid]. (The latter form is used for prefs with "complex" values.)
+ *
+ * In |clear|, each inner array should have the form [pref_name].
+ *
+ * If you set the same pref more than once (or both set and clear a pref),
+ * the behavior of this method is undefined.
+ */
+ pushPrefEnv(inPrefs) {
+ return doPrefEnvOp(() => {
+ let pendingActions = [];
+ let cleanupActions = [];
+
+ for (let [action, prefs] of Object.entries(inPrefs)) {
+ for (let pref of prefs) {
+ let name = pref[0];
+ let value = null;
+ let iid = null;
+ let type = PREF_TYPES[Services.prefs.getPrefType(name)];
+ let originalValue = null;
+
+ if (pref.length == 3) {
+ value = pref[1];
+ iid = pref[2];
+ } else if (pref.length == 2) {
+ value = pref[1];
+ }
+
+ /* If pref is not found or invalid it doesn't exist. */
+ if (type !== "INVALID") {
+ if (
+ (Services.prefs.prefHasUserValue(name) && action == "clear") ||
+ action == "set"
+ ) {
+ originalValue = this._getPref(name, type);
+ }
+ } else if (action == "set") {
+ /* name doesn't exist, so 'clear' is pointless */
+ if (iid) {
+ type = "COMPLEX";
+ }
+ }
+
+ if (type === "INVALID") {
+ type = PREF_TYPES[typeof value];
+ }
+ if (type === "INVALID") {
+ throw new Error("Unexpected preference type for " + name);
+ }
+
+ pendingActions.push({ action, type, name, value, iid });
+
+ /* Push original preference value or clear into cleanup array */
+ var cleanupTodo = { type, name, value: originalValue, iid };
+ if (originalValue == null) {
+ cleanupTodo.action = "clear";
+ } else {
+ cleanupTodo.action = "set";
+ }
+ cleanupActions.push(cleanupTodo);
+ }
+ }
+
+ prefUndoStack.push(cleanupActions);
+ let requiresRefresh = this._applyPrefs(pendingActions);
+ return { requiresRefresh };
+ });
+ }
+
+ async popPrefEnv() {
+ return doPrefEnvOp(() => {
+ let env = prefUndoStack.pop();
+ if (env) {
+ let requiresRefresh = this._applyPrefs(env);
+ return { popped: true, requiresRefresh };
+ }
+ return { popped: false, requiresRefresh: false };
+ });
+ }
+
+ flushPrefEnv() {
+ let requiresRefresh = false;
+ while (prefUndoStack.length) {
+ requiresRefresh |= this.popPrefEnv().requiresRefresh;
+ }
+ return { requiresRefresh };
+ }
+
+ _setPref(name, type, value, iid) {
+ switch (type) {
+ case "BOOL":
+ return Services.prefs.setBoolPref(name, value);
+ case "INT":
+ return Services.prefs.setIntPref(name, value);
+ case "CHAR":
+ return Services.prefs.setCharPref(name, value);
+ case "COMPLEX":
+ return Services.prefs.setComplexValue(name, iid, value);
+ case "STRING":
+ return Services.prefs.setStringPref(name, value);
+ }
+ switch (typeof value) {
+ case "boolean":
+ return Services.prefs.setBoolPref(name, value);
+ case "number":
+ return Services.prefs.setIntPref(name, value);
+ case "string":
+ return Services.prefs.setStringPref(name, value);
+ }
+ throw new Error(
+ `Unexpected preference type: ${type} for ${name} with value ${value} and type ${typeof value}`
+ );
+ }
+
+ _getPref(name, type, defaultValue, iid) {
+ switch (type) {
+ case "BOOL":
+ if (defaultValue !== undefined) {
+ return Services.prefs.getBoolPref(name, defaultValue);
+ }
+ return Services.prefs.getBoolPref(name);
+ case "INT":
+ if (defaultValue !== undefined) {
+ return Services.prefs.getIntPref(name, defaultValue);
+ }
+ return Services.prefs.getIntPref(name);
+ case "CHAR":
+ if (defaultValue !== undefined) {
+ return Services.prefs.getCharPref(name, defaultValue);
+ }
+ return Services.prefs.getCharPref(name);
+ case "COMPLEX":
+ return Services.prefs.getComplexValue(name, iid);
+ case "STRING":
+ if (defaultValue !== undefined) {
+ return Services.prefs.getStringPref(name, defaultValue);
+ }
+ return Services.prefs.getStringPref(name);
+ }
+ throw new Error(
+ `Unexpected preference type: ${type} for preference ${name}`
+ );
+ }
+
+ getBaselinePrefs() {
+ this._basePrefs = this._getAllPreferences();
+ }
+
+ _comparePrefs(base, target, ignorePrefs, partialMatches) {
+ let failures = [];
+ for (const [key, value] of base) {
+ if (ignorePrefs.includes(key)) {
+ continue;
+ }
+ let partialFind = false;
+ partialMatches.forEach(pm => {
+ if (key.startsWith(pm)) {
+ partialFind = true;
+ }
+ });
+ if (partialFind) {
+ continue;
+ }
+
+ if (value === target.get(key)) {
+ continue;
+ }
+ if (!failures.includes(key)) {
+ failures.push(key);
+ }
+ }
+ return failures;
+ }
+
+ comparePrefsToBaseline(ignorePrefs) {
+ let newPrefs = this._getAllPreferences();
+
+ // find all items in ignorePrefs that end in *, add to partialMatch
+ let partialMatch = [];
+ if (ignorePrefs === undefined) {
+ ignorePrefs = [];
+ }
+ ignorePrefs.forEach(pref => {
+ if (pref.endsWith("*")) {
+ partialMatch.push(pref.split("*")[0]);
+ }
+ });
+
+ // find all new prefs different than old
+ let rv1 = this._comparePrefs(
+ newPrefs,
+ this._basePrefs,
+ ignorePrefs,
+ partialMatch
+ );
+
+ // find all old prefs different than new (in case we delete)
+ let rv2 = this._comparePrefs(
+ this._basePrefs,
+ newPrefs,
+ ignorePrefs,
+ partialMatch
+ );
+
+ let failures = [...new Set([...rv1, ...rv2])];
+
+ // reset failures
+ failures.forEach(f => {
+ if (this._basePrefs.get(f)) {
+ this._setPref(
+ f,
+ PREF_TYPES[Services.prefs.getPrefType(f)],
+ this._basePrefs.get(f)
+ );
+ } else {
+ Services.prefs.clearUserPref(f);
+ }
+ });
+
+ if (ignorePrefs.length > 1) {
+ return failures;
+ }
+ return [];
+ }
+
+ _getAllPreferences() {
+ let names = new Map();
+ for (let prefName of Services.prefs.getChildList("")) {
+ let prefType = PREF_TYPES[Services.prefs.getPrefType(prefName)];
+ let prefValue = this._getPref(prefName, prefType);
+ names.set(prefName, prefValue);
+ }
+ return names;
+ }
+
+ _toggleMuteAudio(aMuted) {
+ let browser = this.browsingContext.top.embedderElement;
+ if (aMuted) {
+ browser.mute();
+ } else {
+ browser.unmute();
+ }
+ }
+
+ _permOp(perm) {
+ switch (perm.op) {
+ case "add":
+ Services.perms.addFromPrincipal(
+ perm.principal,
+ perm.type,
+ perm.permission,
+ perm.expireType,
+ perm.expireTime
+ );
+ break;
+ case "remove":
+ Services.perms.removeFromPrincipal(perm.principal, perm.type);
+ break;
+ default:
+ throw new Error(`Unexpected permission op: ${perm.op}`);
+ }
+ }
+
+ pushPermissions(inPermissions) {
+ let pendingPermissions = [];
+ let cleanupPermissions = [];
+
+ for (let permission of inPermissions) {
+ let { principal } = permission;
+ if (principal.isSystemPrincipal) {
+ continue;
+ }
+
+ let originalValue = Services.perms.testPermissionFromPrincipal(
+ principal,
+ permission.type
+ );
+
+ let perm = permission.allow;
+ if (typeof perm === "boolean") {
+ perm = Ci.nsIPermissionManager[perm ? "ALLOW_ACTION" : "DENY_ACTION"];
+ }
+
+ if (permission.remove) {
+ perm = Ci.nsIPermissionManager.UNKNOWN_ACTION;
+ }
+
+ if (originalValue == perm) {
+ continue;
+ }
+
+ let 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);
+
+ if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
+ cleanupTodo.op = "remove";
+ } else {
+ cleanupTodo.value = originalValue;
+ cleanupTodo.permission = originalValue;
+ }
+ cleanupPermissions.push(cleanupTodo);
+ }
+
+ permissionUndoStack.push(cleanupPermissions);
+
+ for (let perm of pendingPermissions) {
+ this._permOp(perm);
+ }
+ }
+
+ popPermissions() {
+ if (permissionUndoStack.length) {
+ for (let perm of permissionUndoStack.pop()) {
+ this._permOp(perm);
+ }
+ }
+ }
+
+ flushPermissions() {
+ while (permissionUndoStack.length) {
+ this.popPermissions();
+ }
+ }
+
+ _spawnChrome(task, args, caller, imports) {
+ let sb = new lazy.SpecialPowersSandbox(
+ null,
+ data => {
+ this.sendAsyncMessage("Assert", data);
+ },
+ { imports }
+ );
+
+ for (let [global, prop] of Object.entries({
+ windowGlobalParent: "manager",
+ browsingContext: "browsingContext",
+ })) {
+ Object.defineProperty(sb.sandbox, global, {
+ get: () => {
+ return this[prop];
+ },
+ enumerable: true,
+ });
+ }
+
+ return sb.execute(task, args, caller);
+ }
+
+ /**
+ * messageManager callback function
+ * This will get requests from our API in the window and process them in chrome for it
+ **/
+ // eslint-disable-next-line complexity
+ async receiveMessage(aMessage) {
+ let startTime = Cu.now();
+ // Try block so we can use a finally statement to add a profiler marker
+ // despite all the return statements.
+ try {
+ // We explicitly return values in the below code so that this function
+ // doesn't trigger a flurry of warnings about "does not always return
+ // a value".
+ switch (aMessage.name) {
+ case "SPToggleMuteAudio":
+ return this._toggleMuteAudio(aMessage.data.mute);
+
+ case "Ping":
+ return undefined;
+
+ case "SpecialPowers.Quit":
+ if (
+ !AppConstants.RELEASE_OR_BETA &&
+ !AppConstants.DEBUG &&
+ !AppConstants.MOZ_CODE_COVERAGE &&
+ !AppConstants.ASAN &&
+ !AppConstants.TSAN
+ ) {
+ if (Services.profiler.IsActive()) {
+ let filename = Services.env.get("MOZ_PROFILER_SHUTDOWN");
+ if (filename) {
+ await Services.profiler.dumpProfileToFileAsync(filename);
+ await Services.profiler.StopProfiler();
+ }
+ }
+ Cu.exitIfInAutomation();
+ } else {
+ Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
+ }
+ return undefined;
+
+ case "EnsureFocus":
+ let bc = aMessage.data.browsingContext;
+ // Send a message to the child telling it to focus the window.
+ // If the message responds with a browsing context, then
+ // a child browsing context in a subframe should be focused.
+ // Iterate until nothing is returned and we get to the most
+ // deeply nested subframe that should be focused.
+ do {
+ let spParent = bc.currentWindowGlobal.getActor("SpecialPowers");
+ if (spParent) {
+ bc = await spParent.sendQuery("EnsureFocus", {
+ blurSubframe: aMessage.data.blurSubframe,
+ });
+ }
+ } while (bc && !aMessage.data.blurSubframe);
+ return undefined;
+
+ case "SpecialPowers.Focus":
+ if (this.manager.rootFrameLoader) {
+ 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 => {
+ console.error(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 "EvictAllContentViewers":
+ this.browsingContext.top.sessionHistory.evictAllContentViewers();
+ return undefined;
+
+ case "getBaselinePrefs":
+ return this.getBaselinePrefs();
+
+ case "comparePrefsToBaseline":
+ return this.comparePrefsToBaseline(aMessage.data);
+
+ case "PushPrefEnv":
+ return this.pushPrefEnv(aMessage.data);
+
+ case "PopPrefEnv":
+ return this.popPrefEnv();
+
+ case "FlushPrefEnv":
+ return this.flushPrefEnv();
+
+ case "PushPermissions":
+ return this.pushPermissions(aMessage.data);
+
+ case "PopPermissions":
+ return this.popPermissions();
+
+ case "FlushPermissions":
+ return this.flushPermissions();
+
+ case "SPPrefService": {
+ let prefs = Services.prefs;
+ let prefType = aMessage.json.prefType.toUpperCase();
+ let { prefName, prefValue, iid, defaultValue } = aMessage.json;
+
+ if (aMessage.json.op == "get") {
+ if (!prefName || !prefType) {
+ throw new SpecialPowersError(
+ "Invalid parameters for get in SPPrefService"
+ );
+ }
+
+ // return null if the pref doesn't exist
+ if (
+ defaultValue === undefined &&
+ prefs.getPrefType(prefName) == prefs.PREF_INVALID
+ ) {
+ return null;
+ }
+ return this._getPref(prefName, prefType, defaultValue, iid);
+ } else if (aMessage.json.op == "set") {
+ if (!prefName || !prefType || prefValue === undefined) {
+ throw new SpecialPowersError(
+ "Invalid parameters for set in SPPrefService"
+ );
+ }
+
+ return this._setPref(prefName, prefType, prefValue, iid);
+ } else if (aMessage.json.op == "clear") {
+ if (!prefName) {
+ throw new SpecialPowersError(
+ "Invalid parameters for clear in SPPrefService"
+ );
+ }
+
+ prefs.clearUserPref(prefName);
+ } else {
+ throw new SpecialPowersError("Invalid operation for SPPrefService");
+ }
+
+ return undefined; // See comment at the beginning of this function.
+ }
+
+ case "SPProcessCrashService": {
+ switch (aMessage.json.op) {
+ case "register-observer":
+ this._addProcessCrashObservers();
+ break;
+ case "unregister-observer":
+ this._removeProcessCrashObservers();
+ break;
+ case "delete-crash-dump-files":
+ return this._deleteCrashDumpFiles(aMessage.json.filenames);
+ case "find-crash-dump-files":
+ return this._findCrashDumpFiles(
+ aMessage.json.crashDumpFilesToIgnore
+ );
+ case "delete-pending-crash-dump-files":
+ return this._deletePendingCrashDumpFiles();
+ default:
+ throw new SpecialPowersError(
+ "Invalid operation for SPProcessCrashService"
+ );
+ }
+ return undefined; // See comment at the beginning of this function.
+ }
+
+ case "SPProcessCrashManagerWait": {
+ let promises = aMessage.json.crashIds.map(crashId => {
+ return Services.crashmanager.ensureCrashIsPresent(crashId);
+ });
+ return Promise.all(promises);
+ }
+
+ case "SPPermissionManager": {
+ let msg = aMessage.data;
+ switch (msg.op) {
+ case "add":
+ case "remove":
+ this._permOp(msg);
+ break;
+ case "has":
+ let hasPerm = Services.perms.testPermissionFromPrincipal(
+ msg.principal,
+ msg.type
+ );
+ return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
+ case "test":
+ let testPerm = Services.perms.testPermissionFromPrincipal(
+ msg.principal,
+ msg.type
+ );
+ return testPerm == msg.value;
+ default:
+ throw new SpecialPowersError(
+ "Invalid operation for SPPermissionManager"
+ );
+ }
+ return undefined; // See comment at the beginning of this function.
+ }
+
+ case "SPObserverService": {
+ let topic = aMessage.json.observerTopic;
+ switch (aMessage.json.op) {
+ case "notify":
+ let data = aMessage.json.observerData;
+ Services.obs.notifyObservers(null, topic, data);
+ break;
+ case "add":
+ this._registerObservers._add(topic);
+ break;
+ default:
+ throw new SpecialPowersError(
+ "Invalid operation for SPObserverervice"
+ );
+ }
+ return undefined; // See comment at the beginning of this function.
+ }
+
+ case "SPLoadChromeScript": {
+ let id = aMessage.json.id;
+ let scriptName;
+
+ let jsScript = aMessage.json.function.body;
+ if (aMessage.json.url) {
+ scriptName = aMessage.json.url;
+ } else if (aMessage.json.function) {
+ scriptName =
+ aMessage.json.function.name ||
+ "<loadChromeScript anonymous function>";
+ } else {
+ throw new SpecialPowersError("SPLoadChromeScript: Invalid script");
+ }
+
+ // Setup a chrome sandbox that has access to sendAsyncMessage
+ // and {add,remove}MessageListener in order to communicate with
+ // the mochitest.
+ let sb = new lazy.SpecialPowersSandbox(
+ scriptName,
+ data => {
+ this.sendAsyncMessage("Assert", data);
+ },
+ aMessage.data
+ );
+
+ Object.assign(sb.sandbox, {
+ createWindowlessBrowser,
+ sendAsyncMessage: (name, message) => {
+ this.sendAsyncMessage("SPChromeScriptMessage", {
+ id,
+ name,
+ message,
+ });
+ },
+ addMessageListener: (name, listener) => {
+ this._chromeScriptListeners.push({ id, name, listener });
+ },
+ removeMessageListener: (name, listener) => {
+ let index = this._chromeScriptListeners.findIndex(function (obj) {
+ return (
+ obj.id == id && obj.name == name && obj.listener == listener
+ );
+ });
+ if (index >= 0) {
+ this._chromeScriptListeners.splice(index, 1);
+ }
+ },
+ actorParent: this.manager,
+ });
+
+ // Evaluate the chrome script
+ try {
+ Cu.evalInSandbox(jsScript, sb.sandbox, "1.8", scriptName, 1);
+ } catch (e) {
+ throw new SpecialPowersError(
+ "Error while executing chrome script '" +
+ scriptName +
+ "':\n" +
+ e +
+ "\n" +
+ e.fileName +
+ ":" +
+ e.lineNumber
+ );
+ }
+ return undefined; // See comment at the beginning of this function.
+ }
+
+ case "SPChromeScriptMessage": {
+ let id = aMessage.json.id;
+ let name = aMessage.json.name;
+ let message = aMessage.json.message;
+ let result;
+ for (let listener of this._chromeScriptListeners) {
+ if (listener.name == name && listener.id == id) {
+ result = listener.listener(message);
+ }
+ }
+ return result;
+ }
+
+ case "SPImportInMainProcess": {
+ var message = { hadError: false, errorMessage: null };
+ try {
+ ChromeUtils.import(aMessage.data);
+ } catch (e) {
+ message.hadError = true;
+ message.errorMessage = e.toString();
+ }
+ return message;
+ }
+
+ case "SPCleanUpSTSData": {
+ let origin = aMessage.data.origin;
+ let uri = Services.io.newURI(origin);
+ let sss = Cc["@mozilla.org/ssservice;1"].getService(
+ Ci.nsISiteSecurityService
+ );
+ sss.resetState(uri);
+ return undefined;
+ }
+
+ case "SPRequestDumpCoverageCounters": {
+ return lazy.PerTestCoverageUtils.afterTest();
+ }
+
+ case "SPRequestResetCoverageCounters": {
+ return lazy.PerTestCoverageUtils.beforeTest();
+ }
+
+ case "SPCheckServiceWorkers": {
+ let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+ );
+ let regs = swm.getAllRegistrations();
+
+ // XXX This code is shared with specialpowers.js.
+ let workers = new Array(regs.length);
+ for (let i = 0; i < regs.length; ++i) {
+ let { scope, scriptSpec } = regs.queryElementAt(
+ i,
+ Ci.nsIServiceWorkerRegistrationInfo
+ );
+ workers[i] = { scope, scriptSpec };
+ }
+ return { workers };
+ }
+
+ case "SPLoadExtension": {
+ let id = aMessage.data.id;
+ let ext = aMessage.data.ext;
+ if (AppConstants.platform === "android") {
+ // Some extension APIs are partially implemented in Java, and the
+ // interface between the JS and Java side (GeckoViewWebExtension)
+ // expects extensions to be registered with the AddonManager.
+ //
+ // For simplicity, default to using an Addon Manager (if not null).
+ if (ext.useAddonManager === undefined) {
+ ext.useAddonManager = "android-only";
+ }
+ }
+ // delayedStartup is only supported in xpcshell
+ if (ext.delayedStartup !== undefined) {
+ throw new Error(
+ `delayedStartup is only supported in xpcshell, use "useAddonManager".`
+ );
+ }
+
+ let extension = lazy.ExtensionTestCommon.generate(ext);
+
+ let resultListener = (...args) => {
+ this.sendAsyncMessage("SPExtensionMessage", {
+ id,
+ type: "testResult",
+ args,
+ });
+ };
+
+ let messageListener = (...args) => {
+ args.shift();
+ this.sendAsyncMessage("SPExtensionMessage", {
+ id,
+ type: "testMessage",
+ args,
+ });
+ };
+
+ // Register pass/fail handlers.
+ extension.on("test-result", resultListener);
+ extension.on("test-eq", resultListener);
+ extension.on("test-log", resultListener);
+ extension.on("test-done", resultListener);
+
+ extension.on("test-message", messageListener);
+
+ this._extensions.set(id, extension);
+ return undefined;
+ }
+
+ case "SPStartupExtension": {
+ let id = aMessage.data.id;
+ // This is either an Extension, or (if useAddonManager is set) a MockExtension.
+ let extension = this._extensions.get(id);
+ extension.on("startup", (eventName, ext) => {
+ if (AppConstants.platform === "android") {
+ // We need a way to notify the embedding layer that a new extension
+ // has been installed, so that the java layer can be updated too.
+ Services.obs.notifyObservers(null, "testing-installed-addon", id);
+ }
+ // ext is always the "real" Extension object, even when "extension"
+ // is a MockExtension.
+ this.sendAsyncMessage("SPExtensionMessage", {
+ id,
+ type: "extensionSetId",
+ args: [ext.id, ext.uuid],
+ });
+ });
+
+ // Make sure the extension passes the packaging checks when
+ // they're run on a bare archive rather than a running instance,
+ // as the add-on manager runs them.
+ let extensionData = new lazy.ExtensionData(extension.rootURI);
+ return extensionData
+ .loadManifest()
+ .then(
+ () => {
+ return extensionData.initAllLocales().then(() => {
+ if (extensionData.errors.length) {
+ return Promise.reject(
+ "Extension contains packaging errors"
+ );
+ }
+ return undefined;
+ });
+ },
+ () => {
+ // loadManifest() will throw if we're loading an embedded
+ // extension, so don't worry about locale errors in that
+ // case.
+ }
+ )
+ .then(async () => {
+ // browser tests do not call startup in ExtensionXPCShellUtils or MockExtension,
+ // in that case we have an ID here and we need to set the override.
+ if (extension.id) {
+ await lazy.ExtensionTestCommon.setIncognitoOverride(extension);
+ }
+ return extension.startup().then(
+ () => {},
+ e => {
+ dump(`Extension startup failed: ${e}\n${e.stack}`);
+ throw e;
+ }
+ );
+ });
+ }
+
+ case "SPExtensionMessage": {
+ let id = aMessage.data.id;
+ let extension = this._extensions.get(id);
+ extension.testMessage(...aMessage.data.args);
+ return undefined;
+ }
+
+ case "SPExtensionGrantActiveTab": {
+ let { id, tabId } = aMessage.data;
+ let { tabManager } = this._extensions.get(id);
+ tabManager.addActiveTabPermission(tabManager.get(tabId).nativeTab);
+ return undefined;
+ }
+
+ case "SPUnloadExtension": {
+ let id = aMessage.data.id;
+ let extension = this._extensions.get(id);
+ this._extensions.delete(id);
+ return extension.shutdown().then(() => {
+ return extension._uninstallPromise;
+ });
+ }
+
+ case "SPExtensionTerminateBackground": {
+ let id = aMessage.data.id;
+ let args = aMessage.data.args;
+ let extension = this._extensions.get(id);
+ return extension.terminateBackground(...args);
+ }
+
+ case "SPExtensionWakeupBackground": {
+ let id = aMessage.data.id;
+ let extension = this._extensions.get(id);
+ return extension.wakeupBackground();
+ }
+
+ case "SetAsDefaultAssertHandler": {
+ defaultAssertHandler = this;
+ return undefined;
+ }
+
+ case "Spawn": {
+ // Use a different variable for the profiler marker start time
+ // so that a marker isn't added when we return, but instead when
+ // our promise resolves.
+ let spawnStartTime = startTime;
+ startTime = undefined;
+ let { browsingContext, task, args, caller, hasHarness, imports } =
+ aMessage.data;
+
+ let spParent =
+ browsingContext.currentWindowGlobal.getActor("SpecialPowers");
+
+ let taskId = nextTaskID++;
+ if (hasHarness) {
+ spParent._taskActors.set(taskId, this);
+ }
+
+ return spParent
+ .sendQuery("Spawn", { task, args, caller, taskId, imports })
+ .finally(() => {
+ ChromeUtils.addProfilerMarker(
+ "SpecialPowers",
+ { startTime: spawnStartTime, category: "Test" },
+ aMessage.name
+ );
+ return spParent._taskActors.delete(taskId);
+ });
+ }
+
+ case "SpawnChrome": {
+ let { task, args, caller, imports } = aMessage.data;
+
+ return this._spawnChrome(task, args, caller, imports);
+ }
+
+ case "Snapshot": {
+ let { browsingContext, rect, background, resetScrollPosition } =
+ aMessage.data;
+
+ return browsingContext.currentWindowGlobal
+ .drawSnapshot(rect, 1.0, background, resetScrollPosition)
+ .then(async image => {
+ let hiddenFrame = new lazy.HiddenFrame();
+ let win = await hiddenFrame.get();
+
+ let canvas = win.document.createElement("canvas");
+ canvas.width = image.width;
+ canvas.height = image.height;
+
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(image, 0, 0);
+
+ let data = ctx.getImageData(0, 0, image.width, image.height);
+ hiddenFrame.destroy();
+ return data;
+ });
+ }
+
+ case "SecurityState": {
+ let { browsingContext } = aMessage.data;
+ return browsingContext.secureBrowserUI.state;
+ }
+
+ case "ProxiedAssert": {
+ let { taskId, data } = aMessage.data;
+
+ let actor = this._taskActors.get(taskId) || defaultAssertHandler;
+ actor.sendAsyncMessage("Assert", data);
+
+ return undefined;
+ }
+
+ case "SPRemoveAllServiceWorkers": {
+ return lazy.ServiceWorkerCleanUp.removeAll();
+ }
+
+ case "SPRemoveServiceWorkerDataForExampleDomain": {
+ return lazy.ServiceWorkerCleanUp.removeFromHost("example.com");
+ }
+
+ case "SPGenerateMediaControlKeyTestEvent": {
+ // eslint-disable-next-line no-undef
+ MediaControlService.generateMediaControlKey(aMessage.data.event);
+ return undefined;
+ }
+
+ default:
+ throw new SpecialPowersError(
+ `Unrecognized Special Powers API: ${aMessage.name}`
+ );
+ }
+ // This should be unreachable. If it ever becomes reachable, ESLint
+ // will produce an error about inconsistent return values.
+ } finally {
+ if (startTime) {
+ ChromeUtils.addProfilerMarker(
+ "SpecialPowers",
+ { startTime, category: "Test" },
+ aMessage.name
+ );
+ }
+ }
+ }
+}
diff --git a/testing/specialpowers/content/SpecialPowersSandbox.sys.mjs b/testing/specialpowers/content/SpecialPowersSandbox.sys.mjs
new file mode 100644
index 0000000000..4f9e060595
--- /dev/null
+++ b/testing/specialpowers/content/SpecialPowersSandbox.sys.mjs
@@ -0,0 +1,141 @@
+/* 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 modules handles creating and provisioning Sandboxes for
+ * executing cross-process code from SpecialPowers. This allows all such
+ * sandboxes to have a similar environment, and in particular allows
+ * them to run test assertions in the target process and propagate
+ * results back to the caller.
+ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Assert: "resource://testing-common/Assert.sys.mjs",
+});
+
+// Note: When updating the set of globals exposed to sandboxes by
+// default, please also update the ESLint plugin rule defined in
+// import-content-task-globals.js.
+const SANDBOX_GLOBALS = [
+ "Blob",
+ "ChromeUtils",
+ "FileReader",
+ "TextDecoder",
+ "TextEncoder",
+ "URL",
+];
+const EXTRA_IMPORTS = {
+ EventUtils: "resource://testing-common/SpecialPowersEventUtils.sys.mjs",
+};
+
+let expectFail = false;
+function expectingFail(fn) {
+ try {
+ expectFail = true;
+ fn();
+ } finally {
+ expectFail = false;
+ }
+}
+
+export class SpecialPowersSandbox {
+ constructor(name, reportCallback, opts = {}) {
+ this.name = name;
+ this.reportCallback = reportCallback;
+
+ this._Assert = null;
+
+ this.sandbox = Cu.Sandbox(
+ Cu.getGlobalForObject({}),
+ Object.assign(
+ { wantGlobalProperties: SANDBOX_GLOBALS },
+ opts.sandboxOptions
+ )
+ );
+
+ for (let prop of ["assert", "Assert"]) {
+ Object.defineProperty(this.sandbox, prop, {
+ get: () => {
+ return this.Assert;
+ },
+ enumerable: true,
+ configurable: true,
+ });
+ }
+
+ let imports = {
+ ...EXTRA_IMPORTS,
+ ...opts.imports,
+ };
+ // We explicitly want these directly in the sandbox, and we aren't going
+ // to be using the globals within this file.
+ // eslint-disable-next-line mozilla/lazy-getter-object-name
+ ChromeUtils.defineESModuleGetters(this.sandbox, imports);
+
+ // Note: When updating the set of globals exposed to sandboxes by
+ // default, please also update the ESLint plugin rule defined in
+ // import-content-task-globals.js.
+ Object.assign(this.sandbox, {
+ BrowsingContext,
+ InspectorUtils,
+ ok: (...args) => {
+ this.Assert.ok(...args);
+ },
+ is: (...args) => {
+ this.Assert.equal(...args);
+ },
+ isnot: (...args) => {
+ this.Assert.notEqual(...args);
+ },
+ todo: (...args) => {
+ expectingFail(() => this.Assert.ok(...args));
+ },
+ todo_is: (...args) => {
+ expectingFail(() => this.Assert.equal(...args));
+ },
+ info: info => {
+ this.reportCallback({ info });
+ },
+ });
+ }
+
+ get Assert() {
+ if (!this._Assert) {
+ this._Assert = new lazy.Assert((err, message, stack) => {
+ this.report(err, message, stack);
+ });
+ }
+ return this._Assert;
+ }
+
+ report(err, name, stack) {
+ let diag;
+ if (err) {
+ diag =
+ `got ${uneval(err.actual)}, expected ${uneval(err.expected)} ` +
+ `(operator ${err.operator})`;
+ }
+
+ this.reportCallback({
+ name,
+ diag,
+ passed: !err,
+ stack: stack && stack.formattedStack,
+ expectFail,
+ });
+ }
+
+ execute(task, args, caller) {
+ let func = Cu.evalInSandbox(
+ `(${task})`,
+ this.sandbox,
+ undefined,
+ caller.filename,
+ caller.lineNumber
+ );
+ return func(...args);
+ }
+}
diff --git a/testing/specialpowers/content/WrapPrivileged.sys.mjs b/testing/specialpowers/content/WrapPrivileged.sys.mjs
new file mode 100644
index 0000000000..5fe67a94e2
--- /dev/null
+++ b/testing/specialpowers/content/WrapPrivileged.sys.mjs
@@ -0,0 +1,385 @@
+/* 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 module handles wrapping privileged objects so that they can be exposed
+ * to unprivileged contexts. It is only to be used in automated tests.
+ *
+ * Its exact semantics are also liable to change at any time, so any callers
+ * relying on undocumented behavior or subtle platform features should expect
+ * breakage. Those callers should, wherever possible, migrate to fully
+ * chrome-privileged scripts when they need to interact with privileged APIs.
+ */
+
+// XPCNativeWrapper is not defined globally in ESLint as it may be going away.
+// See bug 1481337.
+/* globals XPCNativeWrapper */
+
+Cu.crashIfNotInAutomation();
+
+let wrappedObjects = new WeakMap();
+let perWindowInfo = new WeakMap();
+let noAutoWrap = new WeakSet();
+
+function isWrappable(x) {
+ if (typeof x === "object") {
+ return x !== null;
+ }
+ return typeof x === "function";
+}
+
+function isWrapper(x) {
+ try {
+ return isWrappable(x) && wrappedObjects.has(x);
+ } catch (e) {
+ // If `x` is a remote object proxy, trying to access an unexpected property
+ // on it will throw a security error, even though we're chrome privileged.
+ // However, remote proxies are not SpecialPowers wrappers, so:
+ return false;
+ }
+}
+
+function unwrapIfWrapped(x) {
+ return isWrapper(x) ? unwrapPrivileged(x) : x;
+}
+
+function wrapIfUnwrapped(x, w) {
+ return isWrapper(x) ? x : wrapPrivileged(x, w);
+}
+
+function isObjectOrArray(obj) {
+ if (Object(obj) !== obj) {
+ return false;
+ }
+ let arrayClasses = [
+ "Object",
+ "Array",
+ "Int8Array",
+ "Uint8Array",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float32Array",
+ "Float64Array",
+ "Uint8ClampedArray",
+ ];
+ let className = Cu.getClassName(obj, true);
+ return arrayClasses.includes(className);
+}
+
+// In general, we want Xray wrappers for content DOM objects, because waiving
+// Xray gives us Xray waiver wrappers that clamp the principal when we cross
+// compartment boundaries. However, there are some exceptions where we want
+// to use a waiver:
+//
+// * Xray adds some gunk to toString(), which has the potential to confuse
+// consumers that aren't expecting Xray wrappers. Since toString() is a
+// non-privileged method that returns only strings, we can just waive Xray
+// for that case.
+//
+// * We implement Xrays to pure JS [[Object]] and [[Array]] instances that
+// filter out tricky things like callables. This is the right thing for
+// security in general, but tends to break tests that try to pass object
+// literals into SpecialPowers. So we waive [[Object]] and [[Array]]
+// instances before inspecting properties.
+//
+// * When we don't have meaningful Xray semantics, we create an Opaque
+// XrayWrapper for security reasons. For test code, we generally want to see
+// through that sort of thing.
+function waiveXraysIfAppropriate(obj, propName) {
+ if (
+ propName == "toString" ||
+ isObjectOrArray(obj) ||
+ /Opaque/.test(Object.prototype.toString.call(obj))
+ ) {
+ return XPCNativeWrapper.unwrap(obj);
+ }
+ return obj;
+}
+
+// We can't call apply() directy on Xray-wrapped functions, so we have to be
+// clever.
+function doApply(fun, invocant, args) {
+ // We implement Xrays to pure JS [[Object]] instances that filter out tricky
+ // things like callables. This is the right thing for security in general,
+ // but tends to break tests that try to pass object literals into
+ // SpecialPowers. So we waive [[Object]] instances when they're passed to a
+ // SpecialPowers-wrapped callable.
+ //
+ // Note that the transitive nature of Xray waivers means that any property
+ // pulled off such an object will also be waived, and so we'll get principal
+ // clamping for Xrayed DOM objects reached from literals, so passing things
+ // like {l : xoWin.location} won't work. Hopefully the rabbit hole doesn't
+ // go that deep.
+ args = args.map(x => (isObjectOrArray(x) ? Cu.waiveXrays(x) : x));
+ return Reflect.apply(fun, invocant, args);
+}
+
+function wrapPrivileged(obj, win) {
+ // Primitives pass straight through.
+ if (!isWrappable(obj)) {
+ return obj;
+ }
+
+ // No double wrapping.
+ if (isWrapper(obj)) {
+ throw new Error("Trying to double-wrap object!");
+ }
+
+ let { windowID, proxies, handler } = perWindowInfo.get(win) || {};
+ // |windowUtils| is undefined if |win| is a non-window object
+ // such as a sandbox.
+ let currentID = win.windowGlobalChild
+ ? win.windowGlobalChild.innerWindowId
+ : 0;
+ // Values are dead objects if the inner window is changed.
+ if (windowID !== currentID) {
+ windowID = currentID;
+ proxies = new WeakMap();
+ handler = Cu.cloneInto(SpecialPowersHandler, win, {
+ cloneFunctions: true,
+ });
+ handler.wrapped = new win.WeakMap();
+ perWindowInfo.set(win, { windowID, proxies, handler });
+ }
+
+ if (proxies.has(obj)) {
+ return proxies.get(obj).proxy;
+ }
+
+ let className = Cu.getClassName(obj, true);
+ if (className === "ArrayBuffer") {
+ // Since |new Uint8Array(<proxy>)| doesn't work as expected, we have to
+ // return a real ArrayBuffer.
+ return obj instanceof win.ArrayBuffer ? obj : Cu.cloneInto(obj, win);
+ }
+
+ let dummy;
+ if (typeof obj === "function") {
+ dummy = Cu.exportFunction(function () {}, win);
+ } else {
+ dummy = new win.Object();
+ }
+ handler.wrapped.set(dummy, { obj });
+
+ let proxy = new win.Proxy(dummy, handler);
+ wrappedObjects.set(proxy, obj);
+ switch (className) {
+ case "AnonymousContent":
+ // Caching anonymous content will cause crashes (bug 1636015).
+ break;
+ case "CSS2Properties":
+ case "CSSStyleRule":
+ case "CSSStyleSheet":
+ // Caching these classes will cause memory leaks.
+ break;
+ default:
+ proxies.set(obj, { proxy });
+ break;
+ }
+ return proxy;
+}
+
+function unwrapPrivileged(x) {
+ // We don't wrap primitives, so sometimes we have a primitive where we'd
+ // expect to have a wrapper. The proxy pretends to be the type that it's
+ // emulating, so we can just as easily check isWrappable() on a proxy as
+ // we can on an unwrapped object.
+ if (!isWrappable(x)) {
+ return x;
+ }
+
+ // If we have a wrappable type, make sure it's wrapped.
+ if (!isWrapper(x)) {
+ throw new Error("Trying to unwrap a non-wrapped object!");
+ }
+
+ // unwrapped.
+ return wrappedObjects.get(x);
+}
+
+function wrapExceptions(global, fn) {
+ try {
+ return fn();
+ } catch (e) {
+ throw wrapIfUnwrapped(e, global);
+ }
+}
+
+let SpecialPowersHandler = {
+ construct(target, args) {
+ // The arguments may or may not be wrappers. Unwrap them if necessary.
+ var unwrappedArgs = Array.from(Cu.waiveXrays(args), x =>
+ unwrapIfWrapped(Cu.unwaiveXrays(x))
+ );
+
+ // We want to invoke "obj" as a constructor, but using unwrappedArgs as
+ // the arguments.
+ let global = Cu.getGlobalForObject(this);
+ return wrapExceptions(global, () =>
+ wrapIfUnwrapped(
+ Reflect.construct(this.wrapped.get(target).obj, unwrappedArgs),
+ global
+ )
+ );
+ },
+
+ apply(target, thisValue, args) {
+ let wrappedObject = this.wrapped.get(target).obj;
+ let global = Cu.getGlobalForObject(this);
+ // The invocant and arguments may or may not be wrappers. Unwrap
+ // them if necessary.
+ var invocant = unwrapIfWrapped(thisValue);
+
+ return wrapExceptions(global, () => {
+ if (noAutoWrap.has(wrappedObject)) {
+ args = Array.from(Cu.waiveXrays(args), x => Cu.unwaiveXrays(x));
+ return doApply(wrappedObject, invocant, args);
+ }
+
+ if (wrappedObject.name == "then") {
+ args = Array.from(Cu.waiveXrays(args), x =>
+ wrapCallback(Cu.unwaiveXrays(x), global)
+ );
+ } else {
+ args = Array.from(Cu.waiveXrays(args), x =>
+ unwrapIfWrapped(Cu.unwaiveXrays(x))
+ );
+ }
+
+ return wrapIfUnwrapped(doApply(wrappedObject, invocant, args), global);
+ });
+ },
+
+ has(target, prop) {
+ return Reflect.has(this.wrapped.get(target).obj, prop);
+ },
+
+ get(target, prop, receiver) {
+ let global = Cu.getGlobalForObject(this);
+ return wrapExceptions(global, () => {
+ let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
+ let val = Reflect.get(obj, prop);
+ return wrapIfUnwrapped(val, global);
+ });
+ },
+
+ set(target, prop, val, receiver) {
+ return wrapExceptions(Cu.getGlobalForObject(this), () => {
+ let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
+ return Reflect.set(obj, prop, unwrapIfWrapped(val));
+ });
+ },
+
+ delete(target, prop) {
+ return wrapExceptions(Cu.getGlobalForObject(this), () => {
+ return Reflect.deleteProperty(this.wrapped.get(target).obj, prop);
+ });
+ },
+
+ defineProperty(target, prop, descriptor) {
+ throw new Error(
+ "Can't call defineProperty on SpecialPowers wrapped object"
+ );
+ },
+
+ getOwnPropertyDescriptor(target, prop) {
+ let global = Cu.getGlobalForObject(this);
+ return wrapExceptions(global, () => {
+ let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
+ let desc = Reflect.getOwnPropertyDescriptor(obj, prop);
+
+ if (desc === undefined) {
+ return undefined;
+ }
+
+ // Transitively maintain the wrapper membrane.
+ let wrapIfExists = key => {
+ if (key in desc) {
+ desc[key] = wrapIfUnwrapped(desc[key], global);
+ }
+ };
+
+ wrapIfExists("value");
+ wrapIfExists("get");
+ wrapIfExists("set");
+
+ // A trapping proxy's properties must always be configurable, but sometimes
+ // we come across non-configurable properties. Tell a white lie.
+ desc.configurable = true;
+
+ return wrapIfUnwrapped(desc, global);
+ });
+ },
+
+ ownKeys(target) {
+ let props = [];
+
+ // Do the normal thing.
+ let wrappedObject = this.wrapped.get(target).obj;
+ let flt = a => !props.includes(a);
+ props = props.concat(Reflect.ownKeys(wrappedObject).filter(flt));
+
+ // If we've got an Xray wrapper, include the expandos as well.
+ if ("wrappedJSObject" in wrappedObject) {
+ props = props.concat(
+ Reflect.ownKeys(wrappedObject.wrappedJSObject).filter(flt)
+ );
+ }
+
+ return Cu.cloneInto(props, Cu.getGlobalForObject(this));
+ },
+
+ preventExtensions(target) {
+ throw new Error(
+ "Can't call preventExtensions on SpecialPowers wrapped object"
+ );
+ },
+};
+
+function wrapCallback(cb, win) {
+ // Do not wrap if it is already privileged.
+ if (!isWrappable(cb) || Cu.getObjectPrincipal(cb).isSystemPrincipal) {
+ return cb;
+ }
+ return function SpecialPowersCallbackWrapper() {
+ var args = Array.from(arguments, obj => wrapIfUnwrapped(obj, win));
+ let invocant = wrapIfUnwrapped(this, win);
+ return unwrapIfWrapped(cb.apply(invocant, args));
+ };
+}
+
+function wrapCallbackObject(obj, win) {
+ // Do not wrap if it is already privileged.
+ if (!isWrappable(obj) || Cu.getObjectPrincipal(obj).isSystemPrincipal) {
+ return obj;
+ }
+ obj = Cu.waiveXrays(obj);
+ var wrapper = {};
+ for (var i in obj) {
+ if (typeof obj[i] == "function") {
+ wrapper[i] = wrapCallback(Cu.unwaiveXrays(obj[i]), win);
+ } else {
+ wrapper[i] = obj[i];
+ }
+ }
+ return wrapper;
+}
+
+function disableAutoWrap(...objs) {
+ objs.forEach(x => noAutoWrap.add(x));
+}
+
+export var WrapPrivileged = {
+ wrap: wrapIfUnwrapped,
+ unwrap: unwrapIfWrapped,
+
+ isWrapper,
+
+ wrapCallback,
+ wrapCallbackObject,
+
+ disableAutoWrap,
+};