summaryrefslogtreecommitdiffstats
path: root/testing/mochitest/tests/SimpleTest
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mochitest/tests/SimpleTest')
-rw-r--r--testing/mochitest/tests/SimpleTest/AccessibilityUtils.js561
-rw-r--r--testing/mochitest/tests/SimpleTest/ChromeTask.js174
-rw-r--r--testing/mochitest/tests/SimpleTest/EventUtils.js3739
-rw-r--r--testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js180
-rw-r--r--testing/mochitest/tests/SimpleTest/LogController.js96
-rw-r--r--testing/mochitest/tests/SimpleTest/MemoryStats.js131
-rw-r--r--testing/mochitest/tests/SimpleTest/MockObjects.js95
-rw-r--r--testing/mochitest/tests/SimpleTest/MozillaLogger.js102
-rw-r--r--testing/mochitest/tests/SimpleTest/NativeKeyCodes.js369
-rw-r--r--testing/mochitest/tests/SimpleTest/SimpleTest.js2216
-rw-r--r--testing/mochitest/tests/SimpleTest/TestRunner.js1103
-rw-r--r--testing/mochitest/tests/SimpleTest/WindowSnapshot.js122
-rw-r--r--testing/mochitest/tests/SimpleTest/WorkerHandler.js46
-rw-r--r--testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js44
-rw-r--r--testing/mochitest/tests/SimpleTest/iframe-between-tests.html17
-rw-r--r--testing/mochitest/tests/SimpleTest/moz.build27
-rw-r--r--testing/mochitest/tests/SimpleTest/paint_listener.js109
-rw-r--r--testing/mochitest/tests/SimpleTest/setup.js383
-rw-r--r--testing/mochitest/tests/SimpleTest/test.css39
19 files changed, 9553 insertions, 0 deletions
diff --git a/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js
new file mode 100644
index 0000000000..481c6b5d35
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js
@@ -0,0 +1,561 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Accessible states used to check node's state from the accessiblity API
+ * perspective.
+ *
+ * Note: if gecko is built with --disable-accessibility, the interfaces
+ * are not defined. This is why we use getters instead to be able to use
+ * these statically.
+ */
+
+this.AccessibilityUtils = (function () {
+ const FORCE_DISABLE_ACCESSIBILITY_PREF = "accessibility.force_disabled";
+
+ // Accessible states.
+ const { STATE_FOCUSABLE, STATE_INVISIBLE, STATE_LINKED, STATE_UNAVAILABLE } =
+ Ci.nsIAccessibleStates;
+
+ // Accessible action for showing long description.
+ const CLICK_ACTION = "click";
+
+ // Roles that are considered focusable with the keyboard.
+ const KEYBOARD_FOCUSABLE_ROLES = new Set([
+ Ci.nsIAccessibleRole.ROLE_BUTTONMENU,
+ Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
+ Ci.nsIAccessibleRole.ROLE_COMBOBOX,
+ Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX,
+ Ci.nsIAccessibleRole.ROLE_ENTRY,
+ Ci.nsIAccessibleRole.ROLE_LINK,
+ Ci.nsIAccessibleRole.ROLE_LISTBOX,
+ Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
+ Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
+ Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
+ Ci.nsIAccessibleRole.ROLE_SLIDER,
+ Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
+ Ci.nsIAccessibleRole.ROLE_SUMMARY,
+ Ci.nsIAccessibleRole.ROLE_SWITCH,
+ Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
+ ]);
+
+ // Roles that are user interactive.
+ const INTERACTIVE_ROLES = new Set([
+ ...KEYBOARD_FOCUSABLE_ROLES,
+ Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM,
+ Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION,
+ Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION,
+ Ci.nsIAccessibleRole.ROLE_MENUITEM,
+ Ci.nsIAccessibleRole.ROLE_OPTION,
+ Ci.nsIAccessibleRole.ROLE_OUTLINE,
+ Ci.nsIAccessibleRole.ROLE_OUTLINEITEM,
+ Ci.nsIAccessibleRole.ROLE_PAGETAB,
+ Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM,
+ Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
+ Ci.nsIAccessibleRole.ROLE_RICH_OPTION,
+ ]);
+
+ // Roles that are considered interactive when they are focusable.
+ const INTERACTIVE_IF_FOCUSABLE_ROLES = new Set([
+ // If article is focusable, we can assume it is inside a feed.
+ Ci.nsIAccessibleRole.ROLE_ARTICLE,
+ // Column header can be focusable.
+ Ci.nsIAccessibleRole.ROLE_COLUMNHEADER,
+ Ci.nsIAccessibleRole.ROLE_GRID_CELL,
+ Ci.nsIAccessibleRole.ROLE_MENUBAR,
+ Ci.nsIAccessibleRole.ROLE_MENUPOPUP,
+ Ci.nsIAccessibleRole.ROLE_PAGETABLIST,
+ // Row header can be focusable.
+ Ci.nsIAccessibleRole.ROLE_ROWHEADER,
+ Ci.nsIAccessibleRole.ROLE_SCROLLBAR,
+ Ci.nsIAccessibleRole.ROLE_SEPARATOR,
+ Ci.nsIAccessibleRole.ROLE_TOOLBAR,
+ ]);
+
+ // Roles that are considered form controls.
+ const FORM_ROLES = new Set([
+ Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
+ Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION,
+ Ci.nsIAccessibleRole.ROLE_COMBOBOX,
+ Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX,
+ Ci.nsIAccessibleRole.ROLE_ENTRY,
+ Ci.nsIAccessibleRole.ROLE_LISTBOX,
+ Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
+ Ci.nsIAccessibleRole.ROLE_PROGRESSBAR,
+ Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
+ Ci.nsIAccessibleRole.ROLE_SLIDER,
+ Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
+ Ci.nsIAccessibleRole.ROLE_SWITCH,
+ ]);
+
+ const DEFAULT_ENV = Object.freeze({
+ // Checks that accessible object has at least one accessible action.
+ actionCountRule: true,
+ // Checks that accessible object (and its corresponding node) is focusable
+ // (has focusable state and its node's tabindex is not set to -1).
+ focusableRule: true,
+ // Checks that clickable accessible object (and its corresponding node) has
+ // appropriate interactive semantics.
+ ifClickableThenInteractiveRule: true,
+ // Checks that accessible object has a role that is considered to be
+ // interactive.
+ interactiveRule: true,
+ // Checks that accessible object has a non-empty label.
+ labelRule: true,
+ // Checks that a node has a corresponging accessible object.
+ mustHaveAccessibleRule: true,
+ // Checks that accessible object (and its corresponding node) have a non-
+ // negative tabindex. Platform accessibility API still sets focusable state
+ // on tabindex=-1 nodes.
+ nonNegativeTabIndexRule: true,
+ });
+
+ let gA11YChecks = false;
+
+ let gEnv = {
+ ...DEFAULT_ENV,
+ };
+
+ /**
+ * Get role attribute for an accessible object if specified for its
+ * corresponding {@code DOMNode}.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible for which to determine its role attribute value.
+ *
+ * @returns {String}
+ * Role attribute value if specified.
+ */
+ function getAriaRoles(accessible) {
+ try {
+ return accessible.attributes.getStringProperty("xml-roles");
+ } catch (e) {
+ // No xml-roles. nsPersistentProperties throws if the attribute for a key
+ // is not found.
+ }
+
+ return "";
+ }
+
+ /**
+ * Get related accessible objects that are targets of labelled by relation e.g.
+ * labels.
+ * @param {nsIAccessible} accessible
+ * Accessible objects to get labels for.
+ *
+ * @returns {Array}
+ * A list of accessible objects that are labels for a given accessible.
+ */
+ function getLabels(accessible) {
+ const relation = accessible.getRelationByType(
+ Ci.nsIAccessibleRelation.RELATION_LABELLED_BY
+ );
+ return [...relation.getTargets().enumerate(Ci.nsIAccessible)];
+ }
+
+ /**
+ * Test if an accessible has a {@code hidden} attribute.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible object.
+ *
+ * @return {boolean}
+ * True if the accessible object has a {@code hidden} attribute, false
+ * otherwise.
+ */
+ function hasHiddenAttribute(accessible) {
+ let hidden = false;
+ try {
+ hidden = accessible.attributes.getStringProperty("hidden");
+ } catch (e) {}
+ // If the property is missing, error will be thrown
+ return hidden && hidden === "true";
+ }
+
+ /**
+ * Test if an accessible is hidden from the user.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible object.
+ *
+ * @return {boolean}
+ * True if accessible is hidden from user, false otherwise.
+ */
+ function isHidden(accessible) {
+ if (!accessible) {
+ return true;
+ }
+
+ while (accessible) {
+ if (hasHiddenAttribute(accessible)) {
+ return true;
+ }
+
+ accessible = accessible.parent;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if an accessible has a given state.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible object to test.
+ * @param {number} stateToMatch
+ * State to match.
+ *
+ * @return {boolean}
+ * True if |accessible| has |stateToMatch|, false otherwise.
+ */
+ function matchState(accessible, stateToMatch) {
+ const state = {};
+ accessible.getState(state, {});
+
+ return !!(state.value & stateToMatch);
+ }
+
+ /**
+ * Determine if accessible is focusable with the keyboard.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible for which to determine if it is keyboard focusable.
+ *
+ * @returns {Boolean}
+ * True if focusable with the keyboard.
+ */
+ function isKeyboardFocusable(accessible) {
+ // State will be focusable even if the tabindex is negative.
+ return (
+ matchState(accessible, STATE_FOCUSABLE) &&
+ // Platform accessibility will still report STATE_FOCUSABLE even with the
+ // tabindex="-1" so we need to check that it is >= 0 to be considered
+ // keyboard focusable.
+ (!gEnv.nonNegativeTabIndexRule || accessible.DOMNode.tabIndex > -1)
+ );
+ }
+
+ function buildMessage(message, DOMNode) {
+ if (DOMNode) {
+ const { id, tagName, className } = DOMNode;
+ message += `: id: ${id}, tagName: ${tagName}, className: ${className}`;
+ }
+
+ return message;
+ }
+
+ /**
+ * Fail a test with a given message because of an issue with a given
+ * accessible object. This is used for cases where there's an actual
+ * accessibility failure that prevents UI from being accessible to keyboard/AT
+ * users.
+ *
+ * @param {String} message
+ * @param {nsIAccessible} accessible
+ * Accessible to log along with the failure message.
+ */
+ function a11yFail(message, { DOMNode }) {
+ SpecialPowers.SimpleTest.ok(false, buildMessage(message, DOMNode));
+ }
+
+ /**
+ * Log a todo statement with a given message because of an issue with a given
+ * accessible object. This is used for cases where accessibility best
+ * practices are not followed or for something that is not as severe to be
+ * considered a failure.
+ * @param {String} message
+ * @param {nsIAccessible} accessible
+ * Accessible to log along with the todo message.
+ */
+ function a11yWarn(message, { DOMNode }) {
+ SpecialPowers.SimpleTest.todo(false, buildMessage(message, DOMNode));
+ }
+
+ /**
+ * Test if the node's unavailable via the accessibility API.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible object.
+ */
+ function assertEnabled(accessible) {
+ if (matchState(accessible, STATE_UNAVAILABLE)) {
+ a11yFail(
+ "Node is enabled but disabled via the accessibility API",
+ accessible
+ );
+ }
+ }
+
+ /**
+ * Test if it is possible to focus on a node with the keyboard. This method
+ * also checks for additional keyboard focus issues that might arise.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible object for a node.
+ */
+ function assertFocusable(accessible) {
+ if (gEnv.focusableRule && !isKeyboardFocusable(accessible)) {
+ const ariaRoles = getAriaRoles(accessible);
+ // Do not force ARIA combobox or listbox to be focusable.
+ if (!ariaRoles.includes("combobox") && !ariaRoles.includes("listbox")) {
+ a11yFail("Node is not focusable via the accessibility API", accessible);
+ }
+
+ return;
+ }
+
+ if (!INTERACTIVE_IF_FOCUSABLE_ROLES.has(accessible.role)) {
+ // ROLE_TABLE is used for grids too which are considered interactive.
+ if (
+ accessible.role === Ci.nsIAccessibleRole.ROLE_TABLE &&
+ !getAriaRoles(accessible).includes("grid")
+ ) {
+ a11yWarn(
+ "Focusable nodes should have interactive semantics",
+ accessible
+ );
+
+ return;
+ }
+ }
+
+ if (accessible.DOMNode.tabIndex > 0) {
+ a11yWarn("Avoid using tabindex attribute greater than zero", accessible);
+ }
+ }
+
+ /**
+ * Test if it is possible to interact with a node via the accessibility API.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible object for a node.
+ */
+ function assertInteractive(accessible) {
+ if (gEnv.actionCountRule && accessible.actionCount === 0) {
+ a11yFail("Node does not support any accessible actions", accessible);
+
+ return;
+ }
+
+ if (gEnv.interactiveRule && !INTERACTIVE_ROLES.has(accessible.role)) {
+ if (
+ // Labels that have a label for relation with their target are clickable.
+ (accessible.role !== Ci.nsIAccessibleRole.ROLE_LABEL ||
+ accessible.getRelationByType(
+ Ci.nsIAccessibleRelation.RELATION_LABEL_FOR
+ ).targetsCount === 0) &&
+ // Images that are inside an anchor (have linked state).
+ (accessible.role !== Ci.nsIAccessibleRole.ROLE_GRAPHIC ||
+ !matchState(accessible, STATE_LINKED))
+ ) {
+ // Look for click action in the list of actions.
+ for (let i = 0; i < accessible.actionCount; i++) {
+ if (
+ gEnv.ifClickableThenInteractiveRule &&
+ accessible.getActionName(i) === CLICK_ACTION
+ ) {
+ a11yFail(
+ "Clickable nodes must have interactive semantics",
+ accessible
+ );
+ }
+ }
+ }
+
+ a11yFail(
+ "Node does not have a correct interactive role and may not be " +
+ "manipulated via the accessibility API",
+ accessible
+ );
+ }
+ }
+
+ /**
+ * Test if the node is labelled appropriately for accessibility API.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible object for a node.
+ */
+ function assertLabelled(accessible) {
+ const name = accessible.name && accessible.name.trim();
+ if (gEnv.labelRule && !name) {
+ a11yFail("Interactive elements must be labeled", accessible);
+
+ return;
+ }
+
+ const { DOMNode } = accessible;
+ if (FORM_ROLES.has(accessible.role)) {
+ const labels = getLabels(accessible);
+ const hasNameFromVisibleLabel = labels.some(
+ label => !matchState(label, STATE_INVISIBLE)
+ );
+
+ if (!hasNameFromVisibleLabel) {
+ a11yWarn("Form elements should have a visible text label", accessible);
+ }
+ } else if (
+ accessible.role === Ci.nsIAccessibleRole.ROLE_LINK &&
+ DOMNode.nodeName === "AREA" &&
+ DOMNode.hasAttribute("href")
+ ) {
+ const alt = DOMNode.getAttribute("alt");
+ if (alt && alt.trim() !== name) {
+ a11yFail(
+ "Use alt attribute to label area elements that have the href attribute",
+ accessible
+ );
+ }
+ }
+ }
+
+ /**
+ * Test if the node's visible via accessibility API.
+ *
+ * @param {nsIAccessible} accessible
+ * Accessible object for a node.
+ */
+ function assertVisible(accessible) {
+ if (isHidden(accessible)) {
+ a11yFail(
+ "Node is not currently visible via the accessibility API and may not " +
+ "be manipulated by it",
+ accessible
+ );
+ }
+ }
+
+ /**
+ * Walk node ancestry and force refresh driver tick in every document.
+ * @param {DOMNode} node
+ * Node for traversing the ancestry.
+ */
+ function forceRefreshDriverTick(node) {
+ const wins = [];
+ let bc = BrowsingContext.getFromWindow(node.ownerDocument.defaultView); // eslint-disable-line
+ while (bc) {
+ wins.push(bc.associatedWindow);
+ bc = bc.embedderWindowGlobal?.browsingContext;
+ }
+
+ let win = wins.pop();
+ while (win) {
+ // Stop the refresh driver from doing its regular ticks and force two
+ // refresh driver ticks: first to let layout update and notify a11y, and
+ // the second to let a11y process updates.
+ win.windowUtils.advanceTimeAndRefresh(100);
+ win.windowUtils.advanceTimeAndRefresh(100);
+ // Go back to normal refresh driver ticks.
+ win.windowUtils.restoreNormalRefresh();
+ win = wins.pop();
+ }
+ }
+
+ /**
+ * Get an accessible object for a node.
+ * Note: this method will not resolve if accessible object does not become
+ * available for a given node.
+ *
+ * @param {DOMNode} node
+ * Node to get the accessible object for.
+ *
+ * @return {nsIAccessible}
+ * Accessibility object for a given node.
+ */
+ function getAccessible(node) {
+ const accessibilityService = Cc[
+ "@mozilla.org/accessibilityService;1"
+ ].getService(Ci.nsIAccessibilityService);
+ if (!accessibilityService) {
+ // This is likely a build with --disable-accessibility
+ return null;
+ }
+
+ let acc = accessibilityService.getAccessibleFor(node);
+ if (acc) {
+ return acc;
+ }
+
+ // Force refresh tick throughout document hierarchy
+ forceRefreshDriverTick(node);
+ return accessibilityService.getAccessibleFor(node);
+ }
+
+ function runIfA11YChecks(task) {
+ return (...args) => (gA11YChecks ? task(...args) : null);
+ }
+
+ /**
+ * AccessibilityUtils provides utility methods for retrieving accessible objects
+ * and performing accessibility related checks.
+ * Current methods:
+ * assertCanBeClicked
+ * setEnv
+ * resetEnv
+ *
+ */
+ const AccessibilityUtils = {
+ assertCanBeClicked(node) {
+ const acc = getAccessible(node);
+ if (!acc) {
+ if (gEnv.mustHaveAccessibleRule) {
+ a11yFail("Node is not accessible via accessibility API", {
+ DOMNode: node,
+ });
+ }
+
+ return;
+ }
+
+ assertInteractive(acc);
+ assertFocusable(acc);
+ assertVisible(acc);
+ assertEnabled(acc);
+ assertLabelled(acc);
+ },
+
+ setEnv(env = DEFAULT_ENV) {
+ gEnv = {
+ ...DEFAULT_ENV,
+ ...env,
+ };
+ },
+
+ resetEnv() {
+ gEnv = { ...DEFAULT_ENV };
+ },
+
+ reset(a11yChecks = false) {
+ gA11YChecks = a11yChecks;
+
+ const { Services } = SpecialPowers;
+ // Disable accessibility service if it is running and if a11y checks are
+ // disabled.
+ if (!gA11YChecks && Services.appinfo.accessibilityEnabled) {
+ Services.prefs.setIntPref(FORCE_DISABLE_ACCESSIBILITY_PREF, 1);
+ Services.prefs.clearUserPref(FORCE_DISABLE_ACCESSIBILITY_PREF);
+ }
+
+ // Reset accessibility environment flags that might've been set within the
+ // test.
+ this.resetEnv();
+ },
+ };
+
+ AccessibilityUtils.assertCanBeClicked = runIfA11YChecks(
+ AccessibilityUtils.assertCanBeClicked.bind(AccessibilityUtils)
+ );
+
+ AccessibilityUtils.setEnv = runIfA11YChecks(
+ AccessibilityUtils.setEnv.bind(AccessibilityUtils)
+ );
+
+ AccessibilityUtils.resetEnv = runIfA11YChecks(
+ AccessibilityUtils.resetEnv.bind(AccessibilityUtils)
+ );
+
+ return AccessibilityUtils;
+})();
diff --git a/testing/mochitest/tests/SimpleTest/ChromeTask.js b/testing/mochitest/tests/SimpleTest/ChromeTask.js
new file mode 100644
index 0000000000..289f6f2eb2
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/ChromeTask.js
@@ -0,0 +1,174 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function ChromeTask_ChromeScript() {
+ /* eslint-env mozilla/chrome-script */
+
+ "use strict";
+
+ const { Assert: AssertCls } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ addMessageListener("chrome-task:spawn", async function (aData) {
+ let id = aData.id;
+ let source = aData.runnable || "()=>{}";
+
+ function getStack(aStack) {
+ let frames = [];
+ for (let frame = aStack; frame; frame = frame.caller) {
+ frames.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber);
+ }
+ return frames.join("\n");
+ }
+
+ /* eslint-disable no-unused-vars */
+ var Assert = new AssertCls((err, message, stack) => {
+ sendAsyncMessage("chrome-task:test-result", {
+ id,
+ condition: !err,
+ name: err ? err.message : message,
+ stack: getStack(err ? err.stack : stack),
+ });
+ });
+
+ var ok = Assert.ok.bind(Assert);
+ var is = Assert.equal.bind(Assert);
+ var isnot = Assert.notEqual.bind(Assert);
+
+ function todo(expr, name) {
+ sendAsyncMessage("chrome-task:test-todo", { id, expr, name });
+ }
+
+ function todo_is(a, b, name) {
+ sendAsyncMessage("chrome-task:test-todo_is", { id, a, b, name });
+ }
+
+ function info(name) {
+ sendAsyncMessage("chrome-task:test-info", { id, name });
+ }
+ /* eslint-enable no-unused-vars */
+
+ try {
+ let runnablestr = `
+ (() => {
+ return (${source});
+ })();`;
+
+ // eslint-disable-next-line no-eval
+ let runnable = eval(runnablestr);
+ let result = await runnable.call(this, aData.arg);
+ sendAsyncMessage("chrome-task:complete", {
+ id,
+ result,
+ });
+ } catch (ex) {
+ sendAsyncMessage("chrome-task:complete", {
+ id,
+ error: ex.toString(),
+ });
+ }
+ });
+}
+
+/**
+ * This object provides the public module functions.
+ */
+var ChromeTask = {
+ /**
+ * the ChromeScript if it has already been loaded.
+ */
+ _chromeScript: null,
+
+ /**
+ * Mapping from message id to associated promise.
+ */
+ _promises: new Map(),
+
+ /**
+ * Incrementing integer to generate unique message id.
+ */
+ _messageID: 1,
+
+ /**
+ * Creates and starts a new task in the chrome process.
+ *
+ * @param arg A single serializable argument that will be passed to the
+ * task when executed on the content process.
+ * @param task
+ * - A generator or function which will be serialized and sent to
+ * the remote browser to be executed. Unlike Task.spawn, this
+ * argument may not be an iterator as it will be serialized and
+ * sent to the remote browser.
+ * @return A promise object where you can register completion callbacks to be
+ * called when the task terminates.
+ * @resolves With the final returned value of the task if it executes
+ * successfully.
+ * @rejects An error message if execution fails.
+ */
+ spawn: function ChromeTask_spawn(arg, task) {
+ // Load the frame script if needed.
+ let handle = ChromeTask._chromeScript;
+ if (!handle) {
+ handle = SpecialPowers.loadChromeScript(ChromeTask_ChromeScript);
+ handle.addMessageListener("chrome-task:complete", ChromeTask.onComplete);
+ handle.addMessageListener("chrome-task:test-result", ChromeTask.onResult);
+ handle.addMessageListener("chrome-task:test-info", ChromeTask.onInfo);
+ handle.addMessageListener("chrome-task:test-todo", ChromeTask.onTodo);
+ handle.addMessageListener(
+ "chrome-task:test-todo_is",
+ ChromeTask.onTodoIs
+ );
+ ChromeTask._chromeScript = handle;
+ }
+
+ let deferred = {};
+ deferred.promise = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+
+ let id = ChromeTask._messageID++;
+ ChromeTask._promises.set(id, deferred);
+
+ handle.sendAsyncMessage("chrome-task:spawn", {
+ id,
+ runnable: task.toString(),
+ arg,
+ });
+
+ return deferred.promise;
+ },
+
+ onComplete(aData) {
+ let deferred = ChromeTask._promises.get(aData.id);
+ ChromeTask._promises.delete(aData.id);
+
+ if (aData.error) {
+ deferred.reject(aData.error);
+ } else {
+ deferred.resolve(aData.result);
+ }
+ },
+
+ onResult(aData) {
+ SimpleTest.record(aData.condition, aData.name);
+ },
+
+ onInfo(aData) {
+ SimpleTest.info(aData.name);
+ },
+
+ onTodo(aData) {
+ SimpleTest.todo(aData.expr, aData.name);
+ },
+
+ onTodoIs(aData) {
+ SimpleTest.todo_is(aData.a, aData.b, aData.name);
+ },
+};
diff --git a/testing/mochitest/tests/SimpleTest/EventUtils.js b/testing/mochitest/tests/SimpleTest/EventUtils.js
new file mode 100644
index 0000000000..35e4dbb3d0
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/EventUtils.js
@@ -0,0 +1,3739 @@
+/**
+ * EventUtils provides some utility methods for creating and sending DOM events.
+ *
+ * When adding methods to this file, please add a performance test for it.
+ */
+
+// Certain functions assume this is loaded into browser window scope.
+// This is modifiable because certain chrome tests create their own gBrowser.
+/* global gBrowser:true */
+
+// This file is used both in privileged and unprivileged contexts, so we have to
+// be careful about our access to Components.interfaces. We also want to avoid
+// naming collisions with anything that might be defined in the scope that imports
+// this script.
+//
+// Even if the real |Components| doesn't exist, we might shim in a simple JS
+// placebo for compat. An easy way to differentiate this from the real thing
+// is whether the property is read-only or not. The real |Components| property
+// is read-only.
+/* global _EU_Ci, _EU_Cc, _EU_Cu, _EU_ChromeUtils, _EU_OS */
+window.__defineGetter__("_EU_Ci", function () {
+ var c = Object.getOwnPropertyDescriptor(window, "Components");
+ return c && c.value && !c.writable ? Ci : SpecialPowers.Ci;
+});
+
+window.__defineGetter__("_EU_Cc", function () {
+ var c = Object.getOwnPropertyDescriptor(window, "Components");
+ return c && c.value && !c.writable ? Cc : SpecialPowers.Cc;
+});
+
+window.__defineGetter__("_EU_Cu", function () {
+ var c = Object.getOwnPropertyDescriptor(window, "Components");
+ return c && c.value && !c.writable ? Cu : SpecialPowers.Cu;
+});
+
+window.__defineGetter__("_EU_ChromeUtils", function () {
+ var c = Object.getOwnPropertyDescriptor(window, "ChromeUtils");
+ return c && c.value && !c.writable ? ChromeUtils : SpecialPowers.ChromeUtils;
+});
+
+window.__defineGetter__("_EU_OS", function () {
+ delete this._EU_OS;
+ try {
+ this._EU_OS = _EU_ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+ ).platform;
+ } catch (ex) {
+ this._EU_OS = null;
+ }
+ return this._EU_OS;
+});
+
+function _EU_isMac(aWindow = window) {
+ if (window._EU_OS) {
+ return window._EU_OS == "macosx";
+ }
+ if (aWindow) {
+ try {
+ return aWindow.navigator.platform.indexOf("Mac") > -1;
+ } catch (ex) {}
+ }
+ return navigator.platform.indexOf("Mac") > -1;
+}
+
+function _EU_isWin(aWindow = window) {
+ if (window._EU_OS) {
+ return window._EU_OS == "win";
+ }
+ if (aWindow) {
+ try {
+ return aWindow.navigator.platform.indexOf("Win") > -1;
+ } catch (ex) {}
+ }
+ return navigator.platform.indexOf("Win") > -1;
+}
+
+function _EU_isLinux(aWindow = window) {
+ if (window._EU_OS) {
+ return window._EU_OS == "linux";
+ }
+ if (aWindow) {
+ try {
+ return aWindow.navigator.platform.startsWith("Linux");
+ } catch (ex) {}
+ }
+ return navigator.platform.startsWith("Linux");
+}
+
+function _EU_isAndroid(aWindow = window) {
+ if (window._EU_OS) {
+ return window._EU_OS == "android";
+ }
+ if (aWindow) {
+ try {
+ return aWindow.navigator.userAgent.includes("Android");
+ } catch (ex) {}
+ }
+ return navigator.userAgent.includes("Android");
+}
+
+function _EU_maybeWrap(o) {
+ // We're used in some contexts where there is no SpecialPowers and also in
+ // some where it exists but has no wrap() method. And this is somewhat
+ // independent of whether window.Components is a thing...
+ var haveWrap = false;
+ try {
+ haveWrap = SpecialPowers.wrap != undefined;
+ } catch (e) {
+ // Just leave it false.
+ }
+ if (!haveWrap) {
+ // Not much we can do here.
+ return o;
+ }
+ var c = Object.getOwnPropertyDescriptor(window, "Components");
+ return c && c.value && !c.writable ? o : SpecialPowers.wrap(o);
+}
+
+function _EU_maybeUnwrap(o) {
+ var c = Object.getOwnPropertyDescriptor(window, "Components");
+ return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o);
+}
+
+function _EU_getPlatform() {
+ if (_EU_isWin()) {
+ return "windows";
+ }
+ if (_EU_isMac()) {
+ return "mac";
+ }
+ if (_EU_isAndroid()) {
+ return "android";
+ }
+ if (_EU_isLinux()) {
+ return "linux";
+ }
+ return "unknown";
+}
+
+/**
+ * promiseElementReadyForUserInput() dispatches mousemove events to aElement
+ * and waits one of them for a while. Then, returns "resolved" state when it's
+ * successfully received. Otherwise, if it couldn't receive mousemove event on
+ * it, this throws an exception. So, aElement must be an element which is
+ * assumed non-collapsed visible element in the window.
+ *
+ * This is useful if you need to synthesize mouse events via the main process
+ * but your test cannot check whether the element is now in APZ to deliver
+ * a user input event.
+ */
+async function promiseElementReadyForUserInput(
+ aElement,
+ aWindow = window,
+ aLogFunc = null
+) {
+ if (typeof aElement == "string") {
+ aElement = aWindow.document.getElementById(aElement);
+ }
+
+ function waitForMouseMoveForHittest() {
+ return new Promise(resolve => {
+ let timeout;
+ const onHit = () => {
+ if (aLogFunc) {
+ aLogFunc("mousemove received");
+ }
+ aWindow.clearInterval(timeout);
+ resolve(true);
+ };
+ aElement.addEventListener("mousemove", onHit, {
+ capture: true,
+ once: true,
+ });
+ timeout = aWindow.setInterval(() => {
+ if (aLogFunc) {
+ aLogFunc("mousemove not received in this 300ms");
+ }
+ aElement.removeEventListener("mousemove", onHit, {
+ capture: true,
+ });
+ resolve(false);
+ }, 300);
+ synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow);
+ });
+ }
+ for (let i = 0; i < 20; i++) {
+ if (await waitForMouseMoveForHittest()) {
+ return Promise.resolve();
+ }
+ }
+ throw new Error("The element or the window did not become interactive");
+}
+
+function getElement(id) {
+ return typeof id == "string" ? document.getElementById(id) : id;
+}
+
+this.$ = this.getElement;
+
+function computeButton(aEvent) {
+ if (typeof aEvent.button != "undefined") {
+ return aEvent.button;
+ }
+ return aEvent.type == "contextmenu" ? 2 : 0;
+}
+
+function computeButtons(aEvent, utils) {
+ if (typeof aEvent.buttons != "undefined") {
+ return aEvent.buttons;
+ }
+
+ if (typeof aEvent.button != "undefined") {
+ return utils.MOUSE_BUTTONS_NOT_SPECIFIED;
+ }
+
+ if (typeof aEvent.type != "undefined" && aEvent.type != "mousedown") {
+ return utils.MOUSE_BUTTONS_NO_BUTTON;
+ }
+
+ return utils.MOUSE_BUTTONS_NOT_SPECIFIED;
+}
+
+/**
+ * Send a mouse event to the node aTarget (aTarget can be an id, or an
+ * actual node) . The "event" passed in to aEvent is just a JavaScript
+ * object with the properties set that the real mouse event object should
+ * have. This includes the type of the mouse event. Pretty much all those
+ * properties are optional.
+ * E.g. to send an click event to the node with id 'node' you might do this:
+ *
+ * ``sendMouseEvent({type:'click'}, 'node');``
+ */
+function sendMouseEvent(aEvent, aTarget, aWindow) {
+ if (
+ ![
+ "click",
+ "contextmenu",
+ "dblclick",
+ "mousedown",
+ "mouseup",
+ "mouseover",
+ "mouseout",
+ ].includes(aEvent.type)
+ ) {
+ throw new Error(
+ "sendMouseEvent doesn't know about event type '" + aEvent.type + "'"
+ );
+ }
+
+ if (!aWindow) {
+ aWindow = window;
+ }
+
+ if (typeof aTarget == "string") {
+ aTarget = aWindow.document.getElementById(aTarget);
+ }
+
+ if (aEvent.type === "click" && this.AccessibilityUtils) {
+ this.AccessibilityUtils.assertCanBeClicked(aTarget);
+ }
+
+ var event = aWindow.document.createEvent("MouseEvent");
+
+ var typeArg = aEvent.type;
+ var canBubbleArg = true;
+ var cancelableArg = true;
+ var viewArg = aWindow;
+ var detailArg =
+ aEvent.detail ||
+ // eslint-disable-next-line no-nested-ternary
+ (aEvent.type == "click" ||
+ aEvent.type == "mousedown" ||
+ aEvent.type == "mouseup"
+ ? 1
+ : aEvent.type == "dblclick"
+ ? 2
+ : 0);
+ var screenXArg = aEvent.screenX || 0;
+ var screenYArg = aEvent.screenY || 0;
+ var clientXArg = aEvent.clientX || 0;
+ var clientYArg = aEvent.clientY || 0;
+ var ctrlKeyArg = aEvent.ctrlKey || false;
+ var altKeyArg = aEvent.altKey || false;
+ var shiftKeyArg = aEvent.shiftKey || false;
+ var metaKeyArg = aEvent.metaKey || false;
+ var buttonArg = computeButton(aEvent);
+ var relatedTargetArg = aEvent.relatedTarget || null;
+
+ event.initMouseEvent(
+ typeArg,
+ canBubbleArg,
+ cancelableArg,
+ viewArg,
+ detailArg,
+ screenXArg,
+ screenYArg,
+ clientXArg,
+ clientYArg,
+ ctrlKeyArg,
+ altKeyArg,
+ shiftKeyArg,
+ metaKeyArg,
+ buttonArg,
+ relatedTargetArg
+ );
+
+ // If documentURIObject exists or `window` is a stub object, we're in
+ // a chrome scope, so don't bother trying to go through SpecialPowers.
+ if (!window.document || window.document.documentURIObject) {
+ return aTarget.dispatchEvent(event);
+ }
+ return SpecialPowers.dispatchEvent(aWindow, aTarget, event);
+}
+
+function isHidden(aElement) {
+ var box = aElement.getBoundingClientRect();
+ return box.width == 0 && box.height == 0;
+}
+
+/**
+ * Send a drag event to the node aTarget (aTarget can be an id, or an
+ * actual node) . The "event" passed in to aEvent is just a JavaScript
+ * object with the properties set that the real drag event object should
+ * have. This includes the type of the drag event.
+ */
+function sendDragEvent(aEvent, aTarget, aWindow = window) {
+ if (
+ ![
+ "drag",
+ "dragstart",
+ "dragend",
+ "dragover",
+ "dragenter",
+ "dragleave",
+ "drop",
+ ].includes(aEvent.type)
+ ) {
+ throw new Error(
+ "sendDragEvent doesn't know about event type '" + aEvent.type + "'"
+ );
+ }
+
+ if (typeof aTarget == "string") {
+ aTarget = aWindow.document.getElementById(aTarget);
+ }
+
+ /*
+ * Drag event cannot be performed if the element is hidden, except 'dragend'
+ * event where the element can becomes hidden after start dragging.
+ */
+ if (aEvent.type != "dragend" && isHidden(aTarget)) {
+ var targetName = aTarget.nodeName;
+ if ("id" in aTarget && aTarget.id) {
+ targetName += "#" + aTarget.id;
+ }
+ throw new Error(`${aEvent.type} event target ${targetName} is hidden`);
+ }
+
+ var event = aWindow.document.createEvent("DragEvent");
+
+ var typeArg = aEvent.type;
+ var canBubbleArg = true;
+ var cancelableArg = true;
+ var viewArg = aWindow;
+ var detailArg = aEvent.detail || 0;
+ var screenXArg = aEvent.screenX || 0;
+ var screenYArg = aEvent.screenY || 0;
+ var clientXArg = aEvent.clientX || 0;
+ var clientYArg = aEvent.clientY || 0;
+ var ctrlKeyArg = aEvent.ctrlKey || false;
+ var altKeyArg = aEvent.altKey || false;
+ var shiftKeyArg = aEvent.shiftKey || false;
+ var metaKeyArg = aEvent.metaKey || false;
+ var buttonArg = computeButton(aEvent);
+ var relatedTargetArg = aEvent.relatedTarget || null;
+ var dataTransfer = aEvent.dataTransfer || null;
+
+ event.initDragEvent(
+ typeArg,
+ canBubbleArg,
+ cancelableArg,
+ viewArg,
+ detailArg,
+ screenXArg,
+ screenYArg,
+ clientXArg,
+ clientYArg,
+ ctrlKeyArg,
+ altKeyArg,
+ shiftKeyArg,
+ metaKeyArg,
+ buttonArg,
+ relatedTargetArg,
+ dataTransfer
+ );
+
+ if (aEvent._domDispatchOnly) {
+ return aTarget.dispatchEvent(event);
+ }
+
+ var utils = _getDOMWindowUtils(aWindow);
+ return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event);
+}
+
+/**
+ * Send the char aChar to the focused element. This method handles casing of
+ * chars (sends the right charcode, and sends a shift key for uppercase chars).
+ * No other modifiers are handled at this point.
+ *
+ * For now this method only works for ASCII characters and emulates the shift
+ * key state on US keyboard layout.
+ */
+function sendChar(aChar, aWindow) {
+ var hasShift;
+ // Emulate US keyboard layout for the shiftKey state.
+ switch (aChar) {
+ case "!":
+ case "@":
+ case "#":
+ case "$":
+ case "%":
+ case "^":
+ case "&":
+ case "*":
+ case "(":
+ case ")":
+ case "_":
+ case "+":
+ case "{":
+ case "}":
+ case ":":
+ case '"':
+ case "|":
+ case "<":
+ case ">":
+ case "?":
+ hasShift = true;
+ break;
+ default:
+ hasShift =
+ aChar.toLowerCase() != aChar.toUpperCase() &&
+ aChar == aChar.toUpperCase();
+ break;
+ }
+ synthesizeKey(aChar, { shiftKey: hasShift }, aWindow);
+}
+
+/**
+ * Send the string aStr to the focused element.
+ *
+ * For now this method only works for ASCII characters and emulates the shift
+ * key state on US keyboard layout.
+ */
+function sendString(aStr, aWindow) {
+ for (var i = 0; i < aStr.length; ++i) {
+ sendChar(aStr.charAt(i), aWindow);
+ }
+}
+
+/**
+ * Send the non-character key aKey to the focused node.
+ * The name of the key should be the part that comes after ``DOM_VK_`` in the
+ * KeyEvent constant name for this key.
+ * No modifiers are handled at this point.
+ */
+function sendKey(aKey, aWindow) {
+ var keyName = "VK_" + aKey.toUpperCase();
+ synthesizeKey(keyName, { shiftKey: false }, aWindow);
+}
+
+/**
+ * Parse the key modifier flags from aEvent. Used to share code between
+ * synthesizeMouse and synthesizeKey.
+ */
+function _parseModifiers(aEvent, aWindow = window) {
+ var nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils;
+ var mval = 0;
+ if (aEvent.shiftKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_SHIFT;
+ }
+ if (aEvent.ctrlKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_CONTROL;
+ }
+ if (aEvent.altKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_ALT;
+ }
+ if (aEvent.metaKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_META;
+ }
+ if (aEvent.accelKey) {
+ mval |= _EU_isMac(aWindow)
+ ? nsIDOMWindowUtils.MODIFIER_META
+ : nsIDOMWindowUtils.MODIFIER_CONTROL;
+ }
+ if (aEvent.altGrKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH;
+ }
+ if (aEvent.capsLockKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK;
+ }
+ if (aEvent.fnKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_FN;
+ }
+ if (aEvent.fnLockKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_FNLOCK;
+ }
+ if (aEvent.numLockKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK;
+ }
+ if (aEvent.scrollLockKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK;
+ }
+ if (aEvent.symbolKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_SYMBOL;
+ }
+ if (aEvent.symbolLockKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK;
+ }
+ if (aEvent.osKey) {
+ mval |= nsIDOMWindowUtils.MODIFIER_OS;
+ }
+
+ return mval;
+}
+
+/**
+ * Synthesize a mouse event on a target. The actual client point is determined
+ * by taking the aTarget's client box and offseting it by aOffsetX and
+ * aOffsetY. This allows mouse clicks to be simulated by calling this method.
+ *
+ * aEvent is an object which may contain the properties:
+ * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`,
+ * `button`, `type`.
+ * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`.
+ *
+ * If the type is specified, an mouse event of that type is fired. Otherwise,
+ * a mousedown followed by a mouseup is performed.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ *
+ * Returns whether the event had preventDefault() called on it.
+ */
+function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
+ var rect = aTarget.getBoundingClientRect();
+ return synthesizeMouseAtPoint(
+ rect.left + aOffsetX,
+ rect.top + aOffsetY,
+ aEvent,
+ aWindow
+ );
+}
+function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
+ var rect = aTarget.getBoundingClientRect();
+ return synthesizeTouchAtPoint(
+ rect.left + aOffsetX,
+ rect.top + aOffsetY,
+ aEvent,
+ aWindow
+ );
+}
+
+/*
+ * Synthesize a mouse event at a particular point in aWindow.
+ *
+ * aEvent is an object which may contain the properties:
+ * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`,
+ * `button`, `type`.
+ * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`.
+ *
+ * If the type is specified, an mouse event of that type is fired. Otherwise,
+ * a mousedown followed by a mouseup is performed.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeMouseAtPoint(left, top, aEvent, aWindow = window) {
+ var utils = _getDOMWindowUtils(aWindow);
+ var defaultPrevented = false;
+
+ if (utils) {
+ var button = computeButton(aEvent);
+ var clickCount = aEvent.clickCount || 1;
+ var modifiers = _parseModifiers(aEvent, aWindow);
+ var pressure = "pressure" in aEvent ? aEvent.pressure : 0;
+
+ // aWindow might be cross-origin from us.
+ var MouseEvent = _EU_maybeWrap(aWindow).MouseEvent;
+
+ // Default source to mouse.
+ var inputSource =
+ "inputSource" in aEvent
+ ? aEvent.inputSource
+ : MouseEvent.MOZ_SOURCE_MOUSE;
+ // Compute a pointerId if needed.
+ var id;
+ if ("id" in aEvent) {
+ id = aEvent.id;
+ } else {
+ var isFromPen = inputSource === MouseEvent.MOZ_SOURCE_PEN;
+ id = isFromPen
+ ? utils.DEFAULT_PEN_POINTER_ID
+ : utils.DEFAULT_MOUSE_POINTER_ID;
+ }
+
+ var isDOMEventSynthesized =
+ "isSynthesized" in aEvent ? aEvent.isSynthesized : true;
+ var isWidgetEventSynthesized =
+ "isWidgetEventSynthesized" in aEvent
+ ? aEvent.isWidgetEventSynthesized
+ : false;
+ if ("type" in aEvent && aEvent.type) {
+ defaultPrevented = utils.sendMouseEvent(
+ aEvent.type,
+ left,
+ top,
+ button,
+ clickCount,
+ modifiers,
+ false,
+ pressure,
+ inputSource,
+ isDOMEventSynthesized,
+ isWidgetEventSynthesized,
+ computeButtons(aEvent, utils),
+ id
+ );
+ } else {
+ utils.sendMouseEvent(
+ "mousedown",
+ left,
+ top,
+ button,
+ clickCount,
+ modifiers,
+ false,
+ pressure,
+ inputSource,
+ isDOMEventSynthesized,
+ isWidgetEventSynthesized,
+ computeButtons(Object.assign({ type: "mousedown" }, aEvent), utils),
+ id
+ );
+ utils.sendMouseEvent(
+ "mouseup",
+ left,
+ top,
+ button,
+ clickCount,
+ modifiers,
+ false,
+ pressure,
+ inputSource,
+ isDOMEventSynthesized,
+ isWidgetEventSynthesized,
+ computeButtons(Object.assign({ type: "mouseup" }, aEvent), utils),
+ id
+ );
+ }
+ }
+
+ return defaultPrevented;
+}
+
+function synthesizeTouchAtPoint(left, top, aEvent, aWindow = window) {
+ var utils = _getDOMWindowUtils(aWindow);
+ let defaultPrevented = false;
+
+ if (utils) {
+ var id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID;
+ var rx = aEvent.rx || 1;
+ var ry = aEvent.ry || 1;
+ var angle = aEvent.angle || 0;
+ var force = aEvent.force || (aEvent.type === "touchend" ? 0 : 1);
+ var tiltX = aEvent.tiltX || 0;
+ var tiltY = aEvent.tiltY || 0;
+ var twist = aEvent.twist || 0;
+ var modifiers = _parseModifiers(aEvent, aWindow);
+
+ if ("type" in aEvent && aEvent.type) {
+ defaultPrevented = utils.sendTouchEvent(
+ aEvent.type,
+ [id],
+ [left],
+ [top],
+ [rx],
+ [ry],
+ [angle],
+ [force],
+ [tiltX],
+ [tiltY],
+ [twist],
+ modifiers
+ );
+ } else {
+ utils.sendTouchEvent(
+ "touchstart",
+ [id],
+ [left],
+ [top],
+ [rx],
+ [ry],
+ [angle],
+ [force],
+ [tiltX],
+ [tiltY],
+ [twist],
+ modifiers
+ );
+ utils.sendTouchEvent(
+ "touchend",
+ [id],
+ [left],
+ [top],
+ [rx],
+ [ry],
+ [angle],
+ [force],
+ [tiltX],
+ [tiltY],
+ [twist],
+ modifiers
+ );
+ }
+ }
+ return defaultPrevented;
+}
+
+// Call synthesizeMouse with coordinates at the center of aTarget.
+function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) {
+ var rect = aTarget.getBoundingClientRect();
+ return synthesizeMouse(
+ aTarget,
+ rect.width / 2,
+ rect.height / 2,
+ aEvent,
+ aWindow
+ );
+}
+function synthesizeTouchAtCenter(aTarget, aEvent, aWindow) {
+ var rect = aTarget.getBoundingClientRect();
+ synthesizeTouchAtPoint(
+ rect.left + rect.width / 2,
+ rect.top + rect.height / 2,
+ aEvent,
+ aWindow
+ );
+}
+
+/**
+ * Synthesize a wheel event without flush layout at a particular point in
+ * aWindow.
+ *
+ * aEvent is an object which may contain the properties:
+ * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ,
+ * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum,
+ * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX,
+ * expectedOverflowDeltaY
+ *
+ * deltaMode must be defined, others are ok even if undefined.
+ *
+ * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The
+ * value is just checked as 0 or positive or negative.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeWheelAtPoint(aLeft, aTop, aEvent, aWindow = window) {
+ var utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return;
+ }
+
+ var modifiers = _parseModifiers(aEvent, aWindow);
+ var options = 0;
+ if (aEvent.isNoLineOrPageDelta) {
+ options |= utils.WHEEL_EVENT_CAUSED_BY_NO_LINE_OR_PAGE_DELTA_DEVICE;
+ }
+ if (aEvent.isMomentum) {
+ options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM;
+ }
+ if (aEvent.isCustomizedByPrefs) {
+ options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS;
+ }
+ if (typeof aEvent.expectedOverflowDeltaX !== "undefined") {
+ if (aEvent.expectedOverflowDeltaX === 0) {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO;
+ } else if (aEvent.expectedOverflowDeltaX > 0) {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE;
+ } else {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE;
+ }
+ }
+ if (typeof aEvent.expectedOverflowDeltaY !== "undefined") {
+ if (aEvent.expectedOverflowDeltaY === 0) {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO;
+ } else if (aEvent.expectedOverflowDeltaY > 0) {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE;
+ } else {
+ options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE;
+ }
+ }
+
+ // Avoid the JS warnings "reference to undefined property"
+ if (!aEvent.deltaX) {
+ aEvent.deltaX = 0;
+ }
+ if (!aEvent.deltaY) {
+ aEvent.deltaY = 0;
+ }
+ if (!aEvent.deltaZ) {
+ aEvent.deltaZ = 0;
+ }
+
+ var lineOrPageDeltaX =
+ // eslint-disable-next-line no-nested-ternary
+ aEvent.lineOrPageDeltaX != null
+ ? aEvent.lineOrPageDeltaX
+ : aEvent.deltaX > 0
+ ? Math.floor(aEvent.deltaX)
+ : Math.ceil(aEvent.deltaX);
+ var lineOrPageDeltaY =
+ // eslint-disable-next-line no-nested-ternary
+ aEvent.lineOrPageDeltaY != null
+ ? aEvent.lineOrPageDeltaY
+ : aEvent.deltaY > 0
+ ? Math.floor(aEvent.deltaY)
+ : Math.ceil(aEvent.deltaY);
+ utils.sendWheelEvent(
+ aLeft,
+ aTop,
+ aEvent.deltaX,
+ aEvent.deltaY,
+ aEvent.deltaZ,
+ aEvent.deltaMode,
+ modifiers,
+ lineOrPageDeltaX,
+ lineOrPageDeltaY,
+ options
+ );
+}
+
+/**
+ * Synthesize a wheel event on a target. The actual client point is determined
+ * by taking the aTarget's client box and offseting it by aOffsetX and
+ * aOffsetY.
+ *
+ * aEvent is an object which may contain the properties:
+ * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ,
+ * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum,
+ * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX,
+ * expectedOverflowDeltaY
+ *
+ * deltaMode must be defined, others are ok even if undefined.
+ *
+ * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The
+ * value is just checked as 0 or positive or negative.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
+ var rect = aTarget.getBoundingClientRect();
+ synthesizeWheelAtPoint(
+ rect.left + aOffsetX,
+ rect.top + aOffsetY,
+ aEvent,
+ aWindow
+ );
+}
+
+const _FlushModes = {
+ FLUSH: 0,
+ NOFLUSH: 1,
+};
+
+function _sendWheelAndPaint(
+ aTarget,
+ aOffsetX,
+ aOffsetY,
+ aEvent,
+ aCallback,
+ aFlushMode = _FlushModes.FLUSH,
+ aWindow = window
+) {
+ var utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return;
+ }
+
+ if (utils.isMozAfterPaintPending) {
+ // If a paint is pending, then APZ may be waiting for a scroll acknowledgement
+ // from the content thread. If we send a wheel event now, it could be ignored
+ // by APZ (or its scroll offset could be overridden). To avoid problems we
+ // just wait for the paint to complete.
+ aWindow.waitForAllPaintsFlushed(function () {
+ _sendWheelAndPaint(
+ aTarget,
+ aOffsetX,
+ aOffsetY,
+ aEvent,
+ aCallback,
+ aFlushMode,
+ aWindow
+ );
+ });
+ return;
+ }
+
+ var onwheel = function () {
+ SpecialPowers.removeSystemEventListener(window, "wheel", onwheel);
+
+ // Wait one frame since the wheel event has not caused a refresh observer
+ // to be added yet.
+ setTimeout(function () {
+ utils.advanceTimeAndRefresh(1000);
+
+ if (!aCallback) {
+ utils.advanceTimeAndRefresh(0);
+ return;
+ }
+
+ var waitForPaints = function () {
+ SpecialPowers.Services.obs.removeObserver(
+ waitForPaints,
+ "apz-repaints-flushed"
+ );
+ aWindow.waitForAllPaintsFlushed(function () {
+ utils.restoreNormalRefresh();
+ aCallback();
+ });
+ };
+
+ SpecialPowers.Services.obs.addObserver(
+ waitForPaints,
+ "apz-repaints-flushed"
+ );
+ if (!utils.flushApzRepaints(aWindow)) {
+ waitForPaints();
+ }
+ }, 0);
+ };
+
+ // Listen for the system wheel event, because it happens after all of
+ // the other wheel events, including legacy events.
+ SpecialPowers.addSystemEventListener(aWindow, "wheel", onwheel);
+ if (aFlushMode === _FlushModes.FLUSH) {
+ synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
+ } else {
+ synthesizeWheelAtPoint(aOffsetX, aOffsetY, aEvent, aWindow);
+ }
+}
+
+/**
+ * This is a wrapper around synthesizeWheel that waits for the wheel event
+ * to be dispatched and for the subsequent layout/paints to be flushed.
+ *
+ * This requires including paint_listener.js. Tests must call
+ * DOMWindowUtils.restoreNormalRefresh() before finishing, if they use this
+ * function.
+ *
+ * If no callback is provided, the caller is assumed to have its own method of
+ * determining scroll completion and the refresh driver is not automatically
+ * restored.
+ */
+function sendWheelAndPaint(
+ aTarget,
+ aOffsetX,
+ aOffsetY,
+ aEvent,
+ aCallback,
+ aWindow = window
+) {
+ _sendWheelAndPaint(
+ aTarget,
+ aOffsetX,
+ aOffsetY,
+ aEvent,
+ aCallback,
+ _FlushModes.FLUSH,
+ aWindow
+ );
+}
+
+/**
+ * Similar to sendWheelAndPaint but without flushing layout for obtaining
+ * ``aTarget`` position in ``aWindow`` before sending the wheel event.
+ * ``aOffsetX`` and ``aOffsetY`` should be offsets against aWindow.
+ */
+function sendWheelAndPaintNoFlush(
+ aTarget,
+ aOffsetX,
+ aOffsetY,
+ aEvent,
+ aCallback,
+ aWindow = window
+) {
+ _sendWheelAndPaint(
+ aTarget,
+ aOffsetX,
+ aOffsetY,
+ aEvent,
+ aCallback,
+ _FlushModes.NOFLUSH,
+ aWindow
+ );
+}
+
+function synthesizeNativeTapAtCenter(
+ aTarget,
+ aLongTap = false,
+ aCallback = null,
+ aWindow = window
+) {
+ let rect = aTarget.getBoundingClientRect();
+ return synthesizeNativeTap(
+ aTarget,
+ rect.width / 2,
+ rect.height / 2,
+ aLongTap,
+ aCallback,
+ aWindow
+ );
+}
+
+function synthesizeNativeTap(
+ aTarget,
+ aOffsetX,
+ aOffsetY,
+ aLongTap = false,
+ aCallback = null,
+ aWindow = window
+) {
+ let utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return;
+ }
+
+ let scale = aWindow.devicePixelRatio;
+ let rect = aTarget.getBoundingClientRect();
+ let x = (aWindow.mozInnerScreenX + rect.left + aOffsetX) * scale;
+ let y = (aWindow.mozInnerScreenY + rect.top + aOffsetY) * scale;
+
+ let observer = {
+ observe: (subject, topic, data) => {
+ if (aCallback && topic == "mouseevent") {
+ aCallback(data);
+ }
+ },
+ };
+ utils.sendNativeTouchTap(x, y, aLongTap, observer);
+}
+
+/**
+ * Similar to synthesizeMouse but generates a native widget level event
+ * (so will actually move the "real" mouse cursor etc. Be careful because
+ * this can impact later code as well! (e.g. with hover states etc.)
+ *
+ * @description There are 3 mutually exclusive ways of indicating the location of the
+ * mouse event: set ``atCenter``, or pass ``offsetX`` and ``offsetY``,
+ * or pass ``screenX`` and ``screenY``. Do not attempt to mix these.
+ *
+ * @param {object} aParams
+ * @param {string} aParams.type "click", "mousedown", "mouseup" or "mousemove"
+ * @param {Element} aParams.target Origin of offsetX and offsetY, must be an element
+ * @param {Boolean} [aParams.atCenter]
+ * Instead of offsetX/Y, synthesize the event at center of `target`.
+ * @param {Number} [aParams.offsetX]
+ * X offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel")
+ * @param {Number} [aParams.offsetY]
+ * Y offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel")
+ * @param {Number} [aParams.screenX]
+ * X offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"),
+ * Neither offsetX/Y nor atCenter must be set if this is set.
+ * @param {Number} [aParams.screenY]
+ * Y offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"),
+ * Neither offsetX/Y nor atCenter must be set if this is set.
+ * @param {String} [aParams.scale="screenPixelsPerCSSPixel"]
+ * If scale is "screenPixelsPerCSSPixel", devicePixelRatio will be used.
+ * If scale is "inScreenPixels", clientX/Y nor scaleX/Y are not adjusted with screenPixelsPerCSSPixel.
+ * @param {Number} [aParams.button=0]
+ * Defaults to 0, if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button
+ * @param {Object} [aParams.modifiers={}]
+ * Active modifiers, see `_parseNativeModifiers`
+ * @param {Window} [aParams.win=window]
+ * The window to use its utils. Defaults to the window in which EventUtils.js is running.
+ * @param {Element} [aParams.elementOnWidget=target]
+ * Defaults to target. If element under the point is in another widget from target's widget,
+ * e.g., when it's in a XUL <panel>, specify this.
+ */
+function synthesizeNativeMouseEvent(aParams, aCallback = null) {
+ const {
+ type,
+ target,
+ offsetX,
+ offsetY,
+ atCenter,
+ screenX,
+ screenY,
+ scale = "screenPixelsPerCSSPixel",
+ button = 0,
+ modifiers = {},
+ win = window,
+ elementOnWidget = target,
+ } = aParams;
+ if (atCenter) {
+ if (offsetX != undefined || offsetY != undefined) {
+ throw Error(
+ `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
+ );
+ }
+ if (screenX != undefined || screenY != undefined) {
+ throw Error(
+ `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
+ );
+ }
+ if (!target) {
+ throw Error("atCenter is specified, but target is not specified");
+ }
+ } else if (offsetX != undefined && offsetY != undefined) {
+ if (screenX != undefined || screenY != undefined) {
+ throw Error(
+ `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
+ );
+ }
+ if (!target) {
+ throw Error(
+ "offsetX and offsetY are specified, but target is not specified"
+ );
+ }
+ } else if (screenX != undefined && screenY != undefined) {
+ if (offsetX != undefined || offsetY != undefined) {
+ throw Error(
+ `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
+ );
+ }
+ }
+ const utils = _getDOMWindowUtils(win);
+ if (!utils) {
+ return;
+ }
+
+ const rect = target?.getBoundingClientRect();
+ let resolution = 1.0;
+ try {
+ resolution = _getDOMWindowUtils(win.top).getResolution();
+ } catch (e) {
+ // XXX How to get mobile viewport scale on Fission+xorigin since
+ // window.top access isn't allowed due to cross-origin?
+ }
+ const scaleValue = (() => {
+ if (scale === "inScreenPixels") {
+ return 1.0;
+ }
+ if (scale === "screenPixelsPerCSSPixel") {
+ return win.devicePixelRatio;
+ }
+ throw Error(`invalid scale value (${scale}) is specified`);
+ })();
+ // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546),
+ // so use window.top's mozInnerScreen. But this won't work fission+xorigin
+ // with mobile viewport until mozInnerScreen returns valid value with
+ // scale.
+ const x = (() => {
+ if (screenX != undefined) {
+ return screenX * scaleValue;
+ }
+ let winInnerOffsetX = win.mozInnerScreenX;
+ try {
+ winInnerOffsetX =
+ win.top.mozInnerScreenX +
+ (win.mozInnerScreenX - win.top.mozInnerScreenX) * resolution;
+ } catch (e) {
+ // XXX fission+xorigin test throws permission denied since win.top is
+ // cross-origin.
+ }
+ return (
+ (((atCenter ? rect.width / 2 : offsetX) + rect.left) * resolution +
+ winInnerOffsetX) *
+ scaleValue
+ );
+ })();
+ const y = (() => {
+ if (screenY != undefined) {
+ return screenY * scaleValue;
+ }
+ let winInnerOffsetY = win.mozInnerScreenY;
+ try {
+ winInnerOffsetY =
+ win.top.mozInnerScreenY +
+ (win.mozInnerScreenY - win.top.mozInnerScreenY) * resolution;
+ } catch (e) {
+ // XXX fission+xorigin test throws permission denied since win.top is
+ // cross-origin.
+ }
+ return (
+ (((atCenter ? rect.height / 2 : offsetY) + rect.top) * resolution +
+ winInnerOffsetY) *
+ scaleValue
+ );
+ })();
+ const modifierFlags = _parseNativeModifiers(modifiers);
+
+ const observer = {
+ observe: (subject, topic, data) => {
+ if (aCallback && topic == "mouseevent") {
+ aCallback(data);
+ }
+ },
+ };
+ if (type === "click") {
+ utils.sendNativeMouseEvent(
+ x,
+ y,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN,
+ button,
+ modifierFlags,
+ elementOnWidget,
+ function () {
+ utils.sendNativeMouseEvent(
+ x,
+ y,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP,
+ button,
+ modifierFlags,
+ elementOnWidget,
+ observer
+ );
+ }
+ );
+ return;
+ }
+ utils.sendNativeMouseEvent(
+ x,
+ y,
+ (() => {
+ switch (type) {
+ case "mousedown":
+ return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN;
+ case "mouseup":
+ return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP;
+ case "mousemove":
+ return utils.NATIVE_MOUSE_MESSAGE_MOVE;
+ default:
+ throw Error(`Invalid type is specified: ${type}`);
+ }
+ })(),
+ button,
+ modifierFlags,
+ elementOnWidget,
+ observer
+ );
+}
+
+function promiseNativeMouseEvent(aParams) {
+ return new Promise(resolve => synthesizeNativeMouseEvent(aParams, resolve));
+}
+
+function synthesizeNativeMouseEventAndWaitForEvent(aParams, aCallback) {
+ const listener = aParams.eventTargetToListen || aParams.target;
+ const eventType = aParams.eventTypeToWait || aParams.type;
+ listener.addEventListener(eventType, aCallback, {
+ capture: true,
+ once: true,
+ });
+ synthesizeNativeMouseEvent(aParams);
+}
+
+function promiseNativeMouseEventAndWaitForEvent(aParams) {
+ return new Promise(resolve =>
+ synthesizeNativeMouseEventAndWaitForEvent(aParams, resolve)
+ );
+}
+
+/**
+ * This is a wrapper around synthesizeNativeMouseEvent that waits for the mouse
+ * event to be dispatched to the target content.
+ *
+ * This API is supposed to be used in those test cases that synthesize some
+ * input events to chrome process and have some checks in content.
+ */
+function synthesizeAndWaitNativeMouseMove(
+ aTarget,
+ aOffsetX,
+ aOffsetY,
+ aCallback,
+ aWindow = window
+) {
+ let browser = gBrowser.selectedTab.linkedBrowser;
+ let mm = browser.messageManager;
+ let { ContentTask } = _EU_ChromeUtils.importESModule(
+ "resource://testing-common/ContentTask.sys.mjs"
+ );
+
+ let eventRegisteredPromise = new Promise(resolve => {
+ mm.addMessageListener(
+ "Test:MouseMoveRegistered",
+ function processed(message) {
+ mm.removeMessageListener("Test:MouseMoveRegistered", processed);
+ resolve();
+ }
+ );
+ });
+ let eventReceivedPromise = ContentTask.spawn(
+ browser,
+ [aOffsetX, aOffsetY],
+ ([clientX, clientY]) => {
+ return new Promise(resolve => {
+ addEventListener("mousemove", function onMouseMoveEvent(e) {
+ if (e.clientX == clientX && e.clientY == clientY) {
+ removeEventListener("mousemove", onMouseMoveEvent);
+ resolve();
+ }
+ });
+ sendAsyncMessage("Test:MouseMoveRegistered");
+ });
+ }
+ );
+ eventRegisteredPromise.then(() => {
+ synthesizeNativeMouseEvent({
+ type: "mousemove",
+ target: aTarget,
+ offsetX: aOffsetX,
+ offsetY: aOffsetY,
+ win: aWindow,
+ });
+ });
+ return eventReceivedPromise;
+}
+
+/**
+ * Synthesize a key event. It is targeted at whatever would be targeted by an
+ * actual keypress by the user, typically the focused element.
+ *
+ * @param {String} aKey
+ * Should be either:
+ *
+ * - key value (recommended). If you specify a non-printable key name,
+ * prepend the ``KEY_`` prefix. Otherwise, specifying a printable key, the
+ * key value should be specified.
+ *
+ * - keyCode name starting with ``VK_`` (e.g., ``VK_RETURN``). This is available
+ * only for compatibility with legacy API. Don't use this with new tests.
+ *
+ * @param {Object} [aEvent]
+ * Optional event object with more specifics about the key event to
+ * synthesize.
+ * @param {String} [aEvent.code]
+ * If you don't specify this explicitly, it'll be guessed from aKey
+ * of US keyboard layout. Note that this value may be different
+ * between browsers. For example, "Insert" is never set only on
+ * macOS since actual key operation won't cause this code value.
+ * In such case, the value becomes empty string.
+ * If you need to emulate non-US keyboard layout or virtual keyboard
+ * which doesn't emulate hardware key input, you should set this value
+ * to empty string explicitly.
+ * @param {Number} [aEvent.repeat]
+ * If you emulate auto-repeat, you should set the count of repeat.
+ * This method will automatically synthesize keydown (and keypress).
+ * @param {*} aEvent.location
+ * If you want to specify this, you can specify this explicitly.
+ * However, if you don't specify this value, it will be computed
+ * from code value.
+ * @param {String} aEvent.type
+ * Basically, you shouldn't specify this. Then, this function will
+ * synthesize keydown (, keypress) and keyup.
+ * If keydown is specified, this only fires keydown (and keypress if
+ * it should be fired).
+ * If keyup is specified, this only fires keyup.
+ * @param {Number} aEvent.keyCode
+ * Must be 0 - 255 (0xFF). If this is specified explicitly,
+ * .keyCode value is initialized with this value.
+ * @param {Window} aWindow
+ * Is optional and defaults to the current window object.
+ * @param {Function} aCallback
+ * Is optional and can be used to receive notifications from TIP.
+ *
+ * @description
+ * ``accelKey``, ``altKey``, ``altGraphKey``, ``ctrlKey``, ``capsLockKey``,
+ * ``fnKey``, ``fnLockKey``, ``numLockKey``, ``metaKey``, ``osKey``,
+ * ``scrollLockKey``, ``shiftKey``, ``symbolKey``, ``symbolLockKey``
+ * Basically, you shouldn't use these attributes. nsITextInputProcessor
+ * manages modifier key state when you synthesize modifier key events.
+ * However, if some of these attributes are true, this function activates
+ * the modifiers only during dispatching the key events.
+ * Note that if some of these values are false, they are ignored (i.e.,
+ * not inactivated with this function).
+ *
+ */
+function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) {
+ var event = aEvent === undefined || aEvent === null ? {} : aEvent;
+
+ var TIP = _getTIP(aWindow, aCallback);
+ if (!TIP) {
+ return;
+ }
+ var KeyboardEvent = _getKeyboardEvent(aWindow);
+ var modifiers = _emulateToActivateModifiers(TIP, event, aWindow);
+ var keyEventDict = _createKeyboardEventDictionary(aKey, event, TIP, aWindow);
+ var keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
+ var dispatchKeydown =
+ !("type" in event) || event.type === "keydown" || !event.type;
+ var dispatchKeyup =
+ !("type" in event) || event.type === "keyup" || !event.type;
+
+ try {
+ if (dispatchKeydown) {
+ TIP.keydown(keyEvent, keyEventDict.flags);
+ if ("repeat" in event && event.repeat > 1) {
+ keyEventDict.dictionary.repeat = true;
+ var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary);
+ for (var i = 1; i < event.repeat; i++) {
+ TIP.keydown(repeatedKeyEvent, keyEventDict.flags);
+ }
+ }
+ }
+ if (dispatchKeyup) {
+ TIP.keyup(keyEvent, keyEventDict.flags);
+ }
+ } finally {
+ _emulateToInactivateModifiers(TIP, modifiers, aWindow);
+ }
+}
+
+/**
+ * This is a wrapper around synthesizeKey that waits for the key event to be
+ * dispatched to the target content. It returns a promise which is resolved
+ * when the content receives the key event.
+ *
+ * This API is supposed to be used in those test cases that synthesize some
+ * input events to chrome process and have some checks in content.
+ */
+function synthesizeAndWaitKey(
+ aKey,
+ aEvent,
+ aWindow = window,
+ checkBeforeSynthesize,
+ checkAfterSynthesize
+) {
+ let browser = gBrowser.selectedTab.linkedBrowser;
+ let mm = browser.messageManager;
+ let keyCode = _createKeyboardEventDictionary(aKey, aEvent, null, aWindow)
+ .dictionary.keyCode;
+ let { ContentTask } = _EU_ChromeUtils.importESModule(
+ "resource://testing-common/ContentTask.sys.mjs"
+ );
+
+ let keyRegisteredPromise = new Promise(resolve => {
+ mm.addMessageListener("Test:KeyRegistered", function processed(message) {
+ mm.removeMessageListener("Test:KeyRegistered", processed);
+ resolve();
+ });
+ });
+ // eslint-disable-next-line no-shadow
+ let keyReceivedPromise = ContentTask.spawn(browser, keyCode, keyCode => {
+ return new Promise(resolve => {
+ addEventListener("keyup", function onKeyEvent(e) {
+ if (e.keyCode == keyCode) {
+ removeEventListener("keyup", onKeyEvent);
+ resolve();
+ }
+ });
+ sendAsyncMessage("Test:KeyRegistered");
+ });
+ });
+ keyRegisteredPromise.then(() => {
+ if (checkBeforeSynthesize) {
+ checkBeforeSynthesize();
+ }
+ synthesizeKey(aKey, aEvent, aWindow);
+ if (checkAfterSynthesize) {
+ checkAfterSynthesize();
+ }
+ });
+ return keyReceivedPromise;
+}
+
+function _parseNativeModifiers(aModifiers, aWindow = window) {
+ let modifiers = 0;
+ if (aModifiers.capsLockKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK;
+ }
+ if (aModifiers.numLockKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK;
+ }
+ if (aModifiers.shiftKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT;
+ }
+ if (aModifiers.shiftRightKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT;
+ }
+ if (aModifiers.ctrlKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
+ }
+ if (aModifiers.ctrlRightKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
+ }
+ if (aModifiers.altKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT;
+ }
+ if (aModifiers.altRightKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT;
+ }
+ if (aModifiers.metaKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT;
+ }
+ if (aModifiers.metaRightKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT;
+ }
+ if (aModifiers.helpKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP;
+ }
+ if (aModifiers.fnKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION;
+ }
+ if (aModifiers.numericKeyPadKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD;
+ }
+
+ if (aModifiers.accelKey) {
+ modifiers |= _EU_isMac(aWindow)
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT
+ : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
+ }
+ if (aModifiers.accelRightKey) {
+ modifiers |= _EU_isMac(aWindow)
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT
+ : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
+ }
+ if (aModifiers.altGrKey) {
+ modifiers |= _EU_isMac(aWindow)
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT
+ : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH;
+ }
+ return modifiers;
+}
+
+// Mac: Any unused number is okay for adding new keyboard layout.
+// When you add new keyboard layout here, you need to modify
+// TISInputSourceWrapper::InitByLayoutID().
+// Win: These constants can be found by inspecting registry keys under
+// HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Keyboard Layouts
+
+const KEYBOARD_LAYOUT_ARABIC = {
+ name: "Arabic",
+ Mac: 6,
+ Win: 0x00000401,
+ hasAltGrOnWin: false,
+};
+const KEYBOARD_LAYOUT_ARABIC_PC = {
+ name: "Arabic - PC",
+ Mac: 7,
+ Win: null,
+ hasAltGrOnWin: false,
+};
+const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = {
+ name: "Brazilian ABNT",
+ Mac: null,
+ Win: 0x00000416,
+ hasAltGrOnWin: true,
+};
+const KEYBOARD_LAYOUT_DVORAK_QWERTY = {
+ name: "Dvorak-QWERTY",
+ Mac: 4,
+ Win: null,
+ hasAltGrOnWin: false,
+};
+const KEYBOARD_LAYOUT_EN_US = {
+ name: "US",
+ Mac: 0,
+ Win: 0x00000409,
+ hasAltGrOnWin: false,
+};
+const KEYBOARD_LAYOUT_FRENCH = {
+ name: "French",
+ Mac: 8,
+ Win: 0x0000040c,
+ hasAltGrOnWin: true,
+};
+const KEYBOARD_LAYOUT_GREEK = {
+ name: "Greek",
+ Mac: 1,
+ Win: 0x00000408,
+ hasAltGrOnWin: true,
+};
+const KEYBOARD_LAYOUT_GERMAN = {
+ name: "German",
+ Mac: 2,
+ Win: 0x00000407,
+ hasAltGrOnWin: true,
+};
+const KEYBOARD_LAYOUT_HEBREW = {
+ name: "Hebrew",
+ Mac: 9,
+ Win: 0x0000040d,
+ hasAltGrOnWin: true,
+};
+const KEYBOARD_LAYOUT_JAPANESE = {
+ name: "Japanese",
+ Mac: null,
+ Win: 0x00000411,
+ hasAltGrOnWin: false,
+};
+const KEYBOARD_LAYOUT_KHMER = {
+ name: "Khmer",
+ Mac: null,
+ Win: 0x00000453,
+ hasAltGrOnWin: true,
+}; // available on Win7 or later.
+const KEYBOARD_LAYOUT_LITHUANIAN = {
+ name: "Lithuanian",
+ Mac: 10,
+ Win: 0x00010427,
+ hasAltGrOnWin: true,
+};
+const KEYBOARD_LAYOUT_NORWEGIAN = {
+ name: "Norwegian",
+ Mac: 11,
+ Win: 0x00000414,
+ hasAltGrOnWin: true,
+};
+const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = {
+ name: "Russian - Mnemonic",
+ Mac: null,
+ Win: 0x00020419,
+ hasAltGrOnWin: true,
+}; // available on Win8 or later.
+const KEYBOARD_LAYOUT_SPANISH = {
+ name: "Spanish",
+ Mac: 12,
+ Win: 0x0000040a,
+ hasAltGrOnWin: true,
+};
+const KEYBOARD_LAYOUT_SWEDISH = {
+ name: "Swedish",
+ Mac: 3,
+ Win: 0x0000041d,
+ hasAltGrOnWin: true,
+};
+const KEYBOARD_LAYOUT_THAI = {
+ name: "Thai",
+ Mac: 5,
+ Win: 0x0002041e,
+ hasAltGrOnWin: false,
+};
+
+/**
+ * synthesizeNativeKey() dispatches native key event on active window.
+ * This is implemented only on Windows and Mac. Note that this function
+ * dispatches the key event asynchronously and returns immediately. If a
+ * callback function is provided, the callback will be called upon
+ * completion of the key dispatch.
+ *
+ * @param aKeyboardLayout One of KEYBOARD_LAYOUT_* defined above.
+ * @param aNativeKeyCode A native keycode value defined in
+ * NativeKeyCodes.js.
+ * @param aModifiers Modifier keys. If no modifire key is pressed,
+ * this must be {}. Otherwise, one or more items
+ * referred in _parseNativeModifiers() must be
+ * true.
+ * @param aChars Specify characters which should be generated
+ * by the key event.
+ * @param aUnmodifiedChars Specify characters of unmodified (except Shift)
+ * aChar value.
+ * @param aCallback If provided, this callback will be invoked
+ * once the native keys have been processed
+ * by Gecko. Will never be called if this
+ * function returns false.
+ * @return True if this function succeed dispatching
+ * native key event. Otherwise, false.
+ */
+
+function synthesizeNativeKey(
+ aKeyboardLayout,
+ aNativeKeyCode,
+ aModifiers,
+ aChars,
+ aUnmodifiedChars,
+ aCallback,
+ aWindow = window
+) {
+ var utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return false;
+ }
+ var nativeKeyboardLayout = null;
+ if (_EU_isMac(aWindow)) {
+ nativeKeyboardLayout = aKeyboardLayout.Mac;
+ } else if (_EU_isWin(aWindow)) {
+ nativeKeyboardLayout = aKeyboardLayout.Win;
+ }
+ if (nativeKeyboardLayout === null) {
+ return false;
+ }
+
+ var observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aCallback && aTopic == "keyevent") {
+ aCallback(aData);
+ }
+ },
+ };
+ utils.sendNativeKeyEvent(
+ nativeKeyboardLayout,
+ aNativeKeyCode,
+ _parseNativeModifiers(aModifiers, aWindow),
+ aChars,
+ aUnmodifiedChars,
+ observer
+ );
+ return true;
+}
+
+var _gSeenEvent = false;
+
+/**
+ * Indicate that an event with an original target of aExpectedTarget and
+ * a type of aExpectedEvent is expected to be fired, or not expected to
+ * be fired.
+ */
+function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName) {
+ if (!aExpectedTarget || !aExpectedEvent) {
+ return null;
+ }
+
+ _gSeenEvent = false;
+
+ var type =
+ aExpectedEvent.charAt(0) == "!"
+ ? aExpectedEvent.substring(1)
+ : aExpectedEvent;
+ var eventHandler = function (event) {
+ var epassed =
+ !_gSeenEvent &&
+ event.originalTarget == aExpectedTarget &&
+ event.type == type;
+ is(
+ epassed,
+ true,
+ aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : "")
+ );
+ _gSeenEvent = true;
+ };
+
+ aExpectedTarget.addEventListener(type, eventHandler);
+ return eventHandler;
+}
+
+/**
+ * Check if the event was fired or not. The event handler aEventHandler
+ * will be removed.
+ */
+function _checkExpectedEvent(
+ aExpectedTarget,
+ aExpectedEvent,
+ aEventHandler,
+ aTestName
+) {
+ if (aEventHandler) {
+ var expectEvent = aExpectedEvent.charAt(0) != "!";
+ var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1);
+ aExpectedTarget.removeEventListener(type, aEventHandler);
+ var desc = type + " event";
+ if (!expectEvent) {
+ desc += " not";
+ }
+ is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired");
+ }
+
+ _gSeenEvent = false;
+}
+
+/**
+ * Similar to synthesizeMouse except that a test is performed to see if an
+ * event is fired at the right target as a result.
+ *
+ * aExpectedTarget - the expected originalTarget of the event.
+ * aExpectedEvent - the expected type of the event, such as 'select'.
+ * aTestName - the test name when outputing results
+ *
+ * To test that an event is not fired, use an expected type preceded by an
+ * exclamation mark, such as '!select'. This might be used to test that a
+ * click on a disabled element doesn't fire certain events for instance.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeMouseExpectEvent(
+ aTarget,
+ aOffsetX,
+ aOffsetY,
+ aEvent,
+ aExpectedTarget,
+ aExpectedEvent,
+ aTestName,
+ aWindow
+) {
+ var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
+ synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
+ _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
+}
+
+/**
+ * Similar to synthesizeKey except that a test is performed to see if an
+ * event is fired at the right target as a result.
+ *
+ * aExpectedTarget - the expected originalTarget of the event.
+ * aExpectedEvent - the expected type of the event, such as 'select'.
+ * aTestName - the test name when outputing results
+ *
+ * To test that an event is not fired, use an expected type preceded by an
+ * exclamation mark, such as '!select'.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeKeyExpectEvent(
+ key,
+ aEvent,
+ aExpectedTarget,
+ aExpectedEvent,
+ aTestName,
+ aWindow
+) {
+ var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
+ synthesizeKey(key, aEvent, aWindow);
+ _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
+}
+
+function disableNonTestMouseEvents(aDisable) {
+ var domutils = _getDOMWindowUtils();
+ domutils.disableNonTestMouseEvents(aDisable);
+}
+
+function _getDOMWindowUtils(aWindow = window) {
+ // Leave this here as something, somewhere, passes a falsy argument
+ // to this, causing the |window| default argument not to get picked up.
+ if (!aWindow) {
+ aWindow = window;
+ }
+
+ // If documentURIObject exists or `window` is a stub object, we're in
+ // a chrome scope, so don't bother trying to go through SpecialPowers.
+ if (!window.document || window.document.documentURIObject) {
+ return aWindow.windowUtils;
+ }
+
+ // we need parent.SpecialPowers for:
+ // layout/base/tests/test_reftests_with_caret.html
+ // chrome: toolkit/content/tests/chrome/test_findbar.xul
+ // chrome: toolkit/content/tests/chrome/test_popup_anchor.xul
+ if ("SpecialPowers" in window && window.SpecialPowers != undefined) {
+ return SpecialPowers.getDOMWindowUtils(aWindow);
+ }
+ if ("SpecialPowers" in parent && parent.SpecialPowers != undefined) {
+ return parent.SpecialPowers.getDOMWindowUtils(aWindow);
+ }
+
+ // TODO: this is assuming we are in chrome space
+ return aWindow.windowUtils;
+}
+
+function _defineConstant(name, value) {
+ Object.defineProperty(this, name, {
+ value,
+ enumerable: true,
+ writable: false,
+ });
+}
+
+const COMPOSITION_ATTR_RAW_CLAUSE =
+ _EU_Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE;
+_defineConstant("COMPOSITION_ATTR_RAW_CLAUSE", COMPOSITION_ATTR_RAW_CLAUSE);
+const COMPOSITION_ATTR_SELECTED_RAW_CLAUSE =
+ _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE;
+_defineConstant(
+ "COMPOSITION_ATTR_SELECTED_RAW_CLAUSE",
+ COMPOSITION_ATTR_SELECTED_RAW_CLAUSE
+);
+const COMPOSITION_ATTR_CONVERTED_CLAUSE =
+ _EU_Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE;
+_defineConstant(
+ "COMPOSITION_ATTR_CONVERTED_CLAUSE",
+ COMPOSITION_ATTR_CONVERTED_CLAUSE
+);
+const COMPOSITION_ATTR_SELECTED_CLAUSE =
+ _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE;
+_defineConstant(
+ "COMPOSITION_ATTR_SELECTED_CLAUSE",
+ COMPOSITION_ATTR_SELECTED_CLAUSE
+);
+
+var TIPMap = new WeakMap();
+
+function _getTIP(aWindow, aCallback) {
+ if (!aWindow) {
+ aWindow = window;
+ }
+ var tip;
+ if (TIPMap.has(aWindow)) {
+ tip = TIPMap.get(aWindow);
+ } else {
+ tip = _EU_Cc["@mozilla.org/text-input-processor;1"].createInstance(
+ _EU_Ci.nsITextInputProcessor
+ );
+ TIPMap.set(aWindow, tip);
+ }
+ if (!tip.beginInputTransactionForTests(aWindow, aCallback)) {
+ tip = null;
+ TIPMap.delete(aWindow);
+ }
+ return tip;
+}
+
+function _getKeyboardEvent(aWindow = window) {
+ if (typeof KeyboardEvent != "undefined") {
+ try {
+ // See if the object can be instantiated; sometimes this yields
+ // 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'.
+ new KeyboardEvent("", {});
+ return KeyboardEvent;
+ } catch (ex) {}
+ }
+ if (typeof content != "undefined" && "KeyboardEvent" in content) {
+ return content.KeyboardEvent;
+ }
+ return aWindow.KeyboardEvent;
+}
+
+// eslint-disable-next-line complexity
+function _guessKeyNameFromKeyCode(aKeyCode, aWindow = window) {
+ var KeyboardEvent = _getKeyboardEvent(aWindow);
+ switch (aKeyCode) {
+ case KeyboardEvent.DOM_VK_CANCEL:
+ return "Cancel";
+ case KeyboardEvent.DOM_VK_HELP:
+ return "Help";
+ case KeyboardEvent.DOM_VK_BACK_SPACE:
+ return "Backspace";
+ case KeyboardEvent.DOM_VK_TAB:
+ return "Tab";
+ case KeyboardEvent.DOM_VK_CLEAR:
+ return "Clear";
+ case KeyboardEvent.DOM_VK_RETURN:
+ return "Enter";
+ case KeyboardEvent.DOM_VK_SHIFT:
+ return "Shift";
+ case KeyboardEvent.DOM_VK_CONTROL:
+ return "Control";
+ case KeyboardEvent.DOM_VK_ALT:
+ return "Alt";
+ case KeyboardEvent.DOM_VK_PAUSE:
+ return "Pause";
+ case KeyboardEvent.DOM_VK_EISU:
+ return "Eisu";
+ case KeyboardEvent.DOM_VK_ESCAPE:
+ return "Escape";
+ case KeyboardEvent.DOM_VK_CONVERT:
+ return "Convert";
+ case KeyboardEvent.DOM_VK_NONCONVERT:
+ return "NonConvert";
+ case KeyboardEvent.DOM_VK_ACCEPT:
+ return "Accept";
+ case KeyboardEvent.DOM_VK_MODECHANGE:
+ return "ModeChange";
+ case KeyboardEvent.DOM_VK_PAGE_UP:
+ return "PageUp";
+ case KeyboardEvent.DOM_VK_PAGE_DOWN:
+ return "PageDown";
+ case KeyboardEvent.DOM_VK_END:
+ return "End";
+ case KeyboardEvent.DOM_VK_HOME:
+ return "Home";
+ case KeyboardEvent.DOM_VK_LEFT:
+ return "ArrowLeft";
+ case KeyboardEvent.DOM_VK_UP:
+ return "ArrowUp";
+ case KeyboardEvent.DOM_VK_RIGHT:
+ return "ArrowRight";
+ case KeyboardEvent.DOM_VK_DOWN:
+ return "ArrowDown";
+ case KeyboardEvent.DOM_VK_SELECT:
+ return "Select";
+ case KeyboardEvent.DOM_VK_PRINT:
+ return "Print";
+ case KeyboardEvent.DOM_VK_EXECUTE:
+ return "Execute";
+ case KeyboardEvent.DOM_VK_PRINTSCREEN:
+ return "PrintScreen";
+ case KeyboardEvent.DOM_VK_INSERT:
+ return "Insert";
+ case KeyboardEvent.DOM_VK_DELETE:
+ return "Delete";
+ case KeyboardEvent.DOM_VK_WIN:
+ return "OS";
+ case KeyboardEvent.DOM_VK_CONTEXT_MENU:
+ return "ContextMenu";
+ case KeyboardEvent.DOM_VK_SLEEP:
+ return "Standby";
+ case KeyboardEvent.DOM_VK_F1:
+ return "F1";
+ case KeyboardEvent.DOM_VK_F2:
+ return "F2";
+ case KeyboardEvent.DOM_VK_F3:
+ return "F3";
+ case KeyboardEvent.DOM_VK_F4:
+ return "F4";
+ case KeyboardEvent.DOM_VK_F5:
+ return "F5";
+ case KeyboardEvent.DOM_VK_F6:
+ return "F6";
+ case KeyboardEvent.DOM_VK_F7:
+ return "F7";
+ case KeyboardEvent.DOM_VK_F8:
+ return "F8";
+ case KeyboardEvent.DOM_VK_F9:
+ return "F9";
+ case KeyboardEvent.DOM_VK_F10:
+ return "F10";
+ case KeyboardEvent.DOM_VK_F11:
+ return "F11";
+ case KeyboardEvent.DOM_VK_F12:
+ return "F12";
+ case KeyboardEvent.DOM_VK_F13:
+ return "F13";
+ case KeyboardEvent.DOM_VK_F14:
+ return "F14";
+ case KeyboardEvent.DOM_VK_F15:
+ return "F15";
+ case KeyboardEvent.DOM_VK_F16:
+ return "F16";
+ case KeyboardEvent.DOM_VK_F17:
+ return "F17";
+ case KeyboardEvent.DOM_VK_F18:
+ return "F18";
+ case KeyboardEvent.DOM_VK_F19:
+ return "F19";
+ case KeyboardEvent.DOM_VK_F20:
+ return "F20";
+ case KeyboardEvent.DOM_VK_F21:
+ return "F21";
+ case KeyboardEvent.DOM_VK_F22:
+ return "F22";
+ case KeyboardEvent.DOM_VK_F23:
+ return "F23";
+ case KeyboardEvent.DOM_VK_F24:
+ return "F24";
+ case KeyboardEvent.DOM_VK_NUM_LOCK:
+ return "NumLock";
+ case KeyboardEvent.DOM_VK_SCROLL_LOCK:
+ return "ScrollLock";
+ case KeyboardEvent.DOM_VK_VOLUME_MUTE:
+ return "AudioVolumeMute";
+ case KeyboardEvent.DOM_VK_VOLUME_DOWN:
+ return "AudioVolumeDown";
+ case KeyboardEvent.DOM_VK_VOLUME_UP:
+ return "AudioVolumeUp";
+ case KeyboardEvent.DOM_VK_META:
+ return "Meta";
+ case KeyboardEvent.DOM_VK_ALTGR:
+ return "AltGraph";
+ case KeyboardEvent.DOM_VK_PROCESSKEY:
+ return "Process";
+ case KeyboardEvent.DOM_VK_ATTN:
+ return "Attn";
+ case KeyboardEvent.DOM_VK_CRSEL:
+ return "CrSel";
+ case KeyboardEvent.DOM_VK_EXSEL:
+ return "ExSel";
+ case KeyboardEvent.DOM_VK_EREOF:
+ return "EraseEof";
+ case KeyboardEvent.DOM_VK_PLAY:
+ return "Play";
+ default:
+ return "Unidentified";
+ }
+}
+
+function _createKeyboardEventDictionary(
+ aKey,
+ aKeyEvent,
+ aTIP = null,
+ aWindow = window
+) {
+ var result = { dictionary: null, flags: 0 };
+ var keyCodeIsDefined = "keyCode" in aKeyEvent;
+ var keyCode =
+ keyCodeIsDefined && aKeyEvent.keyCode >= 0 && aKeyEvent.keyCode <= 255
+ ? aKeyEvent.keyCode
+ : 0;
+ var keyName = "Unidentified";
+ var code = aKeyEvent.code;
+ if (!aTIP) {
+ aTIP = _getTIP(aWindow);
+ }
+ if (aKey.indexOf("KEY_") == 0) {
+ keyName = aKey.substr("KEY_".length);
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
+ if (code === undefined) {
+ code = aTIP.computeCodeValueOfNonPrintableKey(
+ keyName,
+ aKeyEvent.location
+ );
+ }
+ } else if (aKey.indexOf("VK_") == 0) {
+ keyCode = _getKeyboardEvent(aWindow)["DOM_" + aKey];
+ if (!keyCode) {
+ throw new Error("Unknown key: " + aKey);
+ }
+ keyName = _guessKeyNameFromKeyCode(keyCode, aWindow);
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
+ if (code === undefined) {
+ code = aTIP.computeCodeValueOfNonPrintableKey(
+ keyName,
+ aKeyEvent.location
+ );
+ }
+ } else if (aKey != "") {
+ keyName = aKey;
+ if (!keyCodeIsDefined) {
+ keyCode = aTIP.guessKeyCodeValueOfPrintableKeyInUSEnglishKeyboardLayout(
+ aKey,
+ aKeyEvent.location
+ );
+ }
+ if (!keyCode) {
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
+ }
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
+ if (code === undefined) {
+ code = aTIP.guessCodeValueOfPrintableKeyInUSEnglishKeyboardLayout(
+ keyName,
+ aKeyEvent.location
+ );
+ }
+ }
+ var locationIsDefined = "location" in aKeyEvent;
+ if (locationIsDefined && aKeyEvent.location === 0) {
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD;
+ }
+ if (aKeyEvent.doNotMarkKeydownAsProcessed) {
+ result.flags |=
+ _EU_Ci.nsITextInputProcessor.KEY_DONT_MARK_KEYDOWN_AS_PROCESSED;
+ }
+ if (aKeyEvent.markKeyupAsProcessed) {
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_MARK_KEYUP_AS_PROCESSED;
+ }
+ result.dictionary = {
+ key: keyName,
+ code,
+ location: locationIsDefined ? aKeyEvent.location : 0,
+ repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false,
+ keyCode,
+ };
+ return result;
+}
+
+function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window) {
+ if (!aKeyEvent) {
+ return null;
+ }
+ var KeyboardEvent = _getKeyboardEvent(aWindow);
+
+ var modifiers = {
+ normal: [
+ { key: "Alt", attr: "altKey" },
+ { key: "AltGraph", attr: "altGraphKey" },
+ { key: "Control", attr: "ctrlKey" },
+ { key: "Fn", attr: "fnKey" },
+ { key: "Meta", attr: "metaKey" },
+ { key: "OS", attr: "osKey" },
+ { key: "Shift", attr: "shiftKey" },
+ { key: "Symbol", attr: "symbolKey" },
+ { key: _EU_isMac(aWindow) ? "Meta" : "Control", attr: "accelKey" },
+ ],
+ lockable: [
+ { key: "CapsLock", attr: "capsLockKey" },
+ { key: "FnLock", attr: "fnLockKey" },
+ { key: "NumLock", attr: "numLockKey" },
+ { key: "ScrollLock", attr: "scrollLockKey" },
+ { key: "SymbolLock", attr: "symbolLockKey" },
+ ],
+ };
+
+ for (let i = 0; i < modifiers.normal.length; i++) {
+ if (!aKeyEvent[modifiers.normal[i].attr]) {
+ continue;
+ }
+ if (aTIP.getModifierState(modifiers.normal[i].key)) {
+ continue; // already activated.
+ }
+ let event = new KeyboardEvent("", { key: modifiers.normal[i].key });
+ aTIP.keydown(
+ event,
+ aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
+ );
+ modifiers.normal[i].activated = true;
+ }
+ for (let i = 0; i < modifiers.lockable.length; i++) {
+ if (!aKeyEvent[modifiers.lockable[i].attr]) {
+ continue;
+ }
+ if (aTIP.getModifierState(modifiers.lockable[i].key)) {
+ continue; // already activated.
+ }
+ let event = new KeyboardEvent("", { key: modifiers.lockable[i].key });
+ aTIP.keydown(
+ event,
+ aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
+ );
+ aTIP.keyup(
+ event,
+ aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
+ );
+ modifiers.lockable[i].activated = true;
+ }
+ return modifiers;
+}
+
+function _emulateToInactivateModifiers(aTIP, aModifiers, aWindow = window) {
+ if (!aModifiers) {
+ return;
+ }
+ var KeyboardEvent = _getKeyboardEvent(aWindow);
+ for (let i = 0; i < aModifiers.normal.length; i++) {
+ if (!aModifiers.normal[i].activated) {
+ continue;
+ }
+ let event = new KeyboardEvent("", { key: aModifiers.normal[i].key });
+ aTIP.keyup(
+ event,
+ aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
+ );
+ }
+ for (let i = 0; i < aModifiers.lockable.length; i++) {
+ if (!aModifiers.lockable[i].activated) {
+ continue;
+ }
+ if (!aTIP.getModifierState(aModifiers.lockable[i].key)) {
+ continue; // who already inactivated this?
+ }
+ let event = new KeyboardEvent("", { key: aModifiers.lockable[i].key });
+ aTIP.keydown(
+ event,
+ aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
+ );
+ aTIP.keyup(
+ event,
+ aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
+ );
+ }
+}
+
+/**
+ * Synthesize a composition event and keydown event and keyup events unless
+ * you prevent to dispatch them explicitly (see aEvent.key's explanation).
+ *
+ * Note that you shouldn't call this with "compositionstart" unless you need to
+ * test compositionstart event which is NOT followed by compositionupdate
+ * event immediately. Typically, native IME starts composition with
+ * a pair of keydown and keyup event and dispatch compositionstart and
+ * compositionupdate (and non-standard text event) between them. So, in most
+ * cases, you should call synthesizeCompositionChange() directly.
+ * If you call this with compositionstart, keyup event will be fired
+ * immediately after compositionstart. In other words, you should use
+ * "compositionstart" only when you need to emulate IME which just starts
+ * composition with compositionstart event but does not send composing text to
+ * us until committing the composition. This is behavior of some Chinese IMEs.
+ *
+ * @param aEvent The composition event information. This must
+ * have |type| member. The value must be
+ * "compositionstart", "compositionend",
+ * "compositioncommitasis" or "compositioncommit".
+ *
+ * And also this may have |data| and |locale| which
+ * would be used for the value of each property of
+ * the composition event. Note that the |data| is
+ * ignored if the event type is "compositionstart"
+ * or "compositioncommitasis".
+ *
+ * If |key| is undefined, "keydown" and "keyup"
+ * events which are marked as "processed by IME"
+ * are dispatched. If |key| is not null, "keydown"
+ * and/or "keyup" events are dispatched (if the
+ * |key.type| is specified as "keydown", only
+ * "keydown" event is dispatched). Otherwise,
+ * i.e., if |key| is null, neither "keydown" nor
+ * "keyup" event is dispatched.
+ *
+ * If |key.doNotMarkKeydownAsProcessed| is not true,
+ * key value and keyCode value of "keydown" event
+ * will be set to "Process" and DOM_VK_PROCESSKEY.
+ * If |key.markKeyupAsProcessed| is true,
+ * key value and keyCode value of "keyup" event
+ * will be set to "Process" and DOM_VK_PROCESSKEY.
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @param aCallback Optional (If non-null, use the callback for
+ * receiving notifications to IME)
+ */
+function synthesizeComposition(aEvent, aWindow = window, aCallback) {
+ var TIP = _getTIP(aWindow, aCallback);
+ if (!TIP) {
+ return;
+ }
+ var KeyboardEvent = _getKeyboardEvent(aWindow);
+ var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
+ var keyEventDict = { dictionary: null, flags: 0 };
+ var keyEvent = null;
+ if (aEvent.key && typeof aEvent.key.key === "string") {
+ keyEventDict = _createKeyboardEventDictionary(
+ aEvent.key.key,
+ aEvent.key,
+ TIP,
+ aWindow
+ );
+ keyEvent = new KeyboardEvent(
+ // eslint-disable-next-line no-nested-ternary
+ aEvent.key.type === "keydown"
+ ? "keydown"
+ : aEvent.key.type === "keyup"
+ ? "keyup"
+ : "",
+ keyEventDict.dictionary
+ );
+ } else if (aEvent.key === undefined) {
+ keyEventDict = _createKeyboardEventDictionary(
+ "KEY_Process",
+ {},
+ TIP,
+ aWindow
+ );
+ keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
+ }
+ try {
+ switch (aEvent.type) {
+ case "compositionstart":
+ TIP.startComposition(keyEvent, keyEventDict.flags);
+ break;
+ case "compositioncommitasis":
+ TIP.commitComposition(keyEvent, keyEventDict.flags);
+ break;
+ case "compositioncommit":
+ TIP.commitCompositionWith(aEvent.data, keyEvent, keyEventDict.flags);
+ break;
+ }
+ } finally {
+ _emulateToInactivateModifiers(TIP, modifiers, aWindow);
+ }
+}
+/**
+ * Synthesize eCompositionChange event which causes a DOM text event, may
+ * cause compositionupdate event, and causes keydown event and keyup event
+ * unless you prevent to dispatch them explicitly (see aEvent.key's
+ * explanation).
+ *
+ * Note that if you call this when there is no composition, compositionstart
+ * event will be fired automatically. This is better than you use
+ * synthesizeComposition("compositionstart") in most cases. See the
+ * explanation of synthesizeComposition().
+ *
+ * @param aEvent The compositionchange event's information, this has
+ * |composition| and |caret| members. |composition| has
+ * |string| and |clauses| members. |clauses| must be array
+ * object. Each object has |length| and |attr|. And |caret|
+ * has |start| and |length|. See the following tree image.
+ *
+ * aEvent
+ * +-- composition
+ * | +-- string
+ * | +-- clauses[]
+ * | +-- length
+ * | +-- attr
+ * +-- caret
+ * | +-- start
+ * | +-- length
+ * +-- key
+ *
+ * Set the composition string to |composition.string|. Set its
+ * clauses information to the |clauses| array.
+ *
+ * When it's composing, set the each clauses' length to the
+ * |composition.clauses[n].length|. The sum of the all length
+ * values must be same as the length of |composition.string|.
+ * Set nsICompositionStringSynthesizer.ATTR_* to the
+ * |composition.clauses[n].attr|.
+ *
+ * When it's not composing, set 0 to the
+ * |composition.clauses[0].length| and
+ * |composition.clauses[0].attr|.
+ *
+ * Set caret position to the |caret.start|. It's offset from
+ * the start of the composition string. Set caret length to
+ * |caret.length|. If it's larger than 0, it should be wide
+ * caret. However, current nsEditor doesn't support wide
+ * caret, therefore, you should always set 0 now.
+ *
+ * If |key| is undefined, "keydown" and "keyup" events which
+ * are marked as "processed by IME" are dispatched. If |key|
+ * is not null, "keydown" and/or "keyup" events are dispatched
+ * (if the |key.type| is specified as "keydown", only "keydown"
+ * event is dispatched). Otherwise, i.e., if |key| is null,
+ * neither "keydown" nor "keyup" event is dispatched.
+ * If |key.doNotMarkKeydownAsProcessed| is not true, key value
+ * and keyCode value of "keydown" event will be set to
+ * "Process" and DOM_VK_PROCESSKEY.
+ * If |key.markKeyupAsProcessed| is true key value and keyCode
+ * value of "keyup" event will be set to "Process" and
+ * DOM_VK_PROCESSKEY.
+ *
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @param aCallback Optional (If non-null, use the callback for receiving
+ * notifications to IME)
+ */
+function synthesizeCompositionChange(aEvent, aWindow = window, aCallback) {
+ var TIP = _getTIP(aWindow, aCallback);
+ if (!TIP) {
+ return;
+ }
+ var KeyboardEvent = _getKeyboardEvent(aWindow);
+
+ if (
+ !aEvent.composition ||
+ !aEvent.composition.clauses ||
+ !aEvent.composition.clauses[0]
+ ) {
+ return;
+ }
+
+ TIP.setPendingCompositionString(aEvent.composition.string);
+ if (aEvent.composition.clauses[0].length) {
+ for (var i = 0; i < aEvent.composition.clauses.length; i++) {
+ switch (aEvent.composition.clauses[i].attr) {
+ case TIP.ATTR_RAW_CLAUSE:
+ case TIP.ATTR_SELECTED_RAW_CLAUSE:
+ case TIP.ATTR_CONVERTED_CLAUSE:
+ case TIP.ATTR_SELECTED_CLAUSE:
+ TIP.appendClauseToPendingComposition(
+ aEvent.composition.clauses[i].length,
+ aEvent.composition.clauses[i].attr
+ );
+ break;
+ case 0:
+ // Ignore dummy clause for the argument.
+ break;
+ default:
+ throw new Error("invalid clause attribute specified");
+ }
+ }
+ }
+
+ if (aEvent.caret) {
+ TIP.setCaretInPendingComposition(aEvent.caret.start);
+ }
+
+ var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
+ try {
+ var keyEventDict = { dictionary: null, flags: 0 };
+ var keyEvent = null;
+ if (aEvent.key && typeof aEvent.key.key === "string") {
+ keyEventDict = _createKeyboardEventDictionary(
+ aEvent.key.key,
+ aEvent.key,
+ TIP,
+ aWindow
+ );
+ keyEvent = new KeyboardEvent(
+ // eslint-disable-next-line no-nested-ternary
+ aEvent.key.type === "keydown"
+ ? "keydown"
+ : aEvent.key.type === "keyup"
+ ? "keyup"
+ : "",
+ keyEventDict.dictionary
+ );
+ } else if (aEvent.key === undefined) {
+ keyEventDict = _createKeyboardEventDictionary(
+ "KEY_Process",
+ {},
+ TIP,
+ aWindow
+ );
+ keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
+ }
+ TIP.flushPendingComposition(keyEvent, keyEventDict.flags);
+ } finally {
+ _emulateToInactivateModifiers(TIP, modifiers, aWindow);
+ }
+}
+
+// Must be synchronized with nsIDOMWindowUtils.
+const QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK = 0x0000;
+const QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK = 0x0001;
+
+const QUERY_CONTENT_FLAG_SELECTION_NORMAL = 0x0000;
+const QUERY_CONTENT_FLAG_SELECTION_SPELLCHECK = 0x0002;
+const QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT = 0x0004;
+const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT = 0x0008;
+const QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT = 0x0010;
+const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT = 0x0020;
+const QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY = 0x0040;
+const QUERY_CONTENT_FLAG_SELECTION_FIND = 0x0080;
+const QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY = 0x0100;
+const QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT = 0x0200;
+
+const QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT = 0x0400;
+
+const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK = 0x0000;
+const SELECTION_SET_FLAG_USE_XP_LINE_BREAK = 0x0001;
+const SELECTION_SET_FLAG_REVERSE = 0x0002;
+
+/**
+ * Synthesize a query text content event.
+ *
+ * @param aOffset The character offset. 0 means the first character in the
+ * selection root.
+ * @param aLength The length of getting text. If the length is too long,
+ * the extra length is ignored.
+ * @param aIsRelative Optional (If true, aOffset is relative to start of
+ * composition if there is, or start of selection.)
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @return An nsIQueryContentEventResult object. If this failed,
+ * the result might be null.
+ */
+function synthesizeQueryTextContent(aOffset, aLength, aIsRelative, aWindow) {
+ var utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return null;
+ }
+ var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
+ if (aIsRelative === true) {
+ flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT;
+ }
+ return utils.sendQueryContentEvent(
+ utils.QUERY_TEXT_CONTENT,
+ aOffset,
+ aLength,
+ 0,
+ 0,
+ flags
+ );
+}
+
+/**
+ * Synthesize a query selected text event.
+ *
+ * @param aSelectionType Optional, one of QUERY_CONTENT_FLAG_SELECTION_*.
+ * If null, QUERY_CONTENT_FLAG_SELECTION_NORMAL will
+ * be used.
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @return An nsIQueryContentEventResult object. If this failed,
+ * the result might be null.
+ */
+function synthesizeQuerySelectedText(aSelectionType, aWindow) {
+ var utils = _getDOMWindowUtils(aWindow);
+ var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
+ if (aSelectionType) {
+ flags |= aSelectionType;
+ }
+
+ return utils.sendQueryContentEvent(
+ utils.QUERY_SELECTED_TEXT,
+ 0,
+ 0,
+ 0,
+ 0,
+ flags
+ );
+}
+
+/**
+ * Synthesize a query caret rect event.
+ *
+ * @param aOffset The caret offset. 0 means left side of the first character
+ * in the selection root.
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @return An nsIQueryContentEventResult object. If this failed,
+ * the result might be null.
+ */
+function synthesizeQueryCaretRect(aOffset, aWindow) {
+ var utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return null;
+ }
+ return utils.sendQueryContentEvent(
+ utils.QUERY_CARET_RECT,
+ aOffset,
+ 0,
+ 0,
+ 0,
+ QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
+ );
+}
+
+/**
+ * Synthesize a selection set event.
+ *
+ * @param aOffset The character offset. 0 means the first character in the
+ * selection root.
+ * @param aLength The length of the text. If the length is too long,
+ * the extra length is ignored.
+ * @param aReverse If true, the selection is from |aOffset + aLength| to
+ * |aOffset|. Otherwise, from |aOffset| to |aOffset + aLength|.
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @return True, if succeeded. Otherwise false.
+ */
+async function synthesizeSelectionSet(
+ aOffset,
+ aLength,
+ aReverse,
+ aWindow = window
+) {
+ const utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return false;
+ }
+ // eSetSelection event will be compared with selection cache in
+ // IMEContentObserver, but it may have not been updated yet. Therefore, we
+ // need to flush pending things of IMEContentObserver.
+ await new Promise(resolve =>
+ aWindow.requestAnimationFrame(() => aWindow.requestAnimationFrame(resolve))
+ );
+ const flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0;
+ return utils.sendSelectionSetEvent(aOffset, aLength, flags);
+}
+
+/**
+ * Synthesize a query text rect event.
+ *
+ * @param aOffset The character offset. 0 means the first character in the
+ * selection root.
+ * @param aLength The length of the text. If the length is too long,
+ * the extra length is ignored.
+ * @param aIsRelative Optional (If true, aOffset is relative to start of
+ * composition if there is, or start of selection.)
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @return An nsIQueryContentEventResult object. If this failed,
+ * the result might be null.
+ */
+function synthesizeQueryTextRect(aOffset, aLength, aIsRelative, aWindow) {
+ if (aIsRelative !== undefined && typeof aIsRelative !== "boolean") {
+ throw new Error(
+ "Maybe, you set Window object to the 3rd argument, but it should be a boolean value"
+ );
+ }
+ var utils = _getDOMWindowUtils(aWindow);
+ let flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
+ if (aIsRelative === true) {
+ flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT;
+ }
+ return utils.sendQueryContentEvent(
+ utils.QUERY_TEXT_RECT,
+ aOffset,
+ aLength,
+ 0,
+ 0,
+ flags
+ );
+}
+
+/**
+ * Synthesize a query text rect array event.
+ *
+ * @param aOffset The character offset. 0 means the first character in the
+ * selection root.
+ * @param aLength The length of the text. If the length is too long,
+ * the extra length is ignored.
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @return An nsIQueryContentEventResult object. If this failed,
+ * the result might be null.
+ */
+function synthesizeQueryTextRectArray(aOffset, aLength, aWindow) {
+ var utils = _getDOMWindowUtils(aWindow);
+ return utils.sendQueryContentEvent(
+ utils.QUERY_TEXT_RECT_ARRAY,
+ aOffset,
+ aLength,
+ 0,
+ 0,
+ QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
+ );
+}
+
+/**
+ * Synthesize a query editor rect event.
+ *
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @return An nsIQueryContentEventResult object. If this failed,
+ * the result might be null.
+ */
+function synthesizeQueryEditorRect(aWindow) {
+ var utils = _getDOMWindowUtils(aWindow);
+ return utils.sendQueryContentEvent(
+ utils.QUERY_EDITOR_RECT,
+ 0,
+ 0,
+ 0,
+ 0,
+ QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
+ );
+}
+
+/**
+ * Synthesize a character at point event.
+ *
+ * @param aX, aY The offset in the client area of the DOM window.
+ * @param aWindow Optional (If null, current |window| will be used)
+ * @return An nsIQueryContentEventResult object. If this failed,
+ * the result might be null.
+ */
+function synthesizeCharAtPoint(aX, aY, aWindow) {
+ var utils = _getDOMWindowUtils(aWindow);
+ return utils.sendQueryContentEvent(
+ utils.QUERY_CHARACTER_AT_POINT,
+ 0,
+ 0,
+ aX,
+ aY,
+ QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
+ );
+}
+
+/**
+ * INTERNAL USE ONLY
+ * Create an event object to pass to sendDragEvent.
+ *
+ * @param aType The string represents drag event type.
+ * @param aDestElement The element to fire the drag event, used to calculate
+ * screenX/Y and clientX/Y.
+ * @param aDestWindow Optional; Defaults to the current window object.
+ * @param aDataTransfer dataTransfer for current drag session.
+ * @param aDragEvent The object contains properties to override the event
+ * object
+ * @return An object to pass to sendDragEvent.
+ */
+function createDragEventObject(
+ aType,
+ aDestElement,
+ aDestWindow,
+ aDataTransfer,
+ aDragEvent
+) {
+ var destRect = aDestElement.getBoundingClientRect();
+ var destClientX = destRect.left + destRect.width / 2;
+ var destClientY = destRect.top + destRect.height / 2;
+ var destScreenX = aDestWindow.mozInnerScreenX + destClientX;
+ var destScreenY = aDestWindow.mozInnerScreenY + destClientY;
+ if ("clientX" in aDragEvent && !("screenX" in aDragEvent)) {
+ aDragEvent.screenX = aDestWindow.mozInnerScreenX + aDragEvent.clientX;
+ }
+ if ("clientY" in aDragEvent && !("screenY" in aDragEvent)) {
+ aDragEvent.screenY = aDestWindow.mozInnerScreenY + aDragEvent.clientY;
+ }
+
+ // Wrap only in plain mochitests
+ let dataTransfer;
+ if (aDataTransfer) {
+ dataTransfer = _EU_maybeUnwrap(
+ _EU_maybeWrap(aDataTransfer).mozCloneForEvent(aType)
+ );
+
+ // Copy over the drop effect. This isn't copied over by Clone, as it uses
+ // more complex logic in the actual implementation (see
+ // nsContentUtils::SetDataTransferInEvent for actual impl).
+ dataTransfer.dropEffect = aDataTransfer.dropEffect;
+ }
+
+ return Object.assign(
+ {
+ type: aType,
+ screenX: destScreenX,
+ screenY: destScreenY,
+ clientX: destClientX,
+ clientY: destClientY,
+ dataTransfer,
+ _domDispatchOnly: aDragEvent._domDispatchOnly,
+ },
+ aDragEvent
+ );
+}
+
+/**
+ * Emulate a event sequence of dragstart, dragenter, and dragover.
+ *
+ * @param {Element} aSrcElement
+ * The element to use to start the drag.
+ * @param {Element} aDestElement
+ * The element to fire the dragover, dragenter events
+ * @param {Array} aDragData
+ * The data to supply for the data transfer.
+ * This data is in the format:
+ *
+ * [
+ * [
+ * {"type": value, "data": value },
+ * ...,
+ * ],
+ * ...
+ * ]
+ *
+ * Pass null to avoid modifying dataTransfer.
+ * @param {String} [aDropEffect="move"]
+ * The drop effect to set during the dragstart event, or 'move' if omitted.
+ * @param {Window} [aWindow=window]
+ * The window in which the drag happens. Defaults to the window in which
+ * EventUtils.js is loaded.
+ * @param {Window} [aDestWindow=aWindow]
+ * Used when aDestElement is in a different window than aSrcElement.
+ * Default is to match ``aWindow``.
+ * @param {Object} [aDragEvent={}]
+ * Defaults to empty object. Overwrites an object passed to sendDragEvent.
+ * @return {Array}
+ * A two element array, where the first element is the value returned
+ * from sendDragEvent for dragover event, and the second element is the
+ * dataTransfer for the current drag session.
+ */
+function synthesizeDragOver(
+ aSrcElement,
+ aDestElement,
+ aDragData,
+ aDropEffect,
+ aWindow,
+ aDestWindow,
+ aDragEvent = {}
+) {
+ if (!aWindow) {
+ aWindow = window;
+ }
+ if (!aDestWindow) {
+ aDestWindow = aWindow;
+ }
+
+ // eslint-disable-next-line mozilla/use-services
+ const obs = _EU_Cc["@mozilla.org/observer-service;1"].getService(
+ _EU_Ci.nsIObserverService
+ );
+ const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
+ _EU_Ci.nsIDragService
+ );
+ var sess = ds.getCurrentSession();
+
+ // This method runs before other callbacks, and acts as a way to inject the
+ // initial drag data into the DataTransfer.
+ function fillDrag(event) {
+ if (aDragData) {
+ for (var i = 0; i < aDragData.length; i++) {
+ var item = aDragData[i];
+ for (var j = 0; j < item.length; j++) {
+ _EU_maybeWrap(event.dataTransfer).mozSetDataAt(
+ item[j].type,
+ item[j].data,
+ i
+ );
+ }
+ }
+ }
+ event.dataTransfer.dropEffect = aDropEffect || "move";
+ event.preventDefault();
+ }
+
+ function trapDrag(subject, topic) {
+ if (topic == "on-datatransfer-available") {
+ sess.dataTransfer = _EU_maybeUnwrap(
+ _EU_maybeWrap(subject).mozCloneForEvent("drop")
+ );
+ sess.dataTransfer.dropEffect = subject.dropEffect;
+ }
+ }
+
+ // need to use real mouse action
+ aWindow.addEventListener("dragstart", fillDrag, true);
+ obs.addObserver(trapDrag, "on-datatransfer-available");
+ synthesizeMouseAtCenter(aSrcElement, { type: "mousedown" }, aWindow);
+
+ var rect = aSrcElement.getBoundingClientRect();
+ var x = rect.width / 2;
+ var y = rect.height / 2;
+ synthesizeMouse(aSrcElement, x, y, { type: "mousemove" }, aWindow);
+ synthesizeMouse(aSrcElement, x + 10, y + 10, { type: "mousemove" }, aWindow);
+ aWindow.removeEventListener("dragstart", fillDrag, true);
+ obs.removeObserver(trapDrag, "on-datatransfer-available");
+
+ var dataTransfer = sess.dataTransfer;
+ if (!dataTransfer) {
+ throw new Error("No data transfer object after synthesizing the mouse!");
+ }
+
+ // The EventStateManager will fire our dragenter event if it needs to.
+ var event = createDragEventObject(
+ "dragover",
+ aDestElement,
+ aDestWindow,
+ dataTransfer,
+ aDragEvent
+ );
+ var result = sendDragEvent(event, aDestElement, aDestWindow);
+
+ return [result, dataTransfer];
+}
+
+/**
+ * Emulate the drop event and mouseup event.
+ * This should be called after synthesizeDragOver.
+ *
+ * @param {*} aResult
+ * The first element of the array returned from ``synthesizeDragOver``.
+ * @param {DataTransfer} aDataTransfer
+ * The second element of the array returned from ``synthesizeDragOver``.
+ * @param {Element} aDestElement
+ * The element on which to fire the drop event.
+ * @param {Window} [aDestWindow=window]
+ * The window in which the drop happens. Defaults to the window in which
+ * EventUtils.js is loaded.
+ * @param {Object} [aDragEvent={}]
+ * Defaults to empty object. Overwrites an object passed to sendDragEvent.
+ * @return {String}
+ * "none" if aResult is true, ``aDataTransfer.dropEffect`` otherwise.
+ */
+function synthesizeDropAfterDragOver(
+ aResult,
+ aDataTransfer,
+ aDestElement,
+ aDestWindow,
+ aDragEvent = {}
+) {
+ if (!aDestWindow) {
+ aDestWindow = window;
+ }
+
+ var effect = aDataTransfer.dropEffect;
+ var event;
+
+ if (aResult) {
+ effect = "none";
+ } else if (effect != "none") {
+ event = createDragEventObject(
+ "drop",
+ aDestElement,
+ aDestWindow,
+ aDataTransfer,
+ aDragEvent
+ );
+ sendDragEvent(event, aDestElement, aDestWindow);
+ }
+ synthesizeMouse(aDestElement, 2, 2, { type: "mouseup" }, aDestWindow);
+
+ return effect;
+}
+
+/**
+ * Emulate a drag and drop by emulating a dragstart and firing events dragenter,
+ * dragover, and drop.
+ *
+ * @param {Element} aSrcElement
+ * The element to use to start the drag.
+ * @param {Element} aDestElement
+ * The element to fire the dragover, dragenter events
+ * @param {Array} aDragData
+ * The data to supply for the data transfer.
+ * This data is in the format:
+ *
+ * [
+ * [
+ * {"type": value, "data": value },
+ * ...,
+ * ],
+ * ...
+ * ]
+ *
+ * Pass null to avoid modifying dataTransfer.
+ * @param {String} [aDropEffect="move"]
+ * The drop effect to set during the dragstart event, or 'move' if omitted..
+ * @param {Window} [aWindow=window]
+ * The window in which the drag happens. Defaults to the window in which
+ * EventUtils.js is loaded.
+ * @param {Window} [aDestWindow=aWindow]
+ * Used when aDestElement is in a different window than aSrcElement.
+ * Default is to match ``aWindow``.
+ * @param {Object} [aDragEvent={}]
+ * Defaults to empty object. Overwrites an object passed to sendDragEvent.
+ * @return {String}
+ * The drop effect that was desired.
+ */
+function synthesizeDrop(
+ aSrcElement,
+ aDestElement,
+ aDragData,
+ aDropEffect,
+ aWindow,
+ aDestWindow,
+ aDragEvent = {}
+) {
+ if (!aWindow) {
+ aWindow = window;
+ }
+ if (!aDestWindow) {
+ aDestWindow = aWindow;
+ }
+
+ var ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
+ _EU_Ci.nsIDragService
+ );
+
+ let dropAction;
+ switch (aDropEffect) {
+ case null:
+ case undefined:
+ case "move":
+ dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
+ break;
+ case "copy":
+ dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY;
+ break;
+ case "link":
+ dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK;
+ break;
+ default:
+ throw new Error(`${aDropEffect} is an invalid drop effect value`);
+ }
+
+ ds.startDragSessionForTests(dropAction);
+
+ try {
+ var [result, dataTransfer] = synthesizeDragOver(
+ aSrcElement,
+ aDestElement,
+ aDragData,
+ aDropEffect,
+ aWindow,
+ aDestWindow,
+ aDragEvent
+ );
+ return synthesizeDropAfterDragOver(
+ result,
+ dataTransfer,
+ aDestElement,
+ aDestWindow,
+ aDragEvent
+ );
+ } finally {
+ ds.endDragSession(true, _parseModifiers(aDragEvent));
+ }
+}
+
+function _getFlattenedTreeParentNode(aNode) {
+ return _EU_maybeUnwrap(_EU_maybeWrap(aNode).flattenedTreeParentNode);
+}
+
+function _getInclusiveFlattenedTreeParentElement(aNode) {
+ for (
+ let inclusiveAncestor = aNode;
+ inclusiveAncestor;
+ inclusiveAncestor = _getFlattenedTreeParentNode(inclusiveAncestor)
+ ) {
+ if (inclusiveAncestor.nodeType == Node.ELEMENT_NODE) {
+ return inclusiveAncestor;
+ }
+ }
+ return null;
+}
+
+function _nodeIsFlattenedTreeDescendantOf(
+ aPossibleDescendant,
+ aPossibleAncestor
+) {
+ do {
+ if (aPossibleDescendant == aPossibleAncestor) {
+ return true;
+ }
+ aPossibleDescendant = _getFlattenedTreeParentNode(aPossibleDescendant);
+ } while (aPossibleDescendant);
+ return false;
+}
+
+function _computeSrcElementFromSrcSelection(aSrcSelection) {
+ let srcElement = aSrcSelection.focusNode;
+ while (_EU_maybeWrap(srcElement).isNativeAnonymous) {
+ srcElement = _getFlattenedTreeParentNode(srcElement);
+ }
+ if (srcElement.nodeType !== Node.ELEMENT_NODE) {
+ srcElement = _getInclusiveFlattenedTreeParentElement(srcElement);
+ }
+ return srcElement;
+}
+
+/**
+ * Emulate a drag and drop by emulating a dragstart by mousedown and mousemove,
+ * and firing events dragenter, dragover, drop, and dragend.
+ * This does not modify dataTransfer and tries to emulate the plain drag and
+ * drop as much as possible, compared to synthesizeDrop.
+ * Note that if synthesized dragstart is canceled, this throws an exception
+ * because in such case, Gecko does not start drag session.
+ *
+ * @param {Object} aParams
+ * @param {Event} aParams.dragEvent
+ * The DnD events will be generated with modifiers specified with this.
+ * @param {Element} aParams.srcElement
+ * The element to start dragging. If srcSelection is
+ * set, this is computed for element at focus node.
+ * @param {Selection|nil} aParams.srcSelection
+ * The selection to start to drag, set null if srcElement is set.
+ * @param {Element|nil} aParams.destElement
+ * The element to drop on. Pass null to emulate a drop on an invalid target.
+ * @param {Number} aParams.srcX
+ * The initial x coordinate inside srcElement or ignored if srcSelection is set.
+ * @param {Number} aParams.srcY
+ * The initial y coordinate inside srcElement or ignored if srcSelection is set.
+ * @param {Number} aParams.stepX
+ * The x-axis step for mousemove inside srcElement
+ * @param {Number} aParams.stepY
+ * The y-axis step for mousemove inside srcElement
+ * @param {Number} aParams.finalX
+ * The final x coordinate inside srcElement
+ * @param {Number} aParams.finalY
+ * The final x coordinate inside srcElement
+ * @param {Any} aParams.id
+ * The pointer event id
+ * @param {Window} aParams.srcWindow
+ * The window for dispatching event on srcElement, defaults to the current window object.
+ * @param {Window} aParams.destWindow
+ * The window for dispatching event on destElement, defaults to the current window object.
+ * @param {Boolean} aParams.expectCancelDragStart
+ * Set to true if the test cancels "dragstart"
+ * @param {Boolean} aParams.expectSrcElementDisconnected
+ * Set to true if srcElement will be disconnected and
+ * "dragend" event won't be fired.
+ * @param {Function} aParams.logFunc
+ * Set function which takes one argument if you need to log rect of target. E.g., `console.log`.
+ */
+// eslint-disable-next-line complexity
+async function synthesizePlainDragAndDrop(aParams) {
+ let {
+ dragEvent = {},
+ srcElement,
+ srcSelection,
+ destElement,
+ srcX = 2,
+ srcY = 2,
+ stepX = 9,
+ stepY = 9,
+ finalX = srcX + stepX * 2,
+ finalY = srcY + stepY * 2,
+ id = _getDOMWindowUtils(window).DEFAULT_MOUSE_POINTER_ID,
+ srcWindow = window,
+ destWindow = window,
+ expectCancelDragStart = false,
+ expectSrcElementDisconnected = false,
+ logFunc,
+ } = aParams;
+ // Don't modify given dragEvent object because we modify dragEvent below and
+ // callers may use the object multiple times so that callers must not assume
+ // that it'll be modified.
+ if (aParams.dragEvent !== undefined) {
+ dragEvent = Object.assign({}, aParams.dragEvent);
+ }
+
+ function rectToString(aRect) {
+ return `left: ${aRect.left}, top: ${aRect.top}, right: ${aRect.right}, bottom: ${aRect.bottom}`;
+ }
+
+ if (logFunc) {
+ logFunc("synthesizePlainDragAndDrop() -- START");
+ }
+
+ if (srcSelection) {
+ srcElement = _computeSrcElementFromSrcSelection(srcSelection);
+ let srcElementRect = srcElement.getBoundingClientRect();
+ if (logFunc) {
+ logFunc(
+ `srcElement.getBoundingClientRect(): ${rectToString(srcElementRect)}`
+ );
+ }
+ // Use last selection client rect because nsIDragSession.sourceNode is
+ // initialized from focus node which is usually in last rect.
+ let selectionRectList = srcSelection.getRangeAt(0).getClientRects();
+ let lastSelectionRect = selectionRectList[selectionRectList.length - 1];
+ if (logFunc) {
+ logFunc(
+ `srcSelection.getRangeAt(0).getClientRects()[${
+ selectionRectList.length - 1
+ }]: ${rectToString(lastSelectionRect)}`
+ );
+ }
+ // Click at center of last selection rect.
+ srcX = Math.floor(lastSelectionRect.left + lastSelectionRect.width / 2);
+ srcY = Math.floor(lastSelectionRect.top + lastSelectionRect.height / 2);
+ // Then, adjust srcX and srcY for making them offset relative to
+ // srcElementRect because they will be used when we call synthesizeMouse()
+ // with srcElement.
+ srcX = Math.floor(srcX - srcElementRect.left);
+ srcY = Math.floor(srcY - srcElementRect.top);
+ // Finally, recalculate finalX and finalY with new srcX and srcY if they
+ // are not specified by the caller.
+ if (aParams.finalX === undefined) {
+ finalX = srcX + stepX * 2;
+ }
+ if (aParams.finalY === undefined) {
+ finalY = srcY + stepY * 2;
+ }
+ } else if (logFunc) {
+ logFunc(
+ `srcElement.getBoundingClientRect(): ${rectToString(
+ srcElement.getBoundingClientRect()
+ )}`
+ );
+ }
+
+ const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
+ _EU_Ci.nsIDragService
+ );
+
+ const editingHost = (() => {
+ if (!srcElement.matches(":read-write")) {
+ return null;
+ }
+ let lastEditableElement = srcElement;
+ for (
+ let inclusiveAncestor =
+ _getInclusiveFlattenedTreeParentElement(srcElement);
+ inclusiveAncestor;
+ inclusiveAncestor = _getInclusiveFlattenedTreeParentElement(
+ _getFlattenedTreeParentNode(inclusiveAncestor)
+ )
+ ) {
+ if (inclusiveAncestor.matches(":read-write")) {
+ lastEditableElement = inclusiveAncestor;
+ if (lastEditableElement == srcElement.ownerDocument.body) {
+ break;
+ }
+ }
+ }
+ return lastEditableElement;
+ })();
+ try {
+ _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(true);
+
+ await new Promise(r => setTimeout(r, 0));
+
+ let mouseDownEvent;
+ function onMouseDown(aEvent) {
+ mouseDownEvent = aEvent;
+ if (logFunc) {
+ logFunc(
+ `"${aEvent.type}" event is fired on ${
+ aEvent.target
+ } (composedTarget: ${_EU_maybeUnwrap(
+ _EU_maybeWrap(aEvent).composedTarget
+ )}`
+ );
+ }
+ if (
+ !_nodeIsFlattenedTreeDescendantOf(
+ _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
+ srcElement
+ )
+ ) {
+ // If srcX and srcY does not point in one of rects in srcElement,
+ // "mousedown" target is not in srcElement. Such case must not
+ // be expected by this API users so that we should throw an exception
+ // for making debugging easier.
+ throw new Error(
+ 'event target of "mousedown" is not srcElement nor its descendant'
+ );
+ }
+ }
+ try {
+ srcWindow.addEventListener("mousedown", onMouseDown, { capture: true });
+ synthesizeMouse(
+ srcElement,
+ srcX,
+ srcY,
+ { type: "mousedown", id },
+ srcWindow
+ );
+ if (logFunc) {
+ logFunc(`mousedown at ${srcX}, ${srcY}`);
+ }
+ if (!mouseDownEvent) {
+ throw new Error('"mousedown" event is not fired');
+ }
+ } finally {
+ srcWindow.removeEventListener("mousedown", onMouseDown, {
+ capture: true,
+ });
+ }
+
+ let dragStartEvent;
+ function onDragStart(aEvent) {
+ dragStartEvent = aEvent;
+ if (logFunc) {
+ logFunc(`"${aEvent.type}" event is fired`);
+ }
+ if (
+ !_nodeIsFlattenedTreeDescendantOf(
+ _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
+ srcElement
+ )
+ ) {
+ // If srcX and srcY does not point in one of rects in srcElement,
+ // "dragstart" target is not in srcElement. Such case must not
+ // be expected by this API users so that we should throw an exception
+ // for making debugging easier.
+ throw new Error(
+ 'event target of "dragstart" is not srcElement nor its descendant'
+ );
+ }
+ }
+ let dragEnterEvent;
+ function onDragEnterGenerated(aEvent) {
+ dragEnterEvent = aEvent;
+ }
+ srcWindow.addEventListener("dragstart", onDragStart, { capture: true });
+ srcWindow.addEventListener("dragenter", onDragEnterGenerated, {
+ capture: true,
+ });
+ try {
+ // Wait for the next event tick after each event dispatch, so that UI
+ // elements (e.g. menu) work like the real user input.
+ await new Promise(r => setTimeout(r, 0));
+
+ srcX += stepX;
+ srcY += stepY;
+ synthesizeMouse(
+ srcElement,
+ srcX,
+ srcY,
+ { type: "mousemove", id },
+ srcWindow
+ );
+ if (logFunc) {
+ logFunc(`first mousemove at ${srcX}, ${srcY}`);
+ }
+
+ await new Promise(r => setTimeout(r, 0));
+
+ srcX += stepX;
+ srcY += stepY;
+ synthesizeMouse(
+ srcElement,
+ srcX,
+ srcY,
+ { type: "mousemove", id },
+ srcWindow
+ );
+ if (logFunc) {
+ logFunc(`second mousemove at ${srcX}, ${srcY}`);
+ }
+
+ await new Promise(r => setTimeout(r, 0));
+
+ if (!dragStartEvent) {
+ throw new Error('"dragstart" event is not fired');
+ }
+ } finally {
+ srcWindow.removeEventListener("dragstart", onDragStart, {
+ capture: true,
+ });
+ srcWindow.removeEventListener("dragenter", onDragEnterGenerated, {
+ capture: true,
+ });
+ }
+
+ let session = ds.getCurrentSession();
+ if (!session) {
+ if (expectCancelDragStart) {
+ synthesizeMouse(
+ srcElement,
+ finalX,
+ finalY,
+ { type: "mouseup", id },
+ srcWindow
+ );
+ return;
+ }
+ throw new Error("drag hasn't been started by the operation");
+ } else if (expectCancelDragStart) {
+ throw new Error("drag has been started by the operation");
+ }
+
+ if (destElement) {
+ if (
+ (srcElement != destElement && !dragEnterEvent) ||
+ destElement != dragEnterEvent.target
+ ) {
+ if (logFunc) {
+ logFunc(
+ `destElement.getBoundingClientRect(): ${rectToString(
+ destElement.getBoundingClientRect()
+ )}`
+ );
+ }
+
+ function onDragEnter(aEvent) {
+ dragEnterEvent = aEvent;
+ if (logFunc) {
+ logFunc(`"${aEvent.type}" event is fired`);
+ }
+ if (aEvent.target != destElement) {
+ throw new Error('event target of "dragenter" is not destElement');
+ }
+ }
+ destWindow.addEventListener("dragenter", onDragEnter, {
+ capture: true,
+ });
+ try {
+ let event = createDragEventObject(
+ "dragenter",
+ destElement,
+ destWindow,
+ null,
+ dragEvent
+ );
+ sendDragEvent(event, destElement, destWindow);
+ if (!dragEnterEvent && !destElement.disabled) {
+ throw new Error('"dragenter" event is not fired');
+ }
+ if (dragEnterEvent && destElement.disabled) {
+ throw new Error(
+ '"dragenter" event should not be fired on disable element'
+ );
+ }
+ } finally {
+ destWindow.removeEventListener("dragenter", onDragEnter, {
+ capture: true,
+ });
+ }
+ }
+
+ let dragOverEvent;
+ function onDragOver(aEvent) {
+ dragOverEvent = aEvent;
+ if (logFunc) {
+ logFunc(`"${aEvent.type}" event is fired`);
+ }
+ if (aEvent.target != destElement) {
+ throw new Error('event target of "dragover" is not destElement');
+ }
+ }
+ destWindow.addEventListener("dragover", onDragOver, { capture: true });
+ try {
+ // dragover and drop are only fired to a valid drop target. If the
+ // destElement parameter is null, this function is being used to
+ // simulate a drag'n'drop over an invalid drop target.
+ let event = createDragEventObject(
+ "dragover",
+ destElement,
+ destWindow,
+ null,
+ dragEvent
+ );
+ sendDragEvent(event, destElement, destWindow);
+ if (!dragOverEvent && !destElement.disabled) {
+ throw new Error('"dragover" event is not fired');
+ }
+ if (dragEnterEvent && destElement.disabled) {
+ throw new Error(
+ '"dragover" event should not be fired on disable element'
+ );
+ }
+ } finally {
+ destWindow.removeEventListener("dragover", onDragOver, {
+ capture: true,
+ });
+ }
+
+ await new Promise(r => setTimeout(r, 0));
+
+ // If there is not accept to drop the data, "drop" event shouldn't be
+ // fired.
+ // XXX nsIDragSession.canDrop is different only on Linux. It must be
+ // a bug of gtk/nsDragService since it manages `mCanDrop` by itself.
+ // Thus, we should use nsIDragSession.dragAction instead.
+ if (session.dragAction != _EU_Ci.nsIDragService.DRAGDROP_ACTION_NONE) {
+ let dropEvent;
+ function onDrop(aEvent) {
+ dropEvent = aEvent;
+ if (logFunc) {
+ logFunc(`"${aEvent.type}" event is fired`);
+ }
+ if (
+ !_nodeIsFlattenedTreeDescendantOf(
+ _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
+ destElement
+ )
+ ) {
+ throw new Error(
+ 'event target of "drop" is not destElement nor its descendant'
+ );
+ }
+ }
+ destWindow.addEventListener("drop", onDrop, { capture: true });
+ try {
+ let event = createDragEventObject(
+ "drop",
+ destElement,
+ destWindow,
+ null,
+ dragEvent
+ );
+ sendDragEvent(event, destElement, destWindow);
+ if (!dropEvent && session.canDrop) {
+ throw new Error('"drop" event is not fired');
+ }
+ } finally {
+ destWindow.removeEventListener("drop", onDrop, { capture: true });
+ }
+ return;
+ }
+ }
+
+ // Since we don't synthesize drop event, we need to set drag end point
+ // explicitly for "dragEnd" event which will be fired by
+ // endDragSession().
+ dragEvent.clientX = finalX;
+ dragEvent.clientY = finalY;
+ let event = createDragEventObject(
+ "dragend",
+ destElement || srcElement,
+ destElement ? srcWindow : destWindow,
+ null,
+ dragEvent
+ );
+ session.setDragEndPointForTests(event.screenX, event.screenY);
+ } finally {
+ await new Promise(r => setTimeout(r, 0));
+
+ if (ds.getCurrentSession()) {
+ const sourceNode = ds.sourceNode;
+ let dragEndEvent;
+ function onDragEnd(aEvent) {
+ dragEndEvent = aEvent;
+ if (logFunc) {
+ logFunc(`"${aEvent.type}" event is fired`);
+ }
+ if (
+ !_nodeIsFlattenedTreeDescendantOf(
+ _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
+ srcElement
+ ) &&
+ _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) != editingHost
+ ) {
+ throw new Error(
+ 'event target of "dragend" is not srcElement nor its descendant'
+ );
+ }
+ if (expectSrcElementDisconnected) {
+ throw new Error(
+ `"dragend" event shouldn't be fired when the source node is disconnected (the source node is ${
+ sourceNode?.isConnected ? "connected" : "null or disconnected"
+ })`
+ );
+ }
+ }
+ srcWindow.addEventListener("dragend", onDragEnd, { capture: true });
+ try {
+ ds.endDragSession(true, _parseModifiers(dragEvent));
+ if (!expectSrcElementDisconnected && !dragEndEvent) {
+ // eslint-disable-next-line no-unsafe-finally
+ throw new Error(
+ `"dragend" event is not fired by nsIDragService.endDragSession()${
+ ds.sourceNode && !ds.sourceNode.isConnected
+ ? "(sourceNode was disconnected)"
+ : ""
+ }`
+ );
+ }
+ } finally {
+ srcWindow.removeEventListener("dragend", onDragEnd, { capture: true });
+ }
+ }
+ _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(false);
+ if (logFunc) {
+ logFunc("synthesizePlainDragAndDrop() -- END");
+ }
+ }
+}
+
+function _checkDataTransferItems(aDataTransfer, aExpectedDragData) {
+ try {
+ // We must wrap only in plain mochitests, not chrome
+ let dataTransfer = _EU_maybeWrap(aDataTransfer);
+ if (!dataTransfer) {
+ return null;
+ }
+ if (
+ aExpectedDragData == null ||
+ dataTransfer.mozItemCount != aExpectedDragData.length
+ ) {
+ return dataTransfer;
+ }
+ for (let i = 0; i < dataTransfer.mozItemCount; i++) {
+ let dtTypes = dataTransfer.mozTypesAt(i);
+ if (dtTypes.length != aExpectedDragData[i].length) {
+ return dataTransfer;
+ }
+ for (let j = 0; j < dtTypes.length; j++) {
+ if (dtTypes[j] != aExpectedDragData[i][j].type) {
+ return dataTransfer;
+ }
+ let dtData = dataTransfer.mozGetDataAt(dtTypes[j], i);
+ if (aExpectedDragData[i][j].eqTest) {
+ if (
+ !aExpectedDragData[i][j].eqTest(
+ dtData,
+ aExpectedDragData[i][j].data
+ )
+ ) {
+ return dataTransfer;
+ }
+ } else if (aExpectedDragData[i][j].data != dtData) {
+ return dataTransfer;
+ }
+ }
+ }
+ } catch (ex) {
+ return ex;
+ }
+ return true;
+}
+
+/**
+ * This callback type is used with ``synthesizePlainDragAndCancel()``.
+ * It should compare ``actualData`` and ``expectedData`` and return
+ * true if the two should be considered equal, false otherwise.
+ *
+ * @callback eqTest
+ * @param {*} actualData
+ * @param {*} expectedData
+ * @return {boolean}
+ */
+
+/**
+ * synthesizePlainDragAndCancel() synthesizes drag start with
+ * synthesizePlainDragAndDrop(), but always cancel it with preventing default
+ * of "dragstart". Additionally, this checks whether the dataTransfer of
+ * "dragstart" event has only expected items.
+ *
+ * @param {Object} aParams
+ * The params which is set to the argument of ``synthesizePlainDragAndDrop()``.
+ * @param {Array} aExpectedDataTransferItems
+ * All expected dataTransfer items.
+ * This data is in the format:
+ *
+ * [
+ * [
+ * {"type": value, "data": value, eqTest: function}
+ * ...,
+ * ],
+ * ...
+ * ]
+ *
+ * This can also be null.
+ * You can optionally provide ``eqTest`` {@type eqTest} if the
+ * comparison to the expected data transfer items can't be done
+ * with x == y;
+ * @return {boolean}
+ * true if aExpectedDataTransferItems matches with
+ * DragEvent.dataTransfer of "dragstart" event.
+ * Otherwise, the dataTransfer object (may be null) or
+ * thrown exception, NOT false. Therefore, you shouldn't
+ * use.
+ */
+async function synthesizePlainDragAndCancel(
+ aParams,
+ aExpectedDataTransferItems
+) {
+ let srcElement = aParams.srcSelection
+ ? _computeSrcElementFromSrcSelection(aParams.srcSelection)
+ : aParams.srcElement;
+ let result;
+ function onDragStart(aEvent) {
+ aEvent.preventDefault();
+ result = _checkDataTransferItems(
+ aEvent.dataTransfer,
+ aExpectedDataTransferItems
+ );
+ }
+ SpecialPowers.addSystemEventListener(
+ srcElement.ownerDocument,
+ "dragstart",
+ onDragStart,
+ { capture: true }
+ );
+ try {
+ aParams.expectCancelDragStart = true;
+ await synthesizePlainDragAndDrop(aParams);
+ } finally {
+ SpecialPowers.removeSystemEventListener(
+ srcElement.ownerDocument,
+ "dragstart",
+ onDragStart,
+ { capture: true }
+ );
+ }
+ return result;
+}
+
+class EventCounter {
+ constructor(aTarget, aType, aOptions = {}) {
+ this.target = aTarget;
+ this.type = aType;
+ this.options = aOptions;
+
+ this.eventCount = 0;
+ // Bug 1512817:
+ // SpecialPowers is picky and needs to be passed an explicit reference to
+ // the function to be called. To avoid having to bind "this", we therefore
+ // define the method this way, via a property.
+ this.handleEvent = aEvent => {
+ this.eventCount++;
+ };
+
+ if (aOptions.mozSystemGroup) {
+ SpecialPowers.addSystemEventListener(
+ aTarget,
+ aType,
+ this.handleEvent,
+ aOptions.capture
+ );
+ } else {
+ aTarget.addEventListener(aType, this, aOptions);
+ }
+ }
+
+ unregister() {
+ if (this.options.mozSystemGroup) {
+ SpecialPowers.removeSystemEventListener(
+ this.target,
+ this.type,
+ this.handleEvent,
+ this.options.capture
+ );
+ } else {
+ this.target.removeEventListener(this.type, this, this.options);
+ }
+ }
+
+ get count() {
+ return this.eventCount;
+ }
+}
diff --git a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
new file mode 100644
index 0000000000..fa86116c92
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
@@ -0,0 +1,180 @@
+const { ExtensionTestCommon } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionTestCommon.sys.mjs"
+);
+
+var ExtensionTestUtils = {
+ // Shortcut to more easily access WebExtensionPolicy.backgroundServiceWorkerEnabled
+ // from mochitest-plain tests.
+ getBackgroundServiceWorkerEnabled() {
+ return ExtensionTestCommon.getBackgroundServiceWorkerEnabled();
+ },
+
+ // A test helper used to check if the pref "extension.backgroundServiceWorker.forceInTestExtension"
+ // is set to true.
+ isInBackgroundServiceWorkerTests() {
+ return ExtensionTestCommon.isInBackgroundServiceWorkerTests();
+ },
+
+ get testAssertions() {
+ return ExtensionTestCommon.testAssertions;
+ },
+};
+
+ExtensionTestUtils.loadExtension = function (ext) {
+ // Cleanup functions need to be registered differently depending on
+ // whether we're in browser chrome or plain mochitests.
+ var registerCleanup;
+ /* global registerCleanupFunction */
+ if (typeof registerCleanupFunction != "undefined") {
+ registerCleanup = registerCleanupFunction;
+ } else {
+ registerCleanup = SimpleTest.registerCleanupFunction.bind(SimpleTest);
+ }
+
+ var testResolve;
+ var testDone = new Promise(resolve => {
+ testResolve = resolve;
+ });
+
+ var messageHandler = new Map();
+ var messageAwaiter = new Map();
+
+ var messageQueue = new Set();
+
+ registerCleanup(() => {
+ if (messageQueue.size) {
+ let names = Array.from(messageQueue, ([msg]) => msg);
+ SimpleTest.is(JSON.stringify(names), "[]", "message queue is empty");
+ }
+ if (messageAwaiter.size) {
+ let names = Array.from(messageAwaiter.keys());
+ SimpleTest.is(
+ JSON.stringify(names),
+ "[]",
+ "no tasks awaiting on messages"
+ );
+ }
+ });
+
+ function checkMessages() {
+ for (let message of messageQueue) {
+ let [msg, ...args] = message;
+
+ let listener = messageAwaiter.get(msg);
+ if (listener) {
+ messageQueue.delete(message);
+ messageAwaiter.delete(msg);
+
+ listener.resolve(...args);
+ return;
+ }
+ }
+ }
+
+ function checkDuplicateListeners(msg) {
+ if (messageHandler.has(msg) || messageAwaiter.has(msg)) {
+ throw new Error("only one message handler allowed");
+ }
+ }
+
+ function testHandler(kind, pass, msg, ...args) {
+ if (kind == "test-eq") {
+ let [expected, actual] = args;
+ SimpleTest.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`);
+ } else if (kind == "test-log") {
+ SimpleTest.info(msg);
+ } else if (kind == "test-result") {
+ SimpleTest.ok(pass, msg);
+ }
+ }
+
+ var handler = {
+ async testResult(kind, pass, msg, ...args) {
+ if (kind == "test-done") {
+ SimpleTest.ok(pass, msg);
+ await testResolve(msg);
+ }
+ testHandler(kind, pass, msg, ...args);
+ },
+
+ testMessage(msg, ...args) {
+ var msgHandler = messageHandler.get(msg);
+ if (msgHandler) {
+ msgHandler(...args);
+ } else {
+ messageQueue.add([msg, ...args]);
+ checkMessages();
+ }
+ },
+ };
+
+ // Mimic serialization of functions as done in `Extension.generateXPI` and
+ // `Extension.generateZipFile` because functions are dropped when `ext` object
+ // is sent to the main process via the message manager.
+ ext = Object.assign({}, ext);
+ if (ext.files) {
+ ext.files = Object.assign({}, ext.files);
+ for (let filename of Object.keys(ext.files)) {
+ let file = ext.files[filename];
+ if (typeof file === "function" || Array.isArray(file)) {
+ ext.files[filename] = ExtensionTestCommon.serializeScript(file);
+ }
+ }
+ }
+ if ("background" in ext) {
+ ext.background = ExtensionTestCommon.serializeScript(ext.background);
+ }
+
+ var extension = SpecialPowers.loadExtension(ext, handler);
+
+ registerCleanup(async () => {
+ if (extension.state == "pending" || extension.state == "running") {
+ SimpleTest.ok(false, "Extension left running at test shutdown");
+ await extension.unload();
+ } else if (extension.state == "unloading") {
+ SimpleTest.ok(false, "Extension not fully unloaded at test shutdown");
+ }
+ });
+
+ extension.awaitMessage = msg => {
+ return new Promise(resolve => {
+ checkDuplicateListeners(msg);
+
+ messageAwaiter.set(msg, { resolve });
+ checkMessages();
+ });
+ };
+
+ extension.onMessage = (msg, callback) => {
+ checkDuplicateListeners(msg);
+ messageHandler.set(msg, callback);
+ };
+
+ extension.awaitFinish = msg => {
+ return testDone.then(actual => {
+ if (msg) {
+ SimpleTest.is(actual, msg, "test result correct");
+ }
+ return actual;
+ });
+ };
+
+ SimpleTest.info(`Extension loaded`);
+ return extension;
+};
+
+ExtensionTestUtils.failOnSchemaWarnings = (warningsAsErrors = true) => {
+ let prefName = "extensions.webextensions.warnings-as-errors";
+ let prefPromise = SpecialPowers.setBoolPref(prefName, warningsAsErrors);
+ if (!warningsAsErrors) {
+ let registerCleanup;
+ if (typeof registerCleanupFunction != "undefined") {
+ registerCleanup = registerCleanupFunction;
+ } else {
+ registerCleanup = SimpleTest.registerCleanupFunction.bind(SimpleTest);
+ }
+ registerCleanup(() => SpecialPowers.setBoolPref(prefName, true));
+ }
+ // In mochitests, setBoolPref is async.
+ return prefPromise.then(() => {});
+};
diff --git a/testing/mochitest/tests/SimpleTest/LogController.js b/testing/mochitest/tests/SimpleTest/LogController.js
new file mode 100644
index 0000000000..29580022f8
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/LogController.js
@@ -0,0 +1,96 @@
+/* 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/. */
+
+var LogController = {}; //create the logger object
+
+LogController.counter = 0; //current log message number
+LogController.listeners = [];
+LogController.logLevel = {
+ FATAL: 50,
+ ERROR: 40,
+ WARNING: 30,
+ INFO: 20,
+ DEBUG: 10,
+};
+
+/* set minimum logging level */
+LogController.logLevelAtLeast = function (minLevel) {
+ if (typeof minLevel == "string") {
+ minLevel = LogController.logLevel[minLevel];
+ }
+ return function (msg) {
+ var msgLevel = msg.level;
+ if (typeof msgLevel == "string") {
+ msgLevel = LogController.logLevel[msgLevel];
+ }
+ return msgLevel >= minLevel;
+ };
+};
+
+/* creates the log message with the given level and info */
+LogController.createLogMessage = function (level, info) {
+ var msg = {};
+ msg.num = LogController.counter;
+ msg.level = level;
+ msg.info = info;
+ msg.timestamp = new Date();
+ return msg;
+};
+
+/* helper method to return a sub-array */
+LogController.extend = function (args, skip) {
+ var ret = [];
+ for (var i = skip; i < args.length; i++) {
+ ret.push(args[i]);
+ }
+ return ret;
+};
+
+/* logs message with given level. Currently used locally by log() and error() */
+LogController.logWithLevel = function (level, message /*, ...*/) {
+ var msg = LogController.createLogMessage(
+ level,
+ LogController.extend(arguments, 1)
+ );
+ LogController.dispatchListeners(msg);
+ LogController.counter += 1;
+};
+
+/* log with level INFO */
+LogController.log = function (message /*, ...*/) {
+ LogController.logWithLevel("INFO", message);
+};
+
+/* log with level ERROR */
+LogController.error = function (message /*, ...*/) {
+ LogController.logWithLevel("ERROR", message);
+};
+
+/* send log message to listeners */
+LogController.dispatchListeners = function (msg) {
+ for (var k in LogController.listeners) {
+ var pair = LogController.listeners[k];
+ if (pair.ident != k || (pair[0] && !pair[0](msg))) {
+ continue;
+ }
+ pair[1](msg);
+ }
+};
+
+/* add a listener to this log given an identifier, a filter (can be null) and the listener object */
+LogController.addListener = function (ident, filter, listener) {
+ if (typeof filter == "string") {
+ filter = LogController.logLevelAtLeast(filter);
+ } else if (filter !== null && typeof filter !== "function") {
+ throw new Error("Filter must be a string, a function, or null");
+ }
+ var entry = [filter, listener];
+ entry.ident = ident;
+ LogController.listeners[ident] = entry;
+};
+
+/* remove a listener from this log */
+LogController.removeListener = function (ident) {
+ delete LogController.listeners[ident];
+};
diff --git a/testing/mochitest/tests/SimpleTest/MemoryStats.js b/testing/mochitest/tests/SimpleTest/MemoryStats.js
new file mode 100644
index 0000000000..40548697ea
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/MemoryStats.js
@@ -0,0 +1,131 @@
+/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */
+/* vim:set ts=4 sw=4 sts=4 et: */
+/* 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/. */
+
+var MemoryStats = {};
+
+/**
+ * Statistics that we want to retrieve and display after every test is
+ * done. The keys of this table are intended to be identical to the
+ * relevant attributes of nsIMemoryReporterManager. However, since
+ * nsIMemoryReporterManager doesn't necessarily support all these
+ * statistics in all build configurations, we also use this table to
+ * tell us whether statistics are supported or not.
+ */
+var MEM_STAT_UNKNOWN = 0;
+var MEM_STAT_UNSUPPORTED = 1;
+var MEM_STAT_SUPPORTED = 2;
+
+MemoryStats._hasMemoryStatistics = {};
+MemoryStats._hasMemoryStatistics.vsize = MEM_STAT_UNKNOWN;
+MemoryStats._hasMemoryStatistics.vsizeMaxContiguous = MEM_STAT_UNKNOWN;
+MemoryStats._hasMemoryStatistics.residentFast = MEM_STAT_UNKNOWN;
+MemoryStats._hasMemoryStatistics.heapAllocated = MEM_STAT_UNKNOWN;
+
+MemoryStats._getService = function (className, interfaceName) {
+ var service;
+ try {
+ service = Cc[className].getService(Ci[interfaceName]);
+ } catch (e) {
+ service = SpecialPowers.Cc[className].getService(
+ SpecialPowers.Ci[interfaceName]
+ );
+ }
+ return service;
+};
+
+MemoryStats._nsIFile = function (pathname) {
+ var f;
+ var contractID = "@mozilla.org/file/local;1";
+ try {
+ f = Cc[contractID].createInstance(Ci.nsIFile);
+ } catch (e) {
+ f = SpecialPowers.Cc[contractID].createInstance(SpecialPowers.Ci.nsIFile);
+ }
+ f.initWithPath(pathname);
+ return f;
+};
+
+MemoryStats.constructPathname = function (directory, basename) {
+ var d = MemoryStats._nsIFile(directory);
+ d.append(basename);
+ return d.path;
+};
+
+MemoryStats.dump = function (
+ testNumber,
+ testURL,
+ dumpOutputDirectory,
+ dumpAboutMemory,
+ dumpDMD
+) {
+ // Use dump because treeherder uses --quiet, which drops 'info'
+ // from the structured logger.
+ var info = function (message) {
+ dump(message + "\n");
+ };
+
+ var mrm = MemoryStats._getService(
+ "@mozilla.org/memory-reporter-manager;1",
+ "nsIMemoryReporterManager"
+ );
+ var statMessage = "";
+ for (var stat in MemoryStats._hasMemoryStatistics) {
+ var supported = MemoryStats._hasMemoryStatistics[stat];
+ var firstAccess = false;
+ if (supported == MEM_STAT_UNKNOWN) {
+ firstAccess = true;
+ try {
+ void mrm[stat];
+ supported = MEM_STAT_SUPPORTED;
+ } catch (e) {
+ supported = MEM_STAT_UNSUPPORTED;
+ }
+ MemoryStats._hasMemoryStatistics[stat] = supported;
+ }
+ if (supported == MEM_STAT_SUPPORTED) {
+ var sizeInMB = Math.round(mrm[stat] / (1024 * 1024));
+ statMessage += " | " + stat + " " + sizeInMB + "MB";
+ } else if (firstAccess) {
+ info(
+ "MEMORY STAT " + stat + " not supported in this build configuration."
+ );
+ }
+ }
+ if (statMessage.length) {
+ info("MEMORY STAT" + statMessage);
+ }
+
+ if (dumpAboutMemory) {
+ var basename = "about-memory-" + testNumber + ".json.gz";
+ var dumpfile = MemoryStats.constructPathname(dumpOutputDirectory, basename);
+ info(testURL + " | MEMDUMP-START " + dumpfile);
+ let md = MemoryStats._getService(
+ "@mozilla.org/memory-info-dumper;1",
+ "nsIMemoryInfoDumper"
+ );
+ md.dumpMemoryReportsToNamedFile(
+ dumpfile,
+ function () {
+ info("TEST-INFO | " + testURL + " | MEMDUMP-END");
+ },
+ null,
+ /* anonymize = */ false,
+ /* minimize memory usage = */ false
+ );
+ }
+
+ if (dumpDMD) {
+ let md = MemoryStats._getService(
+ "@mozilla.org/memory-info-dumper;1",
+ "nsIMemoryInfoDumper"
+ );
+ md.dumpMemoryInfoToTempDir(
+ String(testNumber),
+ /* anonymize = */ false,
+ /* minimize memory usage = */ false
+ );
+ }
+};
diff --git a/testing/mochitest/tests/SimpleTest/MockObjects.js b/testing/mochitest/tests/SimpleTest/MockObjects.js
new file mode 100644
index 0000000000..5782707c04
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/MockObjects.js
@@ -0,0 +1,95 @@
+/* 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/. */
+
+/**
+ * Allows registering a mock XPCOM component, that temporarily replaces the
+ * original one when an object implementing a given ContractID is requested
+ * using createInstance.
+ *
+ * @param aContractID
+ * The ContractID of the component to replace, for example
+ * "@mozilla.org/filepicker;1".
+ *
+ * @param aReplacementCtor
+ * The constructor function for the JavaScript object that will be
+ * created every time createInstance is called. This object must
+ * implement QueryInterface and provide the XPCOM interfaces required by
+ * the specified ContractID (for example
+ * Components.interfaces.nsIFilePicker).
+ */
+
+function MockObjectRegisterer(aContractID, aReplacementCtor) {
+ this._contractID = aContractID;
+ this._replacementCtor = aReplacementCtor;
+}
+
+MockObjectRegisterer.prototype = {
+ /**
+ * Replaces the current factory with one that returns a new mock object.
+ *
+ * After register() has been called, it is mandatory to call unregister() to
+ * restore the original component. Usually, you should use a try-catch block
+ * to ensure that unregister() is called.
+ */
+ register: function MOR_register() {
+ if (this._originalCID) {
+ throw new Error("Invalid object state when calling register()");
+ }
+
+ // Define a factory that creates a new object using the given constructor.
+ var isChrome = location.protocol == "chrome:";
+ var providedConstructor = this._replacementCtor;
+ this._mockFactory = {
+ createInstance: function MF_createInstance(aIid) {
+ var inst = new providedConstructor().QueryInterface(aIid);
+ if (!isChrome) {
+ inst = SpecialPowers.wrapCallbackObject(inst);
+ }
+ return inst;
+ },
+ };
+ if (!isChrome) {
+ this._mockFactory = SpecialPowers.wrapCallbackObject(this._mockFactory);
+ }
+
+ var retVal = SpecialPowers.swapFactoryRegistration(
+ null,
+ this._contractID,
+ this._mockFactory
+ );
+ if ("error" in retVal) {
+ throw new Error("ERROR: " + retVal.error);
+ } else {
+ this._originalCID = retVal.originalCID;
+ }
+ },
+
+ /**
+ * Restores the original factory.
+ */
+ unregister: function MOR_unregister() {
+ if (!this._originalCID) {
+ throw new Error("Invalid object state when calling unregister()");
+ }
+
+ // Free references to the mock factory.
+ SpecialPowers.swapFactoryRegistration(this._originalCID, this._contractID);
+
+ // Allow registering a mock factory again later.
+ this._originalCID = null;
+ this._mockFactory = null;
+ },
+
+ // --- Private methods and properties ---
+
+ /**
+ * The factory of the component being replaced.
+ */
+ _originalCID: null,
+
+ /**
+ * The nsIFactory that was automatically generated by this object.
+ */
+ _mockFactory: null,
+};
diff --git a/testing/mochitest/tests/SimpleTest/MozillaLogger.js b/testing/mochitest/tests/SimpleTest/MozillaLogger.js
new file mode 100644
index 0000000000..13ed5bd8f5
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/MozillaLogger.js
@@ -0,0 +1,102 @@
+/**
+ * MozillaLogger, a base class logger that just logs to stdout.
+ */
+
+"use strict";
+
+function formatLogMessage(msg) {
+ return msg.info.join(" ") + "\n";
+}
+
+function importMJS(mjs) {
+ if (typeof ChromeUtils === "object") {
+ return ChromeUtils.importESModule(mjs);
+ }
+ /* globals SpecialPowers */
+ return SpecialPowers.ChromeUtils.importESModule(mjs);
+}
+
+// When running in release builds, we get a fake Components object in
+// web contexts, so we need to check that the Components object is sane,
+// too, not just that it exists.
+let haveComponents =
+ typeof Components === "object" &&
+ typeof Components.Constructor === "function";
+
+let CC = (
+ haveComponents ? Components : SpecialPowers.wrap(SpecialPowers.Components)
+).Constructor;
+
+let ConverterOutputStream = CC(
+ "@mozilla.org/intl/converter-output-stream;1",
+ "nsIConverterOutputStream",
+ "init"
+);
+
+class MozillaLogger {
+ get logCallback() {
+ return msg => {
+ this.log(formatLogMessage(msg));
+ };
+ }
+
+ log(msg) {
+ dump(msg);
+ }
+
+ close() {}
+}
+
+/**
+ * MozillaFileLogger, a log listener that can write to a local file.
+ * intended to be run from chrome space
+ */
+
+/** Init the file logger with the absolute path to the file.
+ It will create and append if the file already exists **/
+class MozillaFileLogger extends MozillaLogger {
+ constructor(aPath) {
+ super();
+
+ const { FileUtils } = importMJS("resource://gre/modules/FileUtils.sys.mjs");
+
+ this._file = FileUtils.File(aPath);
+ this._foStream = FileUtils.openFileOutputStream(
+ this._file,
+ FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_APPEND
+ );
+
+ this._converter = ConverterOutputStream(this._foStream, "UTF-8");
+ }
+
+ get logCallback() {
+ return msg => {
+ if (this._converter) {
+ var data = formatLogMessage(msg);
+ this.log(data);
+
+ if (data.includes("SimpleTest FINISH")) {
+ this.close();
+ }
+ }
+ };
+ }
+
+ log(msg) {
+ if (this._converter) {
+ this._converter.writeString(msg);
+ }
+ }
+
+ close() {
+ this._converter.flush();
+ this._converter.close();
+
+ this._foStream = null;
+ this._converter = null;
+ this._file = null;
+ }
+}
+
+this.MozillaLogger = MozillaLogger;
+this.MozillaFileLogger = MozillaFileLogger;
diff --git a/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js
new file mode 100644
index 0000000000..b256269e71
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js
@@ -0,0 +1,369 @@
+/**
+ * This file defines all virtual keycodes for synthesizeNativeKey() of
+ * EventUtils.js and nsIDOMWindowUtils.sendNativeKeyEvent().
+ * These values are defined in each platform's SDK or documents.
+ */
+
+// Windows
+// Windows' native key code values may include scan code value which can be
+// retrieved with |((code & 0xFFFF0000 >> 16)|. If the value is 0, it will
+// be computed with active keyboard layout automatically.
+// FYI: Don't define scan code here for printable keys, numeric keys and
+// IME keys because they depend on active keyboard layout.
+// XXX: Although, ABNT C1 key depends on keyboard layout in strictly speaking.
+// However, computing its scan code from the virtual keycode,
+// WIN_VK_ABNT_C1, doesn't work fine (computed as 0x0073, "IntlRo").
+// Therefore, we should specify it here explicitly (it should be 0x0056,
+// "IntlBackslash"). Fortunately, the key always generates 0x0056 with
+// any keyboard layouts as far as I've tested. So, this must be safe to
+// test new regressions.
+
+const WIN_VK_LBUTTON = 0x00000001;
+const WIN_VK_RBUTTON = 0x00000002;
+const WIN_VK_CANCEL = 0xe0460003;
+const WIN_VK_MBUTTON = 0x00000004;
+const WIN_VK_XBUTTON1 = 0x00000005;
+const WIN_VK_XBUTTON2 = 0x00000006;
+const WIN_VK_BACK = 0x000e0008;
+const WIN_VK_TAB = 0x000f0009;
+const WIN_VK_CLEAR = 0x004c000c;
+const WIN_VK_RETURN = 0x001c000d;
+const WIN_VK_SHIFT = 0x002a0010;
+const WIN_VK_CONTROL = 0x001d0011;
+const WIN_VK_MENU = 0x00380012;
+const WIN_VK_PAUSE = 0x00450013;
+const WIN_VK_CAPITAL = 0x003a0014;
+const WIN_VK_KANA = 0x00000015;
+const WIN_VK_HANGUEL = 0x00000015;
+const WIN_VK_HANGUL = 0x00000015;
+const WIN_VK_JUNJA = 0x00000017;
+const WIN_VK_FINAL = 0x00000018;
+const WIN_VK_HANJA = 0x00000019;
+const WIN_VK_KANJI = 0x00000019;
+const WIN_VK_ESCAPE = 0x0001001b;
+const WIN_VK_CONVERT = 0x0000001c;
+const WIN_VK_NONCONVERT = 0x0000001d;
+const WIN_VK_ACCEPT = 0x0000001e;
+const WIN_VK_MODECHANGE = 0x0000001f;
+const WIN_VK_SPACE = 0x00390020;
+const WIN_VK_PRIOR = 0xe0490021;
+const WIN_VK_NEXT = 0xe0510022;
+const WIN_VK_END = 0xe04f0023;
+const WIN_VK_HOME = 0xe0470024;
+const WIN_VK_LEFT = 0xe04b0025;
+const WIN_VK_UP = 0xe0480026;
+const WIN_VK_RIGHT = 0xe04d0027;
+const WIN_VK_DOWN = 0xe0500028;
+const WIN_VK_SELECT = 0x00000029;
+const WIN_VK_PRINT = 0x0000002a;
+const WIN_VK_EXECUTE = 0x0000002b;
+const WIN_VK_SNAPSHOT = 0xe037002c;
+const WIN_VK_INSERT = 0xe052002d;
+const WIN_VK_DELETE = 0xe053002e;
+const WIN_VK_HELP = 0x0000002f;
+const WIN_VK_0 = 0x00000030;
+const WIN_VK_1 = 0x00000031;
+const WIN_VK_2 = 0x00000032;
+const WIN_VK_3 = 0x00000033;
+const WIN_VK_4 = 0x00000034;
+const WIN_VK_5 = 0x00000035;
+const WIN_VK_6 = 0x00000036;
+const WIN_VK_7 = 0x00000037;
+const WIN_VK_8 = 0x00000038;
+const WIN_VK_9 = 0x00000039;
+const WIN_VK_A = 0x00000041;
+const WIN_VK_B = 0x00000042;
+const WIN_VK_C = 0x00000043;
+const WIN_VK_D = 0x00000044;
+const WIN_VK_E = 0x00000045;
+const WIN_VK_F = 0x00000046;
+const WIN_VK_G = 0x00000047;
+const WIN_VK_H = 0x00000048;
+const WIN_VK_I = 0x00000049;
+const WIN_VK_J = 0x0000004a;
+const WIN_VK_K = 0x0000004b;
+const WIN_VK_L = 0x0000004c;
+const WIN_VK_M = 0x0000004d;
+const WIN_VK_N = 0x0000004e;
+const WIN_VK_O = 0x0000004f;
+const WIN_VK_P = 0x00000050;
+const WIN_VK_Q = 0x00000051;
+const WIN_VK_R = 0x00000052;
+const WIN_VK_S = 0x00000053;
+const WIN_VK_T = 0x00000054;
+const WIN_VK_U = 0x00000055;
+const WIN_VK_V = 0x00000056;
+const WIN_VK_W = 0x00000057;
+const WIN_VK_X = 0x00000058;
+const WIN_VK_Y = 0x00000059;
+const WIN_VK_Z = 0x0000005a;
+const WIN_VK_LWIN = 0xe05b005b;
+const WIN_VK_RWIN = 0xe05c005c;
+const WIN_VK_APPS = 0xe05d005d;
+const WIN_VK_SLEEP = 0x0000005f;
+const WIN_VK_NUMPAD0 = 0x00520060;
+const WIN_VK_NUMPAD1 = 0x004f0061;
+const WIN_VK_NUMPAD2 = 0x00500062;
+const WIN_VK_NUMPAD3 = 0x00510063;
+const WIN_VK_NUMPAD4 = 0x004b0064;
+const WIN_VK_NUMPAD5 = 0x004c0065;
+const WIN_VK_NUMPAD6 = 0x004d0066;
+const WIN_VK_NUMPAD7 = 0x00470067;
+const WIN_VK_NUMPAD8 = 0x00480068;
+const WIN_VK_NUMPAD9 = 0x00490069;
+const WIN_VK_MULTIPLY = 0x0037006a;
+const WIN_VK_ADD = 0x004e006b;
+const WIN_VK_SEPARATOR = 0x0000006c;
+const WIN_VK_OEM_NEC_SEPARATE = 0x0000006c;
+const WIN_VK_SUBTRACT = 0x004a006d;
+const WIN_VK_DECIMAL = 0x0053006e;
+const WIN_VK_DIVIDE = 0xe035006f;
+const WIN_VK_F1 = 0x003b0070;
+const WIN_VK_F2 = 0x003c0071;
+const WIN_VK_F3 = 0x003d0072;
+const WIN_VK_F4 = 0x003e0073;
+const WIN_VK_F5 = 0x003f0074;
+const WIN_VK_F6 = 0x00400075;
+const WIN_VK_F7 = 0x00410076;
+const WIN_VK_F8 = 0x00420077;
+const WIN_VK_F9 = 0x00430078;
+const WIN_VK_F10 = 0x00440079;
+const WIN_VK_F11 = 0x0057007a;
+const WIN_VK_F12 = 0x0058007b;
+const WIN_VK_F13 = 0x0064007c;
+const WIN_VK_F14 = 0x0065007d;
+const WIN_VK_F15 = 0x0066007e;
+const WIN_VK_F16 = 0x0067007f;
+const WIN_VK_F17 = 0x00680080;
+const WIN_VK_F18 = 0x00690081;
+const WIN_VK_F19 = 0x006a0082;
+const WIN_VK_F20 = 0x006b0083;
+const WIN_VK_F21 = 0x006c0084;
+const WIN_VK_F22 = 0x006d0085;
+const WIN_VK_F23 = 0x006e0086;
+const WIN_VK_F24 = 0x00760087;
+const WIN_VK_NUMLOCK = 0xe0450090;
+const WIN_VK_SCROLL = 0x00460091;
+const WIN_VK_OEM_FJ_JISHO = 0x00000092;
+const WIN_VK_OEM_NEC_EQUAL = 0x00000092;
+const WIN_VK_OEM_FJ_MASSHOU = 0x00000093;
+const WIN_VK_OEM_FJ_TOUROKU = 0x00000094;
+const WIN_VK_OEM_FJ_LOYA = 0x00000095;
+const WIN_VK_OEM_FJ_ROYA = 0x00000096;
+const WIN_VK_LSHIFT = 0x002a00a0;
+const WIN_VK_RSHIFT = 0x003600a1;
+const WIN_VK_LCONTROL = 0x001d00a2;
+const WIN_VK_RCONTROL = 0xe01d00a3;
+const WIN_VK_LMENU = 0x003800a4;
+const WIN_VK_RMENU = 0xe03800a5;
+const WIN_VK_BROWSER_BACK = 0xe06a00a6;
+const WIN_VK_BROWSER_FORWARD = 0xe06900a7;
+const WIN_VK_BROWSER_REFRESH = 0xe06700a8;
+const WIN_VK_BROWSER_STOP = 0xe06800a9;
+const WIN_VK_BROWSER_SEARCH = 0x000000aa;
+const WIN_VK_BROWSER_FAVORITES = 0xe06600ab;
+const WIN_VK_BROWSER_HOME = 0xe03200ac;
+const WIN_VK_VOLUME_MUTE = 0xe02000ad;
+const WIN_VK_VOLUME_DOWN = 0xe02e00ae;
+const WIN_VK_VOLUME_UP = 0xe03000af;
+const WIN_VK_MEDIA_NEXT_TRACK = 0xe01900b0;
+const WIN_VK_OEM_FJ_000 = 0x000000b0;
+const WIN_VK_MEDIA_PREV_TRACK = 0xe01000b1;
+const WIN_VK_OEM_FJ_EUQAL = 0x000000b1;
+const WIN_VK_MEDIA_STOP = 0xe02400b2;
+const WIN_VK_MEDIA_PLAY_PAUSE = 0xe02200b3;
+const WIN_VK_OEM_FJ_00 = 0x000000b3;
+const WIN_VK_LAUNCH_MAIL = 0xe06c00b4;
+const WIN_VK_LAUNCH_MEDIA_SELECT = 0xe06d00b5;
+const WIN_VK_LAUNCH_APP1 = 0xe06b00b6;
+const WIN_VK_LAUNCH_APP2 = 0xe02100b7;
+const WIN_VK_OEM_1 = 0x000000ba;
+const WIN_VK_OEM_PLUS = 0x000000bb;
+const WIN_VK_OEM_COMMA = 0x000000bc;
+const WIN_VK_OEM_MINUS = 0x000000bd;
+const WIN_VK_OEM_PERIOD = 0x000000be;
+const WIN_VK_OEM_2 = 0x000000bf;
+const WIN_VK_OEM_3 = 0x000000c0;
+const WIN_VK_ABNT_C1 = 0x005600c1;
+const WIN_VK_ABNT_C2 = 0x000000c2;
+const WIN_VK_OEM_4 = 0x000000db;
+const WIN_VK_OEM_5 = 0x000000dc;
+const WIN_VK_OEM_6 = 0x000000dd;
+const WIN_VK_OEM_7 = 0x000000de;
+const WIN_VK_OEM_8 = 0x000000df;
+const WIN_VK_OEM_NEC_DP1 = 0x000000e0;
+const WIN_VK_OEM_AX = 0x000000e1;
+const WIN_VK_OEM_NEC_DP2 = 0x000000e1;
+const WIN_VK_OEM_102 = 0x000000e2;
+const WIN_VK_OEM_NEC_DP3 = 0x000000e2;
+const WIN_VK_ICO_HELP = 0x000000e3;
+const WIN_VK_OEM_NEC_DP4 = 0x000000e3;
+const WIN_VK_ICO_00 = 0x000000e4;
+const WIN_VK_PROCESSKEY = 0x000000e5;
+const WIN_VK_ICO_CLEAR = 0x000000e6;
+const WIN_VK_PACKET = 0x000000e7;
+const WIN_VK_ERICSSON_BASE = 0x000000e8;
+const WIN_VK_OEM_RESET = 0x000000e9;
+const WIN_VK_OEM_JUMP = 0x000000ea;
+const WIN_VK_OEM_PA1 = 0x000000eb;
+const WIN_VK_OEM_PA2 = 0x000000ec;
+const WIN_VK_OEM_PA3 = 0x000000ed;
+const WIN_VK_OEM_WSCTRL = 0x000000ee;
+const WIN_VK_OEM_CUSEL = 0x000000ef;
+const WIN_VK_OEM_ATTN = 0x000000f0;
+const WIN_VK_OEM_FINISH = 0x000000f1;
+const WIN_VK_OEM_COPY = 0x000000f2;
+const WIN_VK_OEM_AUTO = 0x000000f3;
+const WIN_VK_OEM_ENLW = 0x000000f4;
+const WIN_VK_OEM_BACKTAB = 0x000000f5;
+const WIN_VK_ATTN = 0x000000f6;
+const WIN_VK_CRSEL = 0x000000f7;
+const WIN_VK_EXSEL = 0x000000f8;
+const WIN_VK_EREOF = 0x000000f9;
+const WIN_VK_PLAY = 0x000000fa;
+const WIN_VK_ZOOM = 0x000000fb;
+const WIN_VK_NONAME = 0x000000fc;
+const WIN_VK_PA1 = 0x000000fd;
+const WIN_VK_OEM_CLEAR = 0x000000fe;
+
+const WIN_VK_NUMPAD_RETURN = 0xe01c000d;
+const WIN_VK_NUMPAD_PRIOR = 0x00490021;
+const WIN_VK_NUMPAD_NEXT = 0x00510022;
+const WIN_VK_NUMPAD_END = 0x004f0023;
+const WIN_VK_NUMPAD_HOME = 0x00470024;
+const WIN_VK_NUMPAD_LEFT = 0x004b0025;
+const WIN_VK_NUMPAD_UP = 0x00480026;
+const WIN_VK_NUMPAD_RIGHT = 0x004d0027;
+const WIN_VK_NUMPAD_DOWN = 0x00500028;
+const WIN_VK_NUMPAD_INSERT = 0x0052002d;
+const WIN_VK_NUMPAD_DELETE = 0x0053002e;
+
+// Mac
+
+const MAC_VK_ANSI_A = 0x00;
+const MAC_VK_ANSI_S = 0x01;
+const MAC_VK_ANSI_D = 0x02;
+const MAC_VK_ANSI_F = 0x03;
+const MAC_VK_ANSI_H = 0x04;
+const MAC_VK_ANSI_G = 0x05;
+const MAC_VK_ANSI_Z = 0x06;
+const MAC_VK_ANSI_X = 0x07;
+const MAC_VK_ANSI_C = 0x08;
+const MAC_VK_ANSI_V = 0x09;
+const MAC_VK_ISO_Section = 0x0a;
+const MAC_VK_ANSI_B = 0x0b;
+const MAC_VK_ANSI_Q = 0x0c;
+const MAC_VK_ANSI_W = 0x0d;
+const MAC_VK_ANSI_E = 0x0e;
+const MAC_VK_ANSI_R = 0x0f;
+const MAC_VK_ANSI_Y = 0x10;
+const MAC_VK_ANSI_T = 0x11;
+const MAC_VK_ANSI_1 = 0x12;
+const MAC_VK_ANSI_2 = 0x13;
+const MAC_VK_ANSI_3 = 0x14;
+const MAC_VK_ANSI_4 = 0x15;
+const MAC_VK_ANSI_6 = 0x16;
+const MAC_VK_ANSI_5 = 0x17;
+const MAC_VK_ANSI_Equal = 0x18;
+const MAC_VK_ANSI_9 = 0x19;
+const MAC_VK_ANSI_7 = 0x1a;
+const MAC_VK_ANSI_Minus = 0x1b;
+const MAC_VK_ANSI_8 = 0x1c;
+const MAC_VK_ANSI_0 = 0x1d;
+const MAC_VK_ANSI_RightBracket = 0x1e;
+const MAC_VK_ANSI_O = 0x1f;
+const MAC_VK_ANSI_U = 0x20;
+const MAC_VK_ANSI_LeftBracket = 0x21;
+const MAC_VK_ANSI_I = 0x22;
+const MAC_VK_ANSI_P = 0x23;
+const MAC_VK_Return = 0x24;
+const MAC_VK_ANSI_L = 0x25;
+const MAC_VK_ANSI_J = 0x26;
+const MAC_VK_ANSI_Quote = 0x27;
+const MAC_VK_ANSI_K = 0x28;
+const MAC_VK_ANSI_Semicolon = 0x29;
+const MAC_VK_ANSI_Backslash = 0x2a;
+const MAC_VK_ANSI_Comma = 0x2b;
+const MAC_VK_ANSI_Slash = 0x2c;
+const MAC_VK_ANSI_N = 0x2d;
+const MAC_VK_ANSI_M = 0x2e;
+const MAC_VK_ANSI_Period = 0x2f;
+const MAC_VK_Tab = 0x30;
+const MAC_VK_Space = 0x31;
+const MAC_VK_ANSI_Grave = 0x32;
+const MAC_VK_Delete = 0x33;
+const MAC_VK_PC_Backspace = 0x33;
+const MAC_VK_Powerbook_KeypadEnter = 0x34;
+const MAC_VK_Escape = 0x35;
+const MAC_VK_RightCommand = 0x36;
+const MAC_VK_Command = 0x37;
+const MAC_VK_Shift = 0x38;
+const MAC_VK_CapsLock = 0x39;
+const MAC_VK_Option = 0x3a;
+const MAC_VK_Control = 0x3b;
+const MAC_VK_RightShift = 0x3c;
+const MAC_VK_RightOption = 0x3d;
+const MAC_VK_RightControl = 0x3e;
+const MAC_VK_Function = 0x3f;
+const MAC_VK_F17 = 0x40;
+const MAC_VK_ANSI_KeypadDecimal = 0x41;
+const MAC_VK_ANSI_KeypadMultiply = 0x43;
+const MAC_VK_ANSI_KeypadPlus = 0x45;
+const MAC_VK_ANSI_KeypadClear = 0x47;
+const MAC_VK_VolumeUp = 0x48;
+const MAC_VK_VolumeDown = 0x49;
+const MAC_VK_Mute = 0x4a;
+const MAC_VK_ANSI_KeypadDivide = 0x4b;
+const MAC_VK_ANSI_KeypadEnter = 0x4c;
+const MAC_VK_ANSI_KeypadMinus = 0x4e;
+const MAC_VK_F18 = 0x4f;
+const MAC_VK_F19 = 0x50;
+const MAC_VK_ANSI_KeypadEquals = 0x51;
+const MAC_VK_ANSI_Keypad0 = 0x52;
+const MAC_VK_ANSI_Keypad1 = 0x53;
+const MAC_VK_ANSI_Keypad2 = 0x54;
+const MAC_VK_ANSI_Keypad3 = 0x55;
+const MAC_VK_ANSI_Keypad4 = 0x56;
+const MAC_VK_ANSI_Keypad5 = 0x57;
+const MAC_VK_ANSI_Keypad6 = 0x58;
+const MAC_VK_ANSI_Keypad7 = 0x59;
+const MAC_VK_F20 = 0x5a;
+const MAC_VK_ANSI_Keypad8 = 0x5b;
+const MAC_VK_ANSI_Keypad9 = 0x5c;
+const MAC_VK_JIS_Yen = 0x5d;
+const MAC_VK_JIS_Underscore = 0x5e;
+const MAC_VK_JIS_KeypadComma = 0x5f;
+const MAC_VK_F5 = 0x60;
+const MAC_VK_F6 = 0x61;
+const MAC_VK_F7 = 0x62;
+const MAC_VK_F3 = 0x63;
+const MAC_VK_F8 = 0x64;
+const MAC_VK_F9 = 0x65;
+const MAC_VK_JIS_Eisu = 0x66;
+const MAC_VK_F11 = 0x67;
+const MAC_VK_JIS_Kana = 0x68;
+const MAC_VK_F13 = 0x69;
+const MAC_VK_PC_PrintScreen = 0x69;
+const MAC_VK_F16 = 0x6a;
+const MAC_VK_F14 = 0x6b;
+const MAC_VK_PC_ScrollLock = 0x6b;
+const MAC_VK_F10 = 0x6d;
+const MAC_VK_PC_ContextMenu = 0x6e;
+const MAC_VK_F12 = 0x6f;
+const MAC_VK_F15 = 0x71;
+const MAC_VK_PC_Pause = 0x71;
+const MAC_VK_Help = 0x72;
+const MAC_VK_PC_Insert = 0x72;
+const MAC_VK_Home = 0x73;
+const MAC_VK_PageUp = 0x74;
+const MAC_VK_ForwardDelete = 0x75;
+const MAC_VK_PC_Delete = 0x75;
+const MAC_VK_F4 = 0x76;
+const MAC_VK_End = 0x77;
+const MAC_VK_F2 = 0x78;
+const MAC_VK_PageDown = 0x79;
+const MAC_VK_F1 = 0x7a;
+const MAC_VK_LeftArrow = 0x7b;
+const MAC_VK_RightArrow = 0x7c;
+const MAC_VK_DownArrow = 0x7d;
+const MAC_VK_UpArrow = 0x7e;
diff --git a/testing/mochitest/tests/SimpleTest/SimpleTest.js b/testing/mochitest/tests/SimpleTest/SimpleTest.js
new file mode 100644
index 0000000000..a5d7528bbc
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js
@@ -0,0 +1,2216 @@
+/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+// Generally gTestPath should be set by the harness.
+/* global gTestPath */
+
+/**
+ * SimpleTest framework object.
+ * @class
+ */
+var SimpleTest = {};
+var parentRunner = null;
+
+// Using a try/catch rather than SpecialPowers.Cu.isRemoteProxy() because
+// it doesn't cover the case where an iframe is xorigin but fission is
+// not enabled.
+let isSameOrigin = function (w) {
+ try {
+ w.top.TestRunner;
+ } catch (e) {
+ if (e instanceof DOMException) {
+ return false;
+ }
+ }
+ return true;
+};
+let isXOrigin = !isSameOrigin(window);
+
+// Note: duplicated in browser-test.js . See also bug 1820150.
+function isErrorOrException(err) {
+ // It'd be nice if we had either `Error.isError(err)` or `Error.isInstance(err)`
+ // but we don't, so do it ourselves:
+ if (!err) {
+ return false;
+ }
+ if (err instanceof Ci.nsIException) {
+ return true;
+ }
+ try {
+ let glob = SpecialPowers.Cu.getGlobalForObject(err);
+ return err instanceof glob.Error;
+ } catch {
+ // getGlobalForObject can be upset if it doesn't get passed an object.
+ // Just do a standard instanceof check using this global and cross fingers:
+ }
+ return err instanceof Error;
+}
+
+// In normal test runs, the window that has a TestRunner in its parent is
+// the primary window. In single test runs, if there is no parent and there
+// is no opener then it is the primary window.
+var isSingleTestRun =
+ parent == window &&
+ !(opener || (window.arguments && window.arguments[0].SimpleTest));
+try {
+ var isPrimaryTestWindow =
+ (isXOrigin && parent != window && parent == top) ||
+ (!isXOrigin && (!!parent.TestRunner || isSingleTestRun));
+} catch (e) {
+ dump(
+ "TEST-UNEXPECTED-FAIL, Exception caught: " +
+ e.message +
+ ", at: " +
+ e.fileName +
+ " (" +
+ e.lineNumber +
+ "), location: " +
+ window.location.href +
+ "\n"
+ );
+}
+
+let xOriginRunner = {
+ init(harnessWindow) {
+ this.harnessWindow = harnessWindow;
+ let url = new URL(document.URL);
+ this.testFile = url.pathname;
+ this.showTestReport = url.searchParams.get("showTestReport") == "true";
+ this.expected = url.searchParams.get("expected");
+ },
+ callHarnessMethod(applyOn, command, ...params) {
+ // Message handled by xOriginTestRunnerHandler in TestRunner.js
+ this.harnessWindow.postMessage(
+ {
+ harnessType: "SimpleTest",
+ applyOn,
+ command,
+ params,
+ },
+ "*"
+ );
+ },
+ getParameterInfo() {
+ let url = new URL(document.URL);
+ return {
+ currentTestURL: url.searchParams.get("currentTestURL"),
+ testRoot: url.searchParams.get("testRoot"),
+ };
+ },
+ addFailedTest(test) {
+ this.callHarnessMethod("runner", "addFailedTest", test);
+ },
+ expectAssertions(min, max) {
+ this.callHarnessMethod("runner", "expectAssertions", min, max);
+ },
+ expectChildProcessCrash() {
+ this.callHarnessMethod("runner", "expectChildProcessCrash");
+ },
+ requestLongerTimeout(factor) {
+ this.callHarnessMethod("runner", "requestLongerTimeout", factor);
+ },
+ _lastAssertionCount: 0,
+ testFinished(tests) {
+ var newAssertionCount = SpecialPowers.assertionCount();
+ var numAsserts = newAssertionCount - this._lastAssertionCount;
+ this._lastAssertionCount = newAssertionCount;
+ this.callHarnessMethod("runner", "addAssertionCount", numAsserts);
+ this.callHarnessMethod("runner", "testFinished", tests);
+ },
+ structuredLogger: {
+ info(msg) {
+ xOriginRunner.callHarnessMethod("logger", "structuredLogger.info", msg);
+ },
+ warning(msg) {
+ xOriginRunner.callHarnessMethod(
+ "logger",
+ "structuredLogger.warning",
+ msg
+ );
+ },
+ error(msg) {
+ xOriginRunner.callHarnessMethod("logger", "structuredLogger.error", msg);
+ },
+ activateBuffering() {
+ xOriginRunner.callHarnessMethod(
+ "logger",
+ "structuredLogger.activateBuffering"
+ );
+ },
+ deactivateBuffering() {
+ xOriginRunner.callHarnessMethod(
+ "logger",
+ "structuredLogger.deactivateBuffering"
+ );
+ },
+ testStatus(url, subtest, status, expected, diagnostic, stack) {
+ xOriginRunner.callHarnessMethod(
+ "logger",
+ "structuredLogger.testStatus",
+ url,
+ subtest,
+ status,
+ expected,
+ diagnostic,
+ stack
+ );
+ },
+ },
+};
+
+// Finds the TestRunner for this test run and the SpecialPowers object (in
+// case it is not defined) from a parent/opener window.
+//
+// Finding the SpecialPowers object is needed when we have ChromePowers in
+// harness.xhtml and we need SpecialPowers in the iframe, and also for tests
+// like test_focus.xhtml where we open a window which opens another window which
+// includes SimpleTest.js.
+(function () {
+ function ancestor(w) {
+ return w.parent != w
+ ? w.parent
+ : w.opener ||
+ (!isXOrigin &&
+ w.arguments &&
+ SpecialPowers.wrap(Window).isInstance(w.arguments[0]) &&
+ w.arguments[0]);
+ }
+
+ var w = ancestor(window);
+ while (w && !parentRunner) {
+ isXOrigin = !isSameOrigin(w);
+
+ if (isXOrigin) {
+ if (w.parent != w) {
+ w = w.top;
+ }
+ xOriginRunner.init(w);
+ parentRunner = xOriginRunner;
+ }
+
+ if (!parentRunner) {
+ parentRunner = w.TestRunner;
+ if (!parentRunner && w.wrappedJSObject) {
+ parentRunner = w.wrappedJSObject.TestRunner;
+ }
+ }
+ w = ancestor(w);
+ }
+
+ if (parentRunner) {
+ SimpleTest.harnessParameters = parentRunner.getParameterInfo();
+ }
+})();
+
+/* Helper functions pulled out of various MochiKit modules */
+if (typeof repr == "undefined") {
+ this.repr = function repr(o) {
+ if (typeof o == "undefined") {
+ return "undefined";
+ } else if (o === null) {
+ return "null";
+ }
+ try {
+ if (typeof o.__repr__ == "function") {
+ return o.__repr__();
+ } else if (typeof o.repr == "function" && o.repr != repr) {
+ return o.repr();
+ }
+ } catch (e) {}
+ try {
+ if (
+ typeof o.NAME == "string" &&
+ (o.toString == Function.prototype.toString ||
+ o.toString == Object.prototype.toString)
+ ) {
+ return o.NAME;
+ }
+ } catch (e) {}
+ var ostring;
+ try {
+ if (o === 0) {
+ ostring = 1 / o > 0 ? "+0" : "-0";
+ } else if (typeof o === "string") {
+ ostring = JSON.stringify(o);
+ } else if (Array.isArray(o)) {
+ ostring = "[" + o.map(val => repr(val)).join(", ") + "]";
+ } else {
+ ostring = o + "";
+ }
+ } catch (e) {
+ return "[" + typeof o + "]";
+ }
+ if (typeof o == "function") {
+ o = ostring.replace(/^\s+/, "");
+ var idx = o.indexOf("{");
+ if (idx != -1) {
+ o = o.substr(0, idx) + "{...}";
+ }
+ }
+ return ostring;
+ };
+}
+
+/* This returns a function that applies the previously given parameters.
+ * This is used by SimpleTest.showReport
+ */
+if (typeof partial == "undefined") {
+ this.partial = function (func) {
+ var args = [];
+ for (let i = 1; i < arguments.length; i++) {
+ args.push(arguments[i]);
+ }
+ return function () {
+ if (arguments.length) {
+ for (let i = 1; i < arguments.length; i++) {
+ args.push(arguments[i]);
+ }
+ }
+ func(args);
+ };
+ };
+}
+
+if (typeof getElement == "undefined") {
+ this.getElement = function (id) {
+ return typeof id == "string" ? document.getElementById(id) : id;
+ };
+ this.$ = this.getElement;
+}
+
+SimpleTest._newCallStack = function (path) {
+ var rval = function callStackHandler() {
+ var callStack = callStackHandler.callStack;
+ for (var i = 0; i < callStack.length; i++) {
+ if (callStack[i].apply(this, arguments) === false) {
+ break;
+ }
+ }
+ try {
+ this[path] = null;
+ } catch (e) {
+ // pass
+ }
+ };
+ rval.callStack = [];
+ return rval;
+};
+
+if (typeof addLoadEvent == "undefined") {
+ this.addLoadEvent = function (func) {
+ var existing = window.onload;
+ var regfunc = existing;
+ if (
+ !(
+ typeof existing == "function" &&
+ typeof existing.callStack == "object" &&
+ existing.callStack !== null
+ )
+ ) {
+ regfunc = SimpleTest._newCallStack("onload");
+ if (typeof existing == "function") {
+ regfunc.callStack.push(existing);
+ }
+ window.onload = regfunc;
+ }
+ regfunc.callStack.push(func);
+ };
+}
+
+function createEl(type, attrs, html) {
+ //use createElementNS so the xul/xhtml tests have no issues
+ var el;
+ if (!document.body) {
+ el = document.createElementNS("http://www.w3.org/1999/xhtml", type);
+ } else {
+ el = document.createElement(type);
+ }
+ if (attrs !== null && attrs !== undefined) {
+ for (var k in attrs) {
+ el.setAttribute(k, attrs[k]);
+ }
+ }
+ if (html !== null && html !== undefined) {
+ el.appendChild(document.createTextNode(html));
+ }
+ return el;
+}
+
+/* lots of tests use this as a helper to get css properties */
+if (typeof computedStyle == "undefined") {
+ this.computedStyle = function (elem, cssProperty) {
+ elem = getElement(elem);
+ if (elem.currentStyle) {
+ return elem.currentStyle[cssProperty];
+ }
+ if (typeof document.defaultView == "undefined" || document === null) {
+ return undefined;
+ }
+ var style = document.defaultView.getComputedStyle(elem);
+ if (typeof style == "undefined" || style === null) {
+ return undefined;
+ }
+
+ var selectorCase = cssProperty.replace(/([A-Z])/g, "-$1").toLowerCase();
+
+ return style.getPropertyValue(selectorCase);
+ };
+}
+
+SimpleTest._tests = [];
+SimpleTest._stopOnLoad = true;
+SimpleTest._cleanupFunctions = [];
+SimpleTest._timeoutFunctions = [];
+SimpleTest._inChaosMode = false;
+// When using failure pattern file to filter unexpected issues,
+// SimpleTest.expected would be an array of [pattern, expected count],
+// and SimpleTest.num_failed would be an array of actual counts which
+// has the same length as SimpleTest.expected.
+SimpleTest.expected = "pass";
+SimpleTest.num_failed = 0;
+
+SpecialPowers.setAsDefaultAssertHandler();
+
+function usesFailurePatterns() {
+ return Array.isArray(SimpleTest.expected);
+}
+
+/**
+ * Checks whether there is any failure pattern matches the given error
+ * message, and if found, bumps the counter of the failure pattern.
+ *
+ * @return {boolean} Whether a matched failure pattern is found.
+ */
+function recordIfMatchesFailurePattern(name, diag) {
+ let index = SimpleTest.expected.findIndex(([pat, count]) => {
+ return (
+ pat == null ||
+ (typeof name == "string" && name.includes(pat)) ||
+ (typeof diag == "string" && diag.includes(pat))
+ );
+ });
+ if (index >= 0) {
+ SimpleTest.num_failed[index]++;
+ return true;
+ }
+ return false;
+}
+
+SimpleTest.setExpected = function () {
+ if (!parentRunner) {
+ return;
+ }
+ if (!Array.isArray(parentRunner.expected)) {
+ SimpleTest.expected = parentRunner.expected;
+ } else {
+ // Assertions are checked by the runner.
+ SimpleTest.expected = parentRunner.expected.filter(
+ ([pat]) => pat != "ASSERTION"
+ );
+ SimpleTest.num_failed = new Array(SimpleTest.expected.length);
+ SimpleTest.num_failed.fill(0);
+ }
+};
+SimpleTest.setExpected();
+
+/**
+ * Something like assert.
+ **/
+SimpleTest.ok = function (condition, name) {
+ if (arguments.length > 2) {
+ const diag = "Too many arguments passed to `ok(condition, name)`";
+ SimpleTest.record(false, name, diag);
+ } else {
+ SimpleTest.record(condition, name);
+ }
+};
+
+SimpleTest.record = function (condition, name, diag, stack, expected) {
+ var test = { result: !!condition, name, diag };
+ let successInfo;
+ let failureInfo;
+ if (SimpleTest.expected == "fail") {
+ if (!test.result) {
+ SimpleTest.num_failed++;
+ test.result = true;
+ }
+ successInfo = {
+ status: "PASS",
+ expected: "PASS",
+ message: "TEST-PASS",
+ };
+ failureInfo = {
+ status: "FAIL",
+ expected: "FAIL",
+ message: "TEST-KNOWN-FAIL",
+ };
+ } else if (!test.result && usesFailurePatterns()) {
+ if (recordIfMatchesFailurePattern(name, diag)) {
+ test.result = true;
+ // Add a mark for unexpected failures suppressed by failure pattern.
+ name = "[suppressed] " + name;
+ }
+ successInfo = {
+ status: "FAIL",
+ expected: "FAIL",
+ message: "TEST-KNOWN-FAIL",
+ };
+ failureInfo = {
+ status: "FAIL",
+ expected: "PASS",
+ message: "TEST-UNEXPECTED-FAIL",
+ };
+ } else if (expected == "fail") {
+ successInfo = {
+ status: "PASS",
+ expected: "FAIL",
+ message: "TEST-UNEXPECTED-PASS",
+ };
+ failureInfo = {
+ status: "FAIL",
+ expected: "FAIL",
+ message: "TEST-KNOWN-FAIL",
+ };
+ } else {
+ successInfo = {
+ status: "PASS",
+ expected: "PASS",
+ message: "TEST-PASS",
+ };
+ failureInfo = {
+ status: "FAIL",
+ expected: "PASS",
+ message: "TEST-UNEXPECTED-FAIL",
+ };
+ }
+
+ if (condition) {
+ stack = null;
+ } else if (!stack) {
+ stack = new Error().stack
+ .replace(/^(.*@)http:\/\/mochi.test:8888\/tests\//gm, " $1")
+ .split("\n");
+ stack.splice(0, 1);
+ stack = stack.join("\n");
+ }
+ SimpleTest._logResult(test, successInfo, failureInfo, stack);
+ SimpleTest._tests.push(test);
+};
+
+/**
+ * Roughly equivalent to ok(Object.is(a, b), name)
+ **/
+SimpleTest.is = function (a, b, name) {
+ // Be lazy and use Object.is til we want to test a browser without it.
+ var pass = Object.is(a, b);
+ var diag = pass ? "" : "got " + repr(a) + ", expected " + repr(b);
+ SimpleTest.record(pass, name, diag);
+};
+
+SimpleTest.isfuzzy = function (a, b, epsilon, name) {
+ var pass = a >= b - epsilon && a <= b + epsilon;
+ var diag = pass
+ ? ""
+ : "got " +
+ repr(a) +
+ ", expected " +
+ repr(b) +
+ " epsilon: +/- " +
+ repr(epsilon);
+ SimpleTest.record(pass, name, diag);
+};
+
+SimpleTest.isnot = function (a, b, name) {
+ var pass = !Object.is(a, b);
+ var diag = pass ? "" : "didn't expect " + repr(a) + ", but got it";
+ SimpleTest.record(pass, name, diag);
+};
+
+/**
+ * Check that the function call throws an exception.
+ */
+SimpleTest.doesThrow = function (fn, name) {
+ var gotException = false;
+ try {
+ fn();
+ } catch (ex) {
+ gotException = true;
+ }
+ ok(gotException, name);
+};
+
+// --------------- Test.Builder/Test.More todo() -----------------
+
+SimpleTest.todo = function (condition, name, diag) {
+ var test = { result: !!condition, name, diag, todo: true };
+ if (
+ test.result &&
+ usesFailurePatterns() &&
+ recordIfMatchesFailurePattern(name, diag)
+ ) {
+ // Flipping the result to false so we don't get unexpected result. There
+ // is no perfect way here. A known failure can trigger unexpected pass,
+ // in which case, tagging it as KNOWN-FAIL probably makes more sense than
+ // marking it PASS.
+ test.result = false;
+ // Add a mark for unexpected failures suppressed by failure pattern.
+ name = "[suppressed] " + name;
+ }
+ var successInfo = {
+ status: "PASS",
+ expected: "FAIL",
+ message: "TEST-UNEXPECTED-PASS",
+ };
+ var failureInfo = {
+ status: "FAIL",
+ expected: "FAIL",
+ message: "TEST-KNOWN-FAIL",
+ };
+ SimpleTest._logResult(test, successInfo, failureInfo);
+ SimpleTest._tests.push(test);
+};
+
+/*
+ * Returns the absolute URL to a test data file from where tests
+ * are served. i.e. the file doesn't necessarely exists where tests
+ * are executed.
+ *
+ * (For android, mochitest are executed on the device, while
+ * all mochitest html (and others) files are served from the test runner
+ * slave)
+ */
+SimpleTest.getTestFileURL = function (path) {
+ var location = window.location;
+ // Remove mochitest html file name from the path
+ var remotePath = location.pathname.replace(/\/[^\/]+?$/, "");
+ var url = location.origin + remotePath + "/" + path;
+ return url;
+};
+
+SimpleTest._getCurrentTestURL = function () {
+ return (
+ (SimpleTest.harnessParameters &&
+ SimpleTest.harnessParameters.currentTestURL) ||
+ (parentRunner && parentRunner.currentTestURL) ||
+ (typeof gTestPath == "string" && gTestPath) ||
+ "unknown test url"
+ );
+};
+
+SimpleTest._forceLogMessageOutput = false;
+
+/**
+ * Force all test messages to be displayed. Only applies for the current test.
+ */
+SimpleTest.requestCompleteLog = function () {
+ if (!parentRunner || SimpleTest._forceLogMessageOutput) {
+ return;
+ }
+
+ parentRunner.structuredLogger.deactivateBuffering();
+ SimpleTest._forceLogMessageOutput = true;
+
+ SimpleTest.registerCleanupFunction(function () {
+ parentRunner.structuredLogger.activateBuffering();
+ SimpleTest._forceLogMessageOutput = false;
+ });
+};
+
+SimpleTest._logResult = function (test, passInfo, failInfo, stack) {
+ var url = SimpleTest._getCurrentTestURL();
+ var result = test.result ? passInfo : failInfo;
+ var diagnostic = test.diag || null;
+ // BUGFIX : coercing test.name to a string, because some a11y tests pass an xpconnect object
+ var subtest = test.name ? String(test.name) : null;
+ var isError = !test.result == !test.todo;
+
+ if (parentRunner) {
+ if (!result.status || !result.expected) {
+ if (diagnostic) {
+ parentRunner.structuredLogger.info(diagnostic);
+ }
+ return;
+ }
+
+ if (isError) {
+ parentRunner.addFailedTest(url);
+ }
+
+ parentRunner.structuredLogger.testStatus(
+ url,
+ subtest,
+ result.status,
+ result.expected,
+ diagnostic,
+ stack
+ );
+ } else if (typeof dump === "function") {
+ var diagMessage = test.name + (test.diag ? " - " + test.diag : "");
+ var debugMsg = [result.message, url, diagMessage].join(" | ");
+ dump(debugMsg + "\n");
+ } else {
+ // Non-Mozilla browser? Just do nothing.
+ }
+};
+
+SimpleTest.info = function (name, message) {
+ var log = message ? name + " | " + message : name;
+ if (parentRunner) {
+ parentRunner.structuredLogger.info(log);
+ } else {
+ dump(log + "\n");
+ }
+};
+
+/**
+ * Copies of is and isnot with the call to ok replaced by a call to todo.
+ **/
+
+SimpleTest.todo_is = function (a, b, name) {
+ var pass = Object.is(a, b);
+ var diag = pass
+ ? repr(a) + " should equal " + repr(b)
+ : "got " + repr(a) + ", expected " + repr(b);
+ SimpleTest.todo(pass, name, diag);
+};
+
+SimpleTest.todo_isnot = function (a, b, name) {
+ var pass = !Object.is(a, b);
+ var diag = pass
+ ? repr(a) + " should not equal " + repr(b)
+ : "didn't expect " + repr(a) + ", but got it";
+ SimpleTest.todo(pass, name, diag);
+};
+
+/**
+ * Makes a test report, returns it as a DIV element.
+ **/
+SimpleTest.report = function () {
+ var passed = 0;
+ var failed = 0;
+ var todo = 0;
+
+ var tallyAndCreateDiv = function (test) {
+ var cls, msg, div;
+ var diag = test.diag ? " - " + test.diag : "";
+ if (test.todo && !test.result) {
+ todo++;
+ cls = "test_todo";
+ msg = "todo | " + test.name + diag;
+ } else if (test.result && !test.todo) {
+ passed++;
+ cls = "test_ok";
+ msg = "passed | " + test.name + diag;
+ } else {
+ failed++;
+ cls = "test_not_ok";
+ msg = "failed | " + test.name + diag;
+ }
+ div = createEl("div", { class: cls }, msg);
+ return div;
+ };
+ var results = [];
+ for (var d = 0; d < SimpleTest._tests.length; d++) {
+ results.push(tallyAndCreateDiv(SimpleTest._tests[d]));
+ }
+
+ var summary_class =
+ // eslint-disable-next-line no-nested-ternary
+ failed != 0 ? "some_fail" : passed == 0 ? "todo_only" : "all_pass";
+
+ var div1 = createEl("div", { class: "tests_report" });
+ var div2 = createEl("div", { class: "tests_summary " + summary_class });
+ var div3 = createEl("div", { class: "tests_passed" }, "Passed: " + passed);
+ var div4 = createEl("div", { class: "tests_failed" }, "Failed: " + failed);
+ var div5 = createEl("div", { class: "tests_todo" }, "Todo: " + todo);
+ div2.appendChild(div3);
+ div2.appendChild(div4);
+ div2.appendChild(div5);
+ div1.appendChild(div2);
+ for (var t = 0; t < results.length; t++) {
+ //iterate in order
+ div1.appendChild(results[t]);
+ }
+ return div1;
+};
+
+/**
+ * Toggle element visibility
+ **/
+SimpleTest.toggle = function (el) {
+ if (computedStyle(el, "display") == "block") {
+ el.style.display = "none";
+ } else {
+ el.style.display = "block";
+ }
+};
+
+/**
+ * Toggle visibility for divs with a specific class.
+ **/
+SimpleTest.toggleByClass = function (cls, evt) {
+ var children = document.getElementsByTagName("div");
+ var elements = [];
+ for (var i = 0; i < children.length; i++) {
+ var child = children[i];
+ var clsName = child.className;
+ if (!clsName) {
+ continue;
+ }
+ var classNames = clsName.split(" ");
+ for (var j = 0; j < classNames.length; j++) {
+ if (classNames[j] == cls) {
+ elements.push(child);
+ break;
+ }
+ }
+ }
+ for (var t = 0; t < elements.length; t++) {
+ //TODO: again, for-in loop over elems seems to break this
+ SimpleTest.toggle(elements[t]);
+ }
+ if (evt) {
+ evt.preventDefault();
+ }
+};
+
+/**
+ * Shows the report in the browser
+ **/
+SimpleTest.showReport = function () {
+ var togglePassed = createEl("a", { href: "#" }, "Toggle passed checks");
+ var toggleFailed = createEl("a", { href: "#" }, "Toggle failed checks");
+ var toggleTodo = createEl("a", { href: "#" }, "Toggle todo checks");
+ togglePassed.onclick = partial(SimpleTest.toggleByClass, "test_ok");
+ toggleFailed.onclick = partial(SimpleTest.toggleByClass, "test_not_ok");
+ toggleTodo.onclick = partial(SimpleTest.toggleByClass, "test_todo");
+ var body = document.body; // Handles HTML documents
+ if (!body) {
+ // Do the XML thing.
+ body = document.getElementsByTagNameNS(
+ "http://www.w3.org/1999/xhtml",
+ "body"
+ )[0];
+ }
+ var firstChild = body.childNodes[0];
+ var addNode;
+ if (firstChild) {
+ addNode = function (el) {
+ body.insertBefore(el, firstChild);
+ };
+ } else {
+ addNode = function (el) {
+ body.appendChild(el);
+ };
+ }
+ addNode(togglePassed);
+ addNode(createEl("span", null, " "));
+ addNode(toggleFailed);
+ addNode(createEl("span", null, " "));
+ addNode(toggleTodo);
+ addNode(SimpleTest.report());
+ // Add a separator from the test content.
+ addNode(createEl("hr"));
+};
+
+/**
+ * Tells SimpleTest to don't finish the test when the document is loaded,
+ * useful for asynchronous tests.
+ *
+ * When SimpleTest.waitForExplicitFinish is called,
+ * explicit SimpleTest.finish() is required.
+ **/
+SimpleTest.waitForExplicitFinish = function () {
+ SimpleTest._stopOnLoad = false;
+};
+
+/**
+ * Multiply the timeout the parent runner uses for this test by the
+ * given factor.
+ *
+ * For example, in a test that may take a long time to complete, using
+ * "SimpleTest.requestLongerTimeout(5)" will give it 5 times as long to
+ * finish.
+ *
+ * @param {Number} factor
+ * The multiplication factor to use on the timeout for this test.
+ */
+SimpleTest.requestLongerTimeout = function (factor) {
+ if (parentRunner) {
+ parentRunner.requestLongerTimeout(factor);
+ } else {
+ dump(
+ "[SimpleTest.requestLongerTimeout()] ignoring request, maybe you meant to call the global `requestLongerTimeout` instead?\n"
+ );
+ }
+};
+
+/**
+ * Note that the given range of assertions is to be expected. When
+ * this function is not called, 0 assertions are expected. When only
+ * one argument is given, that number of assertions are expected.
+ *
+ * A test where we expect to have assertions (which should largely be a
+ * transitional mechanism to get assertion counts down from our current
+ * situation) can call the SimpleTest.expectAssertions() function, with
+ * either one or two arguments: one argument gives an exact number
+ * expected, and two arguments give a range. For example, a test might do
+ * one of the following:
+ *
+ * @example
+ *
+ * // Currently triggers two assertions (bug NNNNNN).
+ * SimpleTest.expectAssertions(2);
+ *
+ * // Currently triggers one assertion on Mac (bug NNNNNN).
+ * if (navigator.platform.indexOf("Mac") == 0) {
+ * SimpleTest.expectAssertions(1);
+ * }
+ *
+ * // Currently triggers two assertions on all platforms (bug NNNNNN),
+ * // but intermittently triggers two additional assertions (bug NNNNNN)
+ * // on Windows.
+ * if (navigator.platform.indexOf("Win") == 0) {
+ * SimpleTest.expectAssertions(2, 4);
+ * } else {
+ * SimpleTest.expectAssertions(2);
+ * }
+ *
+ * // Intermittently triggers up to three assertions (bug NNNNNN).
+ * SimpleTest.expectAssertions(0, 3);
+ */
+SimpleTest.expectAssertions = function (min, max) {
+ if (parentRunner) {
+ parentRunner.expectAssertions(min, max);
+ }
+};
+
+SimpleTest._flakyTimeoutIsOK = false;
+SimpleTest._originalSetTimeout = window.setTimeout;
+window.setTimeout = function SimpleTest_setTimeoutShim() {
+ // Don't break tests that are loaded without a parent runner.
+ if (parentRunner) {
+ // Right now, we only enable these checks for mochitest-plain.
+ switch (SimpleTest.harnessParameters.testRoot) {
+ case "browser":
+ case "chrome":
+ case "a11y":
+ break;
+ default:
+ if (
+ !SimpleTest._alreadyFinished &&
+ arguments.length > 1 &&
+ arguments[1] > 0
+ ) {
+ if (SimpleTest._flakyTimeoutIsOK) {
+ SimpleTest.todo(
+ false,
+ "The author of the test has indicated that flaky timeouts are expected. Reason: " +
+ SimpleTest._flakyTimeoutReason
+ );
+ } else {
+ SimpleTest.ok(
+ false,
+ "Test attempted to use a flaky timeout value " + arguments[1]
+ );
+ }
+ }
+ }
+ }
+ return SimpleTest._originalSetTimeout.apply(window, arguments);
+};
+
+/**
+ * Request the framework to allow usage of setTimeout(func, timeout)
+ * where ``timeout > 0``. This is required to note that the author of
+ * the test is aware of the inherent flakiness in the test caused by
+ * that, and asserts that there is no way around using the magic timeout
+ * value number for some reason.
+ *
+ * Use of this function is **STRONGLY** discouraged. Think twice before
+ * using it. Such magic timeout values could result in intermittent
+ * failures in your test, and are almost never necessary!
+ *
+ * @param {String} reason
+ * A string representation of the reason why the test needs timeouts.
+ *
+ */
+SimpleTest.requestFlakyTimeout = function (reason) {
+ SimpleTest.is(typeof reason, "string", "A valid string reason is expected");
+ SimpleTest.isnot(reason, "", "Reason cannot be empty");
+ SimpleTest._flakyTimeoutIsOK = true;
+ SimpleTest._flakyTimeoutReason = reason;
+};
+
+/**
+ * If the page is not yet loaded, waits for the load event. If the page is
+ * not yet focused, focuses and waits for the window to be focused.
+ * If the current page is 'about:blank', then the page is assumed to not
+ * yet be loaded. Pass true for expectBlankPage to not make this assumption
+ * if you expect a blank page to be present.
+ *
+ * The target object should be specified if it is different than 'window'. The
+ * actual focused window may be a descendant window of aObject.
+ *
+ * @param {Window|browser|BrowsingContext} [aObject]
+ * Optional object to be focused, and may be any of:
+ * window - a window object to focus
+ * browser - a <browser>/<iframe> element. The top-level window
+ * within the frame will be focused.
+ * browsing context - a browsing context containing a window to focus
+ * If not specified, defaults to the global 'window'.
+ * @param {boolean} [expectBlankPage=false]
+ * True if targetWindow.location is 'about:blank'.
+ * @param {boolean} [aBlurSubframe=false]
+ * If true, and a subframe within the window to focus is focused, blur
+ * it so that the specified window or browsing context will receive
+ * focus events.
+ *
+ * @returns The browsing context that was focused.
+ */
+SimpleTest.promiseFocus = async function (
+ aObject,
+ aExpectBlankPage = false,
+ aBlurSubframe = false
+) {
+ let browser;
+ let browsingContext;
+ let windowToFocus;
+
+ if (!aObject) {
+ aObject = window;
+ }
+
+ async function waitForEvent(aTarget, aEventName) {
+ return new Promise(resolve => {
+ aTarget.addEventListener(aEventName, resolve, {
+ capture: true,
+ once: true,
+ });
+ });
+ }
+
+ if (SpecialPowers.wrap(Window).isInstance(aObject)) {
+ windowToFocus = aObject;
+
+ let isBlank = windowToFocus.location.href == "about:blank";
+ if (
+ aExpectBlankPage != isBlank ||
+ windowToFocus.document.readyState != "complete"
+ ) {
+ info("must wait for load");
+ await waitForEvent(windowToFocus, "load");
+ }
+ } else {
+ if (SpecialPowers.wrap(Element).isInstance(aObject)) {
+ // assume this is a browser/iframe element
+ browsingContext = aObject.browsingContext;
+ } else {
+ browsingContext = aObject;
+ }
+
+ browser =
+ browsingContext == aObject ? aObject.top.embedderElement : aObject;
+ windowToFocus = browser.ownerGlobal;
+ }
+
+ if (!windowToFocus.document.hasFocus()) {
+ info("must wait for focus");
+ let focusPromise = waitForEvent(windowToFocus.document, "focus");
+ SpecialPowers.focus(windowToFocus);
+ await focusPromise;
+ }
+
+ if (browser) {
+ if (windowToFocus.document.activeElement != browser) {
+ browser.focus();
+ }
+
+ info("must wait for focus in content");
+
+ // Make sure that the child process thinks it is focused as well.
+ await SpecialPowers.ensureFocus(browsingContext, aBlurSubframe);
+ } else {
+ if (aBlurSubframe) {
+ SpecialPowers.clearFocus(windowToFocus);
+ }
+
+ browsingContext = windowToFocus.browsingContext;
+ }
+
+ // Some tests rely on this delay, likely expecting layout or paint to occur.
+ await new Promise(resolve => {
+ SimpleTest.executeSoon(resolve);
+ });
+
+ return browsingContext;
+};
+
+/**
+ * Version of promiseFocus that uses a callback. For compatibility,
+ * the callback is passed one argument, the window that was focused.
+ * If the focused window is not in the same process, null is supplied.
+ */
+SimpleTest.waitForFocus = function (callback, aObject, expectBlankPage) {
+ SimpleTest.promiseFocus(aObject, expectBlankPage).then(focusedBC => {
+ callback(focusedBC?.window);
+ });
+};
+/* eslint-enable mozilla/use-services */
+
+SimpleTest.stripLinebreaksAndWhitespaceAfterTags = function (aString) {
+ return aString.replace(/(>\s*(\r\n|\n|\r)*\s*)/gm, ">");
+};
+
+/*
+ * `navigator.platform` should include this, when the platform is Windows.
+ */
+const kPlatformWindows = "Win";
+
+/*
+ * See `SimpleTest.waitForClipboard`.
+ */
+const kTextHtmlPrefixClipboardDataWindows =
+ "<html><body>\n<!--StartFragment-->";
+
+/*
+ * See `SimpleTest.waitForClipboard`.
+ */
+const kTextHtmlSuffixClipboardDataWindows =
+ "<!--EndFragment-->\n</body>\n</html>";
+
+/*
+ * Polls the clipboard waiting for the expected value. A known value different than
+ * the expected value is put on the clipboard first (and also polled for) so we
+ * can be sure the value we get isn't just the expected value because it was already
+ * on the clipboard. This only uses the global clipboard and only for text/plain
+ * values.
+ *
+ * @param {String|Function} aExpectedStringOrValidatorFn
+ * The string value that is expected to be on the clipboard, or a
+ * validator function getting expected clipboard data and returning a bool.
+ * If you specify string value, line breakers in clipboard are treated
+ * as LineFeed. Therefore, you cannot include CarriageReturn to the
+ * string.
+ * If you specify string value and expect "text/html" data, this wraps
+ * the expected value with `kTextHtmlPrefixClipboardDataWindows` and
+ * `kTextHtmlSuffixClipboardDataWindows` only when it runs on Windows
+ * because they are appended only by nsDataObj.cpp for Windows.
+ * https://searchfox.org/mozilla-central/rev/8f7b017a31326515cb467e69eef1f6c965b4f00e/widget/windows/nsDataObj.cpp#1798-1805,1839-1840,1842
+ * Therefore, you can specify selected (copied) HTML data simply on any
+ * platforms.
+ * @param {Function} aSetupFn
+ * A function responsible for setting the clipboard to the expected value,
+ * called after the known value setting succeeds.
+ * @param {Function} aSuccessFn
+ * A function called when the expected value is found on the clipboard.
+ * @param {Function} aFailureFn
+ * A function called if the expected value isn't found on the clipboard
+ * within 5s. It can also be called if the known value can't be found.
+ * @param {String} [aFlavor="text/plain"]
+ * The flavor to look for.
+ * @param {Number} [aTimeout=5000]
+ * The timeout (in milliseconds) to wait for a clipboard change.
+ * @param {boolean} [aExpectFailure=false]
+ * If true, fail if the clipboard contents are modified within the timeout
+ * interval defined by aTimeout. When aExpectFailure is true, the argument
+ * aExpectedStringOrValidatorFn must be null, as it won't be used.
+ * @param {boolean} [aDontInitializeClipboardIfExpectFailure=false]
+ * If aExpectFailure and this is set to true, this does NOT initialize
+ * clipboard with random data before running aSetupFn.
+ */
+SimpleTest.waitForClipboard = function (
+ aExpectedStringOrValidatorFn,
+ aSetupFn,
+ aSuccessFn,
+ aFailureFn,
+ aFlavor,
+ aTimeout,
+ aExpectFailure,
+ aDontInitializeClipboardIfExpectFailure
+) {
+ let promise = SimpleTest.promiseClipboardChange(
+ aExpectedStringOrValidatorFn,
+ aSetupFn,
+ aFlavor,
+ aTimeout,
+ aExpectFailure,
+ aDontInitializeClipboardIfExpectFailure
+ );
+ promise.then(aSuccessFn).catch(aFailureFn);
+};
+
+/**
+ * Promise-oriented version of waitForClipboard.
+ */
+SimpleTest.promiseClipboardChange = async function (
+ aExpectedStringOrValidatorFn,
+ aSetupFn,
+ aFlavor,
+ aTimeout,
+ aExpectFailure,
+ aDontInitializeClipboardIfExpectFailure
+) {
+ let requestedFlavor = aFlavor || "text/plain";
+
+ // The known value we put on the clipboard before running aSetupFn
+ let initialVal = "waitForClipboard-known-value-" + Math.random();
+ let preExpectedVal = initialVal;
+
+ let inputValidatorFn;
+ if (aExpectFailure) {
+ // If we expect failure, the aExpectedStringOrValidatorFn should be null
+ if (aExpectedStringOrValidatorFn !== null) {
+ SimpleTest.ok(
+ false,
+ "When expecting failure, aExpectedStringOrValidatorFn must be null"
+ );
+ }
+
+ inputValidatorFn = function (aData) {
+ return aData != initialVal;
+ };
+ // Build a default validator function for common string input.
+ } else if (typeof aExpectedStringOrValidatorFn == "string") {
+ if (aExpectedStringOrValidatorFn.includes("\r")) {
+ throw new Error(
+ "Use function instead of string to compare raw line breakers in clipboard"
+ );
+ }
+ if (requestedFlavor === "text/html" && navigator.platform.includes("Win")) {
+ inputValidatorFn = function (aData) {
+ return (
+ aData.replace(/\r\n?/g, "\n") ===
+ kTextHtmlPrefixClipboardDataWindows +
+ aExpectedStringOrValidatorFn +
+ kTextHtmlSuffixClipboardDataWindows
+ );
+ };
+ } else {
+ inputValidatorFn = function (aData) {
+ return aData.replace(/\r\n?/g, "\n") === aExpectedStringOrValidatorFn;
+ };
+ }
+ } else {
+ inputValidatorFn = aExpectedStringOrValidatorFn;
+ }
+
+ let maxPolls = aTimeout ? aTimeout / 100 : 50;
+
+ async function putAndVerify(operationFn, validatorFn, flavor, expectFailure) {
+ await operationFn();
+
+ let data;
+ for (let i = 0; i < maxPolls; i++) {
+ data = SpecialPowers.getClipboardData(flavor);
+ if (validatorFn(data)) {
+ // Don't show the success message when waiting for preExpectedVal
+ if (preExpectedVal) {
+ preExpectedVal = null;
+ } else {
+ SimpleTest.ok(
+ !expectFailure,
+ "Clipboard has the given value: '" + data + "'"
+ );
+ }
+
+ return data;
+ }
+
+ // Wait 100ms and check again.
+ await new Promise(resolve => {
+ SimpleTest._originalSetTimeout.apply(window, [resolve, 100]);
+ });
+ }
+
+ let errorMsg = `Timed out while polling clipboard for ${
+ preExpectedVal ? "initialized" : "requested"
+ } data, got: ${data}`;
+ SimpleTest.ok(expectFailure, errorMsg);
+ if (!expectFailure) {
+ throw new Error(errorMsg);
+ }
+ return data;
+ }
+
+ if (!aExpectFailure || !aDontInitializeClipboardIfExpectFailure) {
+ // First we wait for a known value different from the expected one.
+ SimpleTest.info(`Initializing clipboard with "${preExpectedVal}"...`);
+ await putAndVerify(
+ function () {
+ SpecialPowers.clipboardCopyString(preExpectedVal);
+ },
+ function (aData) {
+ return aData == preExpectedVal;
+ },
+ "text/plain",
+ false
+ );
+
+ SimpleTest.info(
+ "Succeeded initializing clipboard, start requested things..."
+ );
+ } else {
+ preExpectedVal = null;
+ }
+
+ return putAndVerify(
+ aSetupFn,
+ inputValidatorFn,
+ requestedFlavor,
+ aExpectFailure
+ );
+};
+
+/**
+ * Wait for a condition for a while (actually up to 3s here).
+ *
+ * @param {Function} aCond
+ * A function returns the result of the condition
+ * @param {Function} aCallback
+ * A function called after the condition is passed or timeout.
+ * @param {String} aErrorMsg
+ * The message displayed when the condition failed to pass
+ * before timeout.
+ */
+SimpleTest.waitForCondition = function (aCond, aCallback, aErrorMsg) {
+ this.promiseWaitForCondition(aCond, aErrorMsg).then(() => aCallback());
+};
+SimpleTest.promiseWaitForCondition = async function (aCond, aErrorMsg) {
+ for (let tries = 0; tries < 30; ++tries) {
+ // Wait 100ms between checks.
+ await new Promise(resolve => {
+ SimpleTest._originalSetTimeout.apply(window, [resolve, 100]);
+ });
+
+ let conditionPassed;
+ try {
+ conditionPassed = await aCond();
+ } catch (e) {
+ ok(false, `${e}\n${e.stack}`);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ return;
+ }
+ }
+ ok(false, aErrorMsg);
+};
+
+/**
+ * Executes a function shortly after the call, but lets the caller continue
+ * working (or finish).
+ *
+ * @param {Function} aFunc
+ * Function to execute soon.
+ */
+SimpleTest.executeSoon = function (aFunc) {
+ if ("SpecialPowers" in window) {
+ return SpecialPowers.executeSoon(aFunc, window);
+ }
+ setTimeout(aFunc, 0);
+ return null; // Avoid warning.
+};
+
+/**
+ * Register a cleanup/teardown function (which may be async) to run after all
+ * tasks have finished, before running the next test. If async (or the function
+ * returns a promise), the framework will wait for the promise/async function
+ * to resolve.
+ *
+ * @param {Function} aFunc
+ * The cleanup/teardown function to run.
+ */
+SimpleTest.registerCleanupFunction = function (aFunc) {
+ SimpleTest._cleanupFunctions.push(aFunc);
+};
+
+SimpleTest.registerTimeoutFunction = function (aFunc) {
+ SimpleTest._timeoutFunctions.push(aFunc);
+};
+
+SimpleTest.testInChaosMode = function () {
+ if (SimpleTest._inChaosMode) {
+ // It's already enabled for this test, don't enter twice
+ return;
+ }
+ SpecialPowers.DOMWindowUtils.enterChaosMode();
+ SimpleTest._inChaosMode = true;
+ // increase timeout here as chaosmode is very slow (i.e. 10x)
+ // doing 20x as this overwrites anything the tests set
+ SimpleTest.requestLongerTimeout(20);
+};
+
+SimpleTest.timeout = async function () {
+ for (const func of SimpleTest._timeoutFunctions) {
+ await func();
+ }
+ SimpleTest._timeoutFunctions = [];
+};
+
+SimpleTest.finishWithFailure = function (msg) {
+ SimpleTest.ok(false, msg);
+ SimpleTest.finish();
+};
+
+/**
+ * Finishes the tests. This is automatically called, except when
+ * SimpleTest.waitForExplicitFinish() has been invoked.
+ **/
+SimpleTest.finish = function () {
+ if (SimpleTest._alreadyFinished) {
+ var err =
+ "TEST-UNEXPECTED-FAIL | SimpleTest | this test already called finish!";
+ if (parentRunner) {
+ parentRunner.structuredLogger.error(err);
+ } else {
+ dump(err + "\n");
+ }
+ }
+
+ if (SimpleTest.expected == "fail" && SimpleTest.num_failed <= 0) {
+ let msg = "We expected at least one failure";
+ let test = {
+ result: false,
+ name: "fail-if condition in manifest",
+ diag: msg,
+ };
+ let successInfo = {
+ status: "FAIL",
+ expected: "FAIL",
+ message: "TEST-KNOWN-FAIL",
+ };
+ let failureInfo = {
+ status: "PASS",
+ expected: "FAIL",
+ message: "TEST-UNEXPECTED-PASS",
+ };
+ SimpleTest._logResult(test, successInfo, failureInfo);
+ SimpleTest._tests.push(test);
+ } else if (usesFailurePatterns()) {
+ SimpleTest.expected.forEach(([pat, expected_count], i) => {
+ let count = SimpleTest.num_failed[i];
+ let diag;
+ if (expected_count === null && count == 0) {
+ diag = "expected some failures but got none";
+ } else if (expected_count !== null && expected_count != count) {
+ diag = `expected ${expected_count} failures but got ${count}`;
+ } else {
+ return;
+ }
+ let name = pat
+ ? `failure pattern \`${pat}\` in this test`
+ : "failures in this test";
+ let test = { result: false, name, diag };
+ let successInfo = {
+ status: "PASS",
+ expected: "PASS",
+ message: "TEST-PASS",
+ };
+ let failureInfo = {
+ status: "FAIL",
+ expected: "PASS",
+ message: "TEST-UNEXPECTED-FAIL",
+ };
+ SimpleTest._logResult(test, successInfo, failureInfo);
+ SimpleTest._tests.push(test);
+ });
+ }
+
+ SimpleTest._timeoutFunctions = [];
+
+ SimpleTest.testsLength = SimpleTest._tests.length;
+
+ SimpleTest._alreadyFinished = true;
+
+ if (SimpleTest._inChaosMode) {
+ SpecialPowers.DOMWindowUtils.leaveChaosMode();
+ SimpleTest._inChaosMode = false;
+ }
+
+ var afterCleanup = async function () {
+ SpecialPowers.removeFiles();
+
+ if (SpecialPowers.DOMWindowUtils.isTestControllingRefreshes) {
+ SimpleTest.ok(false, "test left refresh driver under test control");
+ SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+ }
+ if (SimpleTest._expectingUncaughtException) {
+ SimpleTest.ok(
+ false,
+ "expectUncaughtException was called but no uncaught exception was detected!"
+ );
+ }
+ if (!SimpleTest._tests.length) {
+ SimpleTest.ok(
+ false,
+ "[SimpleTest.finish()] No checks actually run. " +
+ "(You need to call ok(), is(), or similar " +
+ "functions at least once. Make sure you use " +
+ "SimpleTest.waitForExplicitFinish() if you need " +
+ "it.)"
+ );
+ }
+
+ let workers = await SpecialPowers.registeredServiceWorkers();
+ let promise = null;
+ if (SimpleTest._expectingRegisteredServiceWorker) {
+ if (workers.length === 0) {
+ SimpleTest.ok(
+ false,
+ "This test is expected to leave a service worker registered"
+ );
+ }
+ } else if (workers.length) {
+ let FULL_PROFILE_WORKERS_TO_IGNORE = [];
+ if (parentRunner.conditionedProfile) {
+ // Full profile has service workers in the profile, without clearing the profile
+ // service workers will be leftover, in all my testing youtube is the only one.
+ FULL_PROFILE_WORKERS_TO_IGNORE = ["https://www.youtube.com/sw.js"];
+ } else {
+ SimpleTest.ok(
+ false,
+ "This test left a service worker registered without cleaning it up"
+ );
+ }
+
+ for (let worker of workers) {
+ if (FULL_PROFILE_WORKERS_TO_IGNORE.includes(worker.scriptSpec)) {
+ continue;
+ }
+ SimpleTest.ok(
+ false,
+ `Left over worker: ${worker.scriptSpec} (scope: ${worker.scope})`
+ );
+ }
+ promise = SpecialPowers.removeAllServiceWorkerData();
+ }
+
+ // If we want to wait for removeAllServiceWorkerData to finish, above,
+ // there's a small chance that spinning the event loop could cause
+ // SpecialPowers and SimpleTest to go away (e.g. if the test did
+ // document.open). promise being non-null should be rare (a test would
+ // have had to already fail by leaving a service worker around), so
+ // limit the chances of the async wait happening to that case.
+ function finish() {
+ if (parentRunner) {
+ /* We're running in an iframe, and the parent has a TestRunner */
+ parentRunner.testFinished(SimpleTest._tests);
+ }
+
+ if (!parentRunner || parentRunner.showTestReport) {
+ SpecialPowers.flushPermissions(function () {
+ SpecialPowers.flushPrefEnv(function () {
+ SimpleTest.showReport();
+ });
+ });
+ }
+ }
+
+ if (promise) {
+ promise.then(finish);
+ } else {
+ finish();
+ }
+ };
+
+ var executeCleanupFunction = function () {
+ var func = SimpleTest._cleanupFunctions.pop();
+
+ if (!func) {
+ afterCleanup();
+ return;
+ }
+
+ var ret;
+ try {
+ ret = func();
+ } catch (ex) {
+ SimpleTest.ok(false, "Cleanup function threw exception: " + ex);
+ }
+
+ if (ret && ret.constructor.name == "Promise") {
+ ret.then(executeCleanupFunction, ex =>
+ SimpleTest.ok(false, "Cleanup promise rejected: " + ex)
+ );
+ } else {
+ executeCleanupFunction();
+ }
+ };
+
+ executeCleanupFunction();
+
+ SpecialPowers.notifyObservers(null, "test-complete");
+};
+
+/**
+ * Monitor console output from now until endMonitorConsole is called.
+ *
+ * Expect to receive all console messages described by the elements of
+ * ``msgs``, an array, in the order listed in ``msgs``; each element is an
+ * object which may have any number of the following properties:
+ *
+ * message, errorMessage, sourceName, sourceLine, category: string or regexp
+ * lineNumber, columnNumber: number
+ * isScriptError, isWarning: boolean
+ *
+ * Strings, numbers, and booleans must compare equal to the named
+ * property of the Nth console message. Regexps must match. Any
+ * fields present in the message but not in the pattern object are ignored.
+ *
+ * In addition to the above properties, elements in ``msgs`` may have a ``forbid``
+ * boolean property. When ``forbid`` is true, a failure is logged each time a
+ * matching message is received.
+ *
+ * If ``forbidUnexpectedMsgs`` is true, then the messages received in the console
+ * must exactly match the non-forbidden messages in ``msgs``; for each received
+ * message not described by the next element in ``msgs``, a failure is logged. If
+ * false, then other non-forbidden messages are ignored, but all expected
+ * messages must still be received.
+ *
+ * After endMonitorConsole is called, ``continuation`` will be called
+ * asynchronously. (Normally, you will want to pass ``SimpleTest.finish`` here.)
+ *
+ * It is incorrect to use this function in a test which has not called
+ * SimpleTest.waitForExplicitFinish.
+ */
+SimpleTest.monitorConsole = function (
+ continuation,
+ msgs,
+ forbidUnexpectedMsgs
+) {
+ if (SimpleTest._stopOnLoad) {
+ ok(false, "Console monitoring requires use of waitForExplicitFinish.");
+ }
+
+ function msgMatches(msg, pat) {
+ for (var k in pat) {
+ if (!(k in msg)) {
+ return false;
+ }
+ if (pat[k] instanceof RegExp && typeof msg[k] === "string") {
+ if (!pat[k].test(msg[k])) {
+ return false;
+ }
+ } else if (msg[k] !== pat[k]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ var forbiddenMsgs = [];
+ var i = 0;
+ while (i < msgs.length) {
+ let pat = msgs[i];
+ if ("forbid" in pat) {
+ var forbid = pat.forbid;
+ delete pat.forbid;
+ if (forbid) {
+ forbiddenMsgs.push(pat);
+ msgs.splice(i, 1);
+ continue;
+ }
+ }
+ i++;
+ }
+
+ var counter = 0;
+ var assertionLabel = JSON.stringify(msgs);
+ function listener(msg) {
+ if (msg.message === "SENTINEL" && !msg.isScriptError) {
+ is(
+ counter,
+ msgs.length,
+ "monitorConsole | number of messages " + assertionLabel
+ );
+ SimpleTest.executeSoon(continuation);
+ return;
+ }
+ for (let pat of forbiddenMsgs) {
+ if (msgMatches(msg, pat)) {
+ ok(
+ false,
+ "monitorConsole | observed forbidden message " + JSON.stringify(msg)
+ );
+ return;
+ }
+ }
+ if (counter >= msgs.length) {
+ var str = "monitorConsole | extra message | " + JSON.stringify(msg);
+ if (forbidUnexpectedMsgs) {
+ ok(false, str);
+ } else {
+ info(str);
+ }
+ return;
+ }
+ var matches = msgMatches(msg, msgs[counter]);
+ if (forbidUnexpectedMsgs) {
+ ok(
+ matches,
+ "monitorConsole | [" + counter + "] must match " + JSON.stringify(msg)
+ );
+ } else {
+ info(
+ "monitorConsole | [" +
+ counter +
+ "] " +
+ (matches ? "matched " : "did not match ") +
+ JSON.stringify(msg)
+ );
+ }
+ if (matches) {
+ counter++;
+ }
+ }
+ SpecialPowers.registerConsoleListener(listener);
+};
+
+/**
+ * Stop monitoring console output.
+ */
+SimpleTest.endMonitorConsole = function () {
+ SpecialPowers.postConsoleSentinel();
+};
+
+/**
+ * Run ``testfn`` synchronously, and monitor its console output.
+ *
+ * ``msgs`` is handled as described above for monitorConsole.
+ *
+ * After ``testfn`` returns, console monitoring will stop, and ``continuation``
+ * will be called asynchronously.
+ *
+ */
+SimpleTest.expectConsoleMessages = function (testfn, msgs, continuation) {
+ SimpleTest.monitorConsole(continuation, msgs);
+ testfn();
+ SimpleTest.executeSoon(SimpleTest.endMonitorConsole);
+};
+
+/**
+ * Wrapper around ``expectConsoleMessages`` for the case where the test has
+ * only one ``testfn`` to run.
+ */
+SimpleTest.runTestExpectingConsoleMessages = function (testfn, msgs) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectConsoleMessages(testfn, msgs, SimpleTest.finish);
+};
+
+/**
+ * Indicates to the test framework that the current test expects one or
+ * more crashes (from plugins or IPC documents), and that the minidumps from
+ * those crashes should be removed.
+ */
+SimpleTest.expectChildProcessCrash = function () {
+ if (parentRunner) {
+ parentRunner.expectChildProcessCrash();
+ }
+};
+
+/**
+ * Indicates to the test framework that the next uncaught exception during
+ * the test is expected, and should not cause a test failure.
+ */
+SimpleTest.expectUncaughtException = function (aExpecting) {
+ SimpleTest._expectingUncaughtException =
+ aExpecting === void 0 || !!aExpecting;
+};
+
+/**
+ * Returns whether the test has indicated that it expects an uncaught exception
+ * to occur.
+ */
+SimpleTest.isExpectingUncaughtException = function () {
+ return SimpleTest._expectingUncaughtException;
+};
+
+/**
+ * Indicates to the test framework that all of the uncaught exceptions
+ * during the test are known problems that should be fixed in the future,
+ * but which should not cause the test to fail currently.
+ */
+SimpleTest.ignoreAllUncaughtExceptions = function (aIgnoring) {
+ SimpleTest._ignoringAllUncaughtExceptions =
+ aIgnoring === void 0 || !!aIgnoring;
+};
+
+/**
+ * Returns whether the test has indicated that all uncaught exceptions should be
+ * ignored.
+ */
+SimpleTest.isIgnoringAllUncaughtExceptions = function () {
+ return SimpleTest._ignoringAllUncaughtExceptions;
+};
+
+/**
+ * Indicates to the test framework that this test is expected to leave a
+ * service worker registered when it finishes.
+ */
+SimpleTest.expectRegisteredServiceWorker = function () {
+ SimpleTest._expectingRegisteredServiceWorker = true;
+};
+
+/**
+ * Resets any state this SimpleTest object has. This is important for
+ * browser chrome mochitests, which reuse the same SimpleTest object
+ * across a run.
+ */
+SimpleTest.reset = function () {
+ SimpleTest._ignoringAllUncaughtExceptions = false;
+ SimpleTest._expectingUncaughtException = false;
+ SimpleTest._expectingRegisteredServiceWorker = false;
+ SimpleTest._bufferedMessages = [];
+};
+
+if (isPrimaryTestWindow) {
+ addLoadEvent(function () {
+ if (SimpleTest._stopOnLoad) {
+ SimpleTest.finish();
+ }
+ });
+}
+
+// --------------- Test.Builder/Test.More isDeeply() -----------------
+
+SimpleTest.DNE = { dne: "Does not exist" };
+SimpleTest.LF = "\r\n";
+
+SimpleTest._deepCheck = function (e1, e2, stack, seen) {
+ var ok = false;
+ if (Object.is(e1, e2)) {
+ // Handles identical primitives and references.
+ ok = true;
+ } else if (
+ typeof e1 != "object" ||
+ typeof e2 != "object" ||
+ e1 === null ||
+ e2 === null
+ ) {
+ // If either argument is a primitive or function, don't consider the arguments the same.
+ ok = false;
+ } else if (e1 == SimpleTest.DNE || e2 == SimpleTest.DNE) {
+ ok = false;
+ } else if (SimpleTest.isa(e1, "Array") && SimpleTest.isa(e2, "Array")) {
+ ok = SimpleTest._eqArray(e1, e2, stack, seen);
+ } else {
+ ok = SimpleTest._eqAssoc(e1, e2, stack, seen);
+ }
+ return ok;
+};
+
+SimpleTest._eqArray = function (a1, a2, stack, seen) {
+ // Return if they're the same object.
+ if (a1 == a2) {
+ return true;
+ }
+
+ // JavaScript objects have no unique identifiers, so we have to store
+ // references to them all in an array, and then compare the references
+ // directly. It's slow, but probably won't be much of an issue in
+ // practice. Start by making a local copy of the array to as to avoid
+ // confusing a reference seen more than once (such as [a, a]) for a
+ // circular reference.
+ for (var j = 0; j < seen.length; j++) {
+ if (seen[j][0] == a1) {
+ return seen[j][1] == a2;
+ }
+ }
+
+ // If we get here, we haven't seen a1 before, so store it with reference
+ // to a2.
+ seen.push([a1, a2]);
+
+ var ok = true;
+ // Only examines enumerable attributes. Only works for numeric arrays!
+ // Associative arrays return 0. So call _eqAssoc() for them, instead.
+ var max = Math.max(a1.length, a2.length);
+ if (max == 0) {
+ return SimpleTest._eqAssoc(a1, a2, stack, seen);
+ }
+ for (var i = 0; i < max; i++) {
+ var e1 = i < a1.length ? a1[i] : SimpleTest.DNE;
+ var e2 = i < a2.length ? a2[i] : SimpleTest.DNE;
+ stack.push({ type: "Array", idx: i, vals: [e1, e2] });
+ ok = SimpleTest._deepCheck(e1, e2, stack, seen);
+ if (ok) {
+ stack.pop();
+ } else {
+ break;
+ }
+ }
+ return ok;
+};
+
+SimpleTest._eqAssoc = function (o1, o2, stack, seen) {
+ // Return if they're the same object.
+ if (o1 == o2) {
+ return true;
+ }
+
+ // JavaScript objects have no unique identifiers, so we have to store
+ // references to them all in an array, and then compare the references
+ // directly. It's slow, but probably won't be much of an issue in
+ // practice. Start by making a local copy of the array to as to avoid
+ // confusing a reference seen more than once (such as [a, a]) for a
+ // circular reference.
+ seen = seen.slice(0);
+ for (let j = 0; j < seen.length; j++) {
+ if (seen[j][0] == o1) {
+ return seen[j][1] == o2;
+ }
+ }
+
+ // If we get here, we haven't seen o1 before, so store it with reference
+ // to o2.
+ seen.push([o1, o2]);
+
+ // They should be of the same class.
+
+ var ok = true;
+ // Only examines enumerable attributes.
+ var o1Size = 0;
+ // eslint-disable-next-line no-unused-vars
+ for (let i in o1) {
+ o1Size++;
+ }
+ var o2Size = 0;
+ // eslint-disable-next-line no-unused-vars
+ for (let i in o2) {
+ o2Size++;
+ }
+ var bigger = o1Size > o2Size ? o1 : o2;
+ for (let i in bigger) {
+ var e1 = i in o1 ? o1[i] : SimpleTest.DNE;
+ var e2 = i in o2 ? o2[i] : SimpleTest.DNE;
+ stack.push({ type: "Object", idx: i, vals: [e1, e2] });
+ ok = SimpleTest._deepCheck(e1, e2, stack, seen);
+ if (ok) {
+ stack.pop();
+ } else {
+ break;
+ }
+ }
+ return ok;
+};
+
+SimpleTest._formatStack = function (stack) {
+ var variable = "$Foo";
+ for (let i = 0; i < stack.length; i++) {
+ var entry = stack[i];
+ var type = entry.type;
+ var idx = entry.idx;
+ if (idx != null) {
+ if (type == "Array") {
+ // Numeric array index.
+ variable += "[" + idx + "]";
+ } else {
+ // Associative array index.
+ idx = idx.replace("'", "\\'");
+ variable += "['" + idx + "']";
+ }
+ }
+ }
+
+ var vals = stack[stack.length - 1].vals.slice(0, 2);
+ var vars = [
+ variable.replace("$Foo", "got"),
+ variable.replace("$Foo", "expected"),
+ ];
+
+ var out = "Structures begin differing at:" + SimpleTest.LF;
+ for (let i = 0; i < vals.length; i++) {
+ var val = vals[i];
+ if (val === SimpleTest.DNE) {
+ val = "Does not exist";
+ } else {
+ val = repr(val);
+ }
+ out += vars[i] + " = " + val + SimpleTest.LF;
+ }
+
+ return " " + out;
+};
+
+SimpleTest.isDeeply = function (it, as, name) {
+ var stack = [{ vals: [it, as] }];
+ var seen = [];
+ if (SimpleTest._deepCheck(it, as, stack, seen)) {
+ SimpleTest.record(true, name);
+ } else {
+ SimpleTest.record(false, name, SimpleTest._formatStack(stack));
+ }
+};
+
+SimpleTest.typeOf = function (object) {
+ var c = Object.prototype.toString.apply(object);
+ var name = c.substring(8, c.length - 1);
+ if (name != "Object") {
+ return name;
+ }
+ // It may be a non-core class. Try to extract the class name from
+ // the constructor function. This may not work in all implementations.
+ if (/function ([^(\s]+)/.test(Function.toString.call(object.constructor))) {
+ return RegExp.$1;
+ }
+ // No idea. :-(
+ return name;
+};
+
+SimpleTest.isa = function (object, clas) {
+ return SimpleTest.typeOf(object) == clas;
+};
+
+// Global symbols:
+var ok = SimpleTest.ok;
+var record = SimpleTest.record;
+var is = SimpleTest.is;
+var isfuzzy = SimpleTest.isfuzzy;
+var isnot = SimpleTest.isnot;
+var todo = SimpleTest.todo;
+var todo_is = SimpleTest.todo_is;
+var todo_isnot = SimpleTest.todo_isnot;
+var isDeeply = SimpleTest.isDeeply;
+var info = SimpleTest.info;
+
+var gOldOnError = window.onerror;
+window.onerror = function simpletestOnerror(
+ errorMsg,
+ url,
+ lineNumber,
+ columnNumber,
+ originalException
+) {
+ // Log the message.
+ // XXX Chrome mochitests sometimes trigger this window.onerror handler,
+ // but there are a number of uncaught JS exceptions from those tests.
+ // For now, for tests that self identify as having unintentional uncaught
+ // exceptions, just dump it so that the error is visible but doesn't cause
+ // a test failure. See bug 652494.
+ var isExpected = !!SimpleTest._expectingUncaughtException;
+ var message = (isExpected ? "expected " : "") + "uncaught exception";
+ var error = errorMsg + " at ";
+ try {
+ error += originalException.stack;
+ } catch (e) {
+ // At least use the url+line+column we were given
+ error += url + ":" + lineNumber + ":" + columnNumber;
+ }
+ if (!SimpleTest._ignoringAllUncaughtExceptions) {
+ // Don't log if SimpleTest.finish() is already called, it would cause failures
+ if (!SimpleTest._alreadyFinished) {
+ SimpleTest.record(isExpected, message, error);
+ }
+ SimpleTest._expectingUncaughtException = false;
+ } else {
+ SimpleTest.todo(false, message + ": " + error);
+ }
+ // There is no Components.stack.caller to log. (See bug 511888.)
+
+ // Call previous handler.
+ if (gOldOnError) {
+ try {
+ // Ignore return value: always run default handler.
+ gOldOnError(errorMsg, url, lineNumber);
+ } catch (e) {
+ // Log the error.
+ SimpleTest.info("Exception thrown by gOldOnError(): " + e);
+ // Log its stack.
+ if (e.stack) {
+ SimpleTest.info("JavaScript error stack:\n" + e.stack);
+ }
+ }
+ }
+
+ if (!SimpleTest._stopOnLoad && !isExpected && !SimpleTest._alreadyFinished) {
+ // Need to finish() manually here, yet let the test actually end first.
+ SimpleTest.executeSoon(SimpleTest.finish);
+ }
+};
+
+// Lifted from dom/media/test/manifest.js
+// Make sure to not touch navigator in here, since we want to push prefs that
+// will affect the APIs it exposes, but the set of exposed APIs is determined
+// when Navigator.prototype is created. So if we touch navigator before pushing
+// the prefs, the APIs it exposes will not take those prefs into account. We
+// work around this by using a navigator object from a different global for our
+// UA string testing.
+var gAndroidSdk = null;
+function getAndroidSdk() {
+ if (gAndroidSdk === null) {
+ var iframe = document.documentElement.appendChild(
+ document.createElement("iframe")
+ );
+ iframe.style.display = "none";
+ var nav = iframe.contentWindow.navigator;
+ if (
+ !nav.userAgent.includes("Mobile") &&
+ !nav.userAgent.includes("Tablet")
+ ) {
+ gAndroidSdk = -1;
+ } else {
+ // See nsSystemInfo.cpp, the getProperty('version') returns different value
+ // on each platforms, so we need to distinguish the android platform.
+ var versionString = nav.userAgent.includes("Android")
+ ? "version"
+ : "sdk_version";
+ gAndroidSdk = SpecialPowers.Services.sysinfo.getProperty(versionString);
+ }
+ document.documentElement.removeChild(iframe);
+ }
+ return gAndroidSdk;
+}
+
+// add_task(generatorFunction):
+// Call `add_task(generatorFunction)` for each separate
+// asynchronous task in a mochitest. Tasks are run consecutively.
+// Before the first task, `SimpleTest.waitForExplicitFinish()`
+// will be called automatically, and after the last task,
+// `SimpleTest.finish()` will be called.
+var add_task = (function () {
+ // The list of tasks to run.
+ var task_list = [];
+ var run_only_this_task = null;
+
+ function isGenerator(value) {
+ return (
+ value && typeof value === "object" && typeof value.next === "function"
+ );
+ }
+
+ // The "add_task" function
+ return function (generatorFunction, options = { isSetup: false }) {
+ if (task_list.length === 0) {
+ // This is the first time add_task has been called.
+ // First, confirm that SimpleTest is available.
+ if (!SimpleTest) {
+ throw new Error("SimpleTest not available.");
+ }
+ // Don't stop tests until asynchronous tasks are finished.
+ SimpleTest.waitForExplicitFinish();
+ // Because the client is using add_task for this set of tests,
+ // we need to spawn a "master task" that calls each task in succesion.
+ // Use setTimeout to ensure the master task runs after the client
+ // script finishes.
+ setTimeout(function nextTick() {
+ // If we are in a HTML document, we should wait for the document
+ // to be fully loaded.
+ // These checks ensure that we are in an HTML document without
+ // throwing TypeError; also I am told that readyState in XUL documents
+ // are totally bogus so we don't try to do this there.
+ if (
+ typeof window !== "undefined" &&
+ typeof HTMLDocument !== "undefined" &&
+ window.document instanceof HTMLDocument &&
+ window.document.readyState !== "complete"
+ ) {
+ setTimeout(nextTick);
+ return;
+ }
+
+ (async () => {
+ // Allow for a task to be skipped; we need only use the structured logger
+ // for this, whilst deactivating log buffering to ensure that messages
+ // are always printed to stdout.
+ function skipTask(name) {
+ let logger = parentRunner && parentRunner.structuredLogger;
+ if (!logger) {
+ info("add_task | Skipping test " + name);
+ return;
+ }
+ logger.deactivateBuffering();
+ logger.testStatus(SimpleTest._getCurrentTestURL(), name, "SKIP");
+ logger.warning("add_task | Skipping test " + name);
+ logger.activateBuffering();
+ }
+
+ // We stop the entire test file at the first exception because this
+ // may mean that the state of subsequent tests may be corrupt.
+ try {
+ for (var task of task_list) {
+ var name = task.name || "";
+ if (
+ task.__skipMe ||
+ (run_only_this_task && task != run_only_this_task)
+ ) {
+ skipTask(name);
+ continue;
+ }
+ const taskInfo = action =>
+ info(
+ `${
+ task.isSetup ? "add_setup" : "add_task"
+ } | ${action} ${name}`
+ );
+ taskInfo("Entering");
+ let result = await task();
+ if (isGenerator(result)) {
+ ok(false, "Task returned a generator");
+ }
+ taskInfo("Leaving");
+ }
+ } catch (ex) {
+ try {
+ let serializedEx;
+ if (
+ typeof ex == "string" ||
+ (typeof ex == "object" && isErrorOrException(ex))
+ ) {
+ serializedEx = `${ex}`;
+ } else {
+ serializedEx = JSON.stringify(ex);
+ }
+
+ SimpleTest.record(
+ false,
+ serializedEx,
+ "Should not throw any errors",
+ ex.stack
+ );
+ } catch (ex2) {
+ SimpleTest.record(
+ false,
+ "(The exception cannot be converted to string.)",
+ "Should not throw any errors",
+ ex.stack
+ );
+ }
+ }
+ // All tasks are finished.
+ SimpleTest.finish();
+ })();
+ });
+ }
+ generatorFunction.skip = () => (generatorFunction.__skipMe = true);
+ generatorFunction.only = () => (run_only_this_task = generatorFunction);
+ // Add the task to the list of tasks to run after
+ // the main thread is finished.
+ if (options.isSetup) {
+ generatorFunction.isSetup = true;
+ let lastSetupIndex = task_list.findLastIndex(t => t.isSetup) + 1;
+ task_list.splice(lastSetupIndex, 0, generatorFunction);
+ } else {
+ task_list.push(generatorFunction);
+ }
+ return generatorFunction;
+ };
+})();
+
+// Like add_task, but setup tasks are executed first.
+function add_setup(generatorFunction) {
+ return add_task(generatorFunction, { isSetup: true });
+}
+
+// Request complete log when using failure patterns so that failure info
+// from infra can be useful.
+if (usesFailurePatterns()) {
+ SimpleTest.requestCompleteLog();
+}
+
+addEventListener("message", async event => {
+ if (event.data == "SimpleTest:timeout") {
+ await SimpleTest.timeout();
+ SimpleTest.finish();
+ }
+});
diff --git a/testing/mochitest/tests/SimpleTest/TestRunner.js b/testing/mochitest/tests/SimpleTest/TestRunner.js
new file mode 100644
index 0000000000..6efcec9f85
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/TestRunner.js
@@ -0,0 +1,1103 @@
+/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */
+/*
+ * e10s event dispatcher from content->chrome
+ *
+ * type = eventName (QuitApplication)
+ * data = json object {"filename":filename} <- for LoggerInit
+ */
+
+// This file expects the following files to be loaded.
+/* import-globals-from LogController.js */
+/* import-globals-from MemoryStats.js */
+/* import-globals-from MozillaLogger.js */
+
+/* eslint-disable no-unsanitized/property */
+
+"use strict";
+
+const { StructuredLogger, StructuredFormatter } =
+ SpecialPowers.ChromeUtils.importESModule(
+ "resource://testing-common/StructuredLog.sys.mjs"
+ );
+
+function getElement(id) {
+ return typeof id == "string" ? document.getElementById(id) : id;
+}
+
+this.$ = this.getElement;
+
+function contentDispatchEvent(type, data, sync) {
+ if (typeof data == "undefined") {
+ data = {};
+ }
+
+ var event = new CustomEvent("contentEvent", {
+ bubbles: true,
+ detail: {
+ sync,
+ type,
+ data: JSON.stringify(data),
+ },
+ });
+ document.dispatchEvent(event);
+}
+
+function contentAsyncEvent(type, data) {
+ contentDispatchEvent(type, data, 0);
+}
+
+/* Helper Function */
+function extend(obj, /* optional */ skip) {
+ // Extend an array with an array-like object starting
+ // from the skip index
+ if (!skip) {
+ skip = 0;
+ }
+ if (obj) {
+ var l = obj.length;
+ var ret = [];
+ for (var i = skip; i < l; i++) {
+ ret.push(obj[i]);
+ }
+ }
+ return ret;
+}
+
+function flattenArguments(lst /* ...*/) {
+ var res = [];
+ var args = extend(arguments);
+ while (args.length) {
+ var o = args.shift();
+ if (o && typeof o == "object" && typeof o.length == "number") {
+ for (var i = o.length - 1; i >= 0; i--) {
+ args.unshift(o[i]);
+ }
+ } else {
+ res.push(o);
+ }
+ }
+ return res;
+}
+
+function testInXOriginFrame() {
+ // Check if the test running in an iframe is a cross origin test.
+ try {
+ $("testframe").contentWindow.origin;
+ return false;
+ } catch (e) {
+ return true;
+ }
+}
+
+function testInDifferentProcess() {
+ // Check if the test running in an iframe that is loaded in a different process.
+ return SpecialPowers.Cu.isRemoteProxy($("testframe").contentWindow);
+}
+
+/**
+ * TestRunner: A test runner for SimpleTest
+ * TODO:
+ *
+ * * Avoid moving iframes: That causes reloads on mozilla and opera.
+ *
+ *
+ **/
+var TestRunner = {};
+TestRunner.logEnabled = false;
+TestRunner._currentTest = 0;
+TestRunner._lastTestFinished = -1;
+TestRunner._loopIsRestarting = false;
+TestRunner.currentTestURL = "";
+TestRunner.originalTestURL = "";
+TestRunner._urls = [];
+TestRunner._lastAssertionCount = 0;
+TestRunner._expectedMinAsserts = 0;
+TestRunner._expectedMaxAsserts = 0;
+
+TestRunner.timeout = 300 * 1000; // 5 minutes.
+TestRunner.maxTimeouts = 4; // halt testing after too many timeouts
+TestRunner.runSlower = false;
+TestRunner.dumpOutputDirectory = "";
+TestRunner.dumpAboutMemoryAfterTest = false;
+TestRunner.dumpDMDAfterTest = false;
+TestRunner.slowestTestTime = 0;
+TestRunner.slowestTestURL = "";
+TestRunner.interactiveDebugger = false;
+TestRunner.cleanupCrashes = false;
+TestRunner.timeoutAsPass = false;
+TestRunner.conditionedProfile = false;
+TestRunner.comparePrefs = false;
+
+TestRunner._expectingProcessCrash = false;
+TestRunner._structuredFormatter = new StructuredFormatter();
+
+/**
+ * Make sure the tests don't hang indefinitely.
+ **/
+TestRunner._numTimeouts = 0;
+TestRunner._currentTestStartTime = new Date().valueOf();
+TestRunner._timeoutFactor = 1;
+
+/**
+ * Used to collect code coverage with the js debugger.
+ */
+TestRunner.jscovDirPrefix = "";
+var coverageCollector = {};
+
+function record(succeeded, expectedFail, msg) {
+ let successInfo;
+ let failureInfo;
+ if (expectedFail) {
+ successInfo = {
+ status: "PASS",
+ expected: "FAIL",
+ message: "TEST-UNEXPECTED-PASS",
+ };
+ failureInfo = {
+ status: "FAIL",
+ expected: "FAIL",
+ message: "TEST-KNOWN-FAIL",
+ };
+ } else {
+ successInfo = {
+ status: "PASS",
+ expected: "PASS",
+ message: "TEST-PASS",
+ };
+ failureInfo = {
+ status: "FAIL",
+ expected: "PASS",
+ message: "TEST-UNEXPECTED-FAIL",
+ };
+ }
+
+ let result = succeeded ? successInfo : failureInfo;
+
+ TestRunner.structuredLogger.testStatus(
+ TestRunner.currentTestURL,
+ msg,
+ result.status,
+ result.expected,
+ "",
+ ""
+ );
+}
+
+TestRunner._checkForHangs = function () {
+ function reportError(win, msg) {
+ if (testInXOriginFrame() || "SimpleTest" in win) {
+ record(false, TestRunner.timeoutAsPass, msg);
+ } else if ("W3CTest" in win) {
+ win.W3CTest.logFailure(msg);
+ }
+ }
+
+ async function killTest(win) {
+ if (testInXOriginFrame()) {
+ win.postMessage("SimpleTest:timeout", "*");
+ } else if ("SimpleTest" in win) {
+ await win.SimpleTest.timeout();
+ win.SimpleTest.finish();
+ } else if ("W3CTest" in win) {
+ await win.W3CTest.timeout();
+ }
+ }
+
+ if (TestRunner._currentTest < TestRunner._urls.length) {
+ var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;
+ if (runtime >= TestRunner.timeout * TestRunner._timeoutFactor) {
+ let testIframe = $("testframe");
+ var frameWindow =
+ (!testInXOriginFrame() && testIframe.contentWindow.wrappedJSObject) ||
+ testIframe.contentWindow;
+ reportError(frameWindow, "Test timed out.");
+ TestRunner.updateUI([{ result: false }]);
+
+ // If we have too many timeouts, give up. We don't want to wait hours
+ // for results if some bug causes lots of tests to time out.
+ if (++TestRunner._numTimeouts >= TestRunner.maxTimeouts) {
+ TestRunner._haltTests = true;
+
+ TestRunner.currentTestURL = "(SimpleTest/TestRunner.js)";
+ reportError(
+ frameWindow,
+ TestRunner.maxTimeouts + " test timeouts, giving up."
+ );
+ var skippedTests = TestRunner._urls.length - TestRunner._currentTest;
+ reportError(
+ frameWindow,
+ "Skipping " + skippedTests + " remaining tests."
+ );
+ }
+
+ // Add a little (1 second) delay to ensure automation.py has time to notice
+ // "Test timed out" log and process it (= take a screenshot).
+ setTimeout(async function delayedKillTest() {
+ try {
+ await killTest(frameWindow);
+ } catch (e) {
+ reportError(frameWindow, "Test error: " + e);
+ TestRunner.updateUI([{ result: false }]);
+ }
+ }, 1000);
+
+ if (TestRunner._haltTests) {
+ return;
+ }
+ }
+
+ setTimeout(TestRunner._checkForHangs, 30000);
+ }
+};
+
+TestRunner.requestLongerTimeout = function (factor) {
+ TestRunner._timeoutFactor = factor;
+};
+
+/**
+ * This is used to loop tests
+ **/
+TestRunner.repeat = 0;
+TestRunner._currentLoop = 1;
+
+TestRunner.expectAssertions = function (min, max) {
+ if (typeof max == "undefined") {
+ max = min;
+ }
+ if (
+ typeof min != "number" ||
+ typeof max != "number" ||
+ min < 0 ||
+ max < min
+ ) {
+ throw new Error("bad parameter to expectAssertions");
+ }
+ TestRunner._expectedMinAsserts = min;
+ TestRunner._expectedMaxAsserts = max;
+};
+
+/**
+ * This function is called after generating the summary.
+ **/
+TestRunner.onComplete = null;
+
+/**
+ * Adds a failed test case to a list so we can rerun only the failed tests
+ **/
+TestRunner._failedTests = {};
+TestRunner._failureFile = "";
+
+TestRunner.addFailedTest = function (testName) {
+ if (TestRunner._failedTests[testName] == undefined) {
+ TestRunner._failedTests[testName] = "";
+ }
+};
+
+TestRunner.setFailureFile = function (fileName) {
+ TestRunner._failureFile = fileName;
+};
+
+TestRunner.generateFailureList = function () {
+ if (TestRunner._failureFile) {
+ var failures = new MozillaFileLogger(TestRunner._failureFile);
+ failures.log(JSON.stringify(TestRunner._failedTests));
+ failures.close();
+ }
+};
+
+/**
+ * If logEnabled is true, this is the logger that will be used.
+ **/
+
+// This delimiter is used to avoid interleaving Mochitest/Gecko logs.
+var LOG_DELIMITER = "\ue175\uee31\u2c32\uacbf";
+
+// A log callback for StructuredLog.sys.mjs
+TestRunner._dumpMessage = function (message) {
+ var str;
+
+ // This is a directive to python to format these messages
+ // for compatibility with mozharness. This can be removed
+ // with the MochitestFormatter (see bug 1045525).
+ message.js_source = "TestRunner.js";
+ if (
+ TestRunner.interactiveDebugger &&
+ message.action in TestRunner._structuredFormatter
+ ) {
+ str = TestRunner._structuredFormatter[message.action](message);
+ } else {
+ str = LOG_DELIMITER + JSON.stringify(message) + LOG_DELIMITER;
+ }
+ // BUGFIX: browser-chrome tests don't use LogController
+ if (Object.keys(LogController.listeners).length !== 0) {
+ LogController.log(str);
+ } else {
+ dump("\n" + str + "\n");
+ }
+ // Checking for error messages
+ if (message.expected || message.level === "ERROR") {
+ TestRunner.failureHandler();
+ }
+};
+
+// From https://searchfox.org/mozilla-central/source/testing/modules/StructuredLog.sys.mjs
+TestRunner.structuredLogger = new StructuredLogger(
+ "mochitest",
+ TestRunner._dumpMessage,
+ [],
+ TestRunner
+);
+TestRunner.structuredLogger.deactivateBuffering = function () {
+ TestRunner.structuredLogger.logData("buffering_off");
+};
+TestRunner.structuredLogger.activateBuffering = function () {
+ TestRunner.structuredLogger.logData("buffering_on");
+};
+
+TestRunner.log = function (msg) {
+ if (TestRunner.logEnabled) {
+ TestRunner.structuredLogger.info(msg);
+ } else {
+ dump(msg + "\n");
+ }
+};
+
+TestRunner.error = function (msg) {
+ if (TestRunner.logEnabled) {
+ TestRunner.structuredLogger.error(msg);
+ } else {
+ dump(msg + "\n");
+ TestRunner.failureHandler();
+ }
+};
+
+TestRunner.failureHandler = function () {
+ if (TestRunner.runUntilFailure) {
+ TestRunner._haltTests = true;
+ }
+
+ if (TestRunner.debugOnFailure) {
+ // You've hit this line because you requested to break into the
+ // debugger upon a testcase failure on your test run.
+ // eslint-disable-next-line no-debugger
+ debugger;
+ }
+};
+
+/**
+ * Toggle element visibility
+ **/
+TestRunner._toggle = function (el) {
+ if (el.className == "noshow") {
+ el.className = "";
+ el.style.cssText = "";
+ } else {
+ el.className = "noshow";
+ el.style.cssText = "width:0px; height:0px; border:0px;";
+ }
+};
+
+/**
+ * Creates the iframe that contains a test
+ **/
+TestRunner._makeIframe = function (url, retry) {
+ var iframe = $("testframe");
+ if (
+ url != "about:blank" &&
+ (("hasFocus" in document && !document.hasFocus()) ||
+ ("activeElement" in document && document.activeElement != iframe))
+ ) {
+ contentAsyncEvent("Focus");
+ window.focus();
+ SpecialPowers.focus();
+ iframe.focus();
+ if (retry < 3) {
+ window.setTimeout(function () {
+ TestRunner._makeIframe(url, retry + 1);
+ }, 1000);
+ return;
+ }
+
+ TestRunner.structuredLogger.info(
+ "Error: Unable to restore focus, expect failures and timeouts."
+ );
+ }
+ window.scrollTo(0, $("indicator").offsetTop);
+ try {
+ let urlObj = new URL(url);
+ if (TestRunner.xOriginTests) {
+ // The test will run in a xorigin iframe, so we pass in additional test params in the
+ // URL since the content process won't be able to access them from the parentRunner
+ // directly.
+ let params = TestRunner.getParameterInfo();
+ urlObj.searchParams.append(
+ "currentTestURL",
+ urlObj.pathname.replace("/tests/", "")
+ );
+ urlObj.searchParams.append("closeWhenDone", params.closeWhenDone);
+ urlObj.searchParams.append("showTestReport", TestRunner.showTestReport);
+ urlObj.searchParams.append("expected", TestRunner.expected);
+ iframe.src = urlObj.href;
+ } else {
+ iframe.src = url;
+ }
+ } catch {
+ // If the provided `url` is not a valid URL (i.e. doesn't include a protocol)
+ // then the new URL() constructor will raise a TypeError. This is expected in the
+ // usual case (i.e. non-xorigin iFrame tests) so set the URL in the usual way.
+ iframe.src = url;
+ }
+ iframe.name = url;
+ iframe.width = "500";
+};
+
+/**
+ * Returns the current test URL.
+ * We use this to tell whether the test has navigated to another test without
+ * being finished first.
+ */
+TestRunner.getLoadedTestURL = function () {
+ if (!testInXOriginFrame()) {
+ var prefix = "";
+ // handle mochitest-chrome URIs
+ if ($("testframe").contentWindow.location.protocol == "chrome:") {
+ prefix = "chrome://mochitests";
+ }
+ return prefix + $("testframe").contentWindow.location.pathname;
+ }
+ return TestRunner.currentTestURL;
+};
+
+TestRunner.setParameterInfo = function (params) {
+ this._params = params;
+};
+
+TestRunner.getParameterInfo = function () {
+ return this._params;
+};
+
+/**
+ * Print information about which prefs are set.
+ * This is used to help validate that the tests are actually
+ * running in the expected context.
+ */
+TestRunner.dumpPrefContext = function () {
+ let prefs = ["fission.autostart"];
+
+ let message = ["Dumping test context:"];
+ prefs.forEach(function formatPref(pref) {
+ let val = SpecialPowers.getBoolPref(pref);
+ message.push(pref + "=" + val);
+ });
+ TestRunner.structuredLogger.info(message.join("\n "));
+};
+
+/**
+ * TestRunner entry point.
+ *
+ * The arguments are the URLs of the test to be ran.
+ *
+ **/
+TestRunner.runTests = function (/*url...*/) {
+ TestRunner.structuredLogger.info("SimpleTest START");
+ TestRunner.dumpPrefContext();
+ TestRunner.originalTestURL = $("current-test").innerHTML;
+
+ SpecialPowers.registerProcessCrashObservers();
+
+ // Initialize code coverage
+ if (TestRunner.jscovDirPrefix != "") {
+ var { CoverageCollector } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://testing-common/CoverageUtils.sys.mjs"
+ );
+ coverageCollector = new CoverageCollector(TestRunner.jscovDirPrefix);
+ }
+
+ SpecialPowers.requestResetCoverageCounters().then(() => {
+ TestRunner._urls = flattenArguments(arguments);
+
+ var singleTestRun = this._urls.length <= 1 && TestRunner.repeat <= 1;
+ TestRunner.showTestReport = singleTestRun;
+ var frame = $("testframe");
+ frame.src = "";
+ if (singleTestRun) {
+ // Can't use document.body because this runs in a XUL doc as well...
+ var body = document.getElementsByTagName("body")[0];
+ body.setAttribute("singletest", "true");
+ frame.removeAttribute("scrolling");
+ }
+ TestRunner._checkForHangs();
+ TestRunner.runNextTest();
+ });
+};
+
+/**
+ * Used for running a set of tests in a loop for debugging purposes
+ * Takes an array of URLs
+ **/
+TestRunner.resetTests = function (listURLs) {
+ TestRunner._currentTest = 0;
+ // Reset our "Current-test" line - functionality depends on it
+ $("current-test").innerHTML = TestRunner.originalTestURL;
+ if (TestRunner.logEnabled) {
+ TestRunner.structuredLogger.info(
+ "SimpleTest START Loop " + TestRunner._currentLoop
+ );
+ }
+
+ TestRunner._urls = listURLs;
+ $("testframe").src = "";
+ TestRunner._checkForHangs();
+ TestRunner.runNextTest();
+};
+
+TestRunner.getNextUrl = function () {
+ var url = "";
+ // sometimes we have a subtest/harness which doesn't use a manifest
+ if (
+ TestRunner._urls[TestRunner._currentTest] instanceof Object &&
+ "test" in TestRunner._urls[TestRunner._currentTest]
+ ) {
+ url = TestRunner._urls[TestRunner._currentTest].test.url;
+ TestRunner.expected =
+ TestRunner._urls[TestRunner._currentTest].test.expected;
+ } else {
+ url = TestRunner._urls[TestRunner._currentTest];
+ TestRunner.expected = "pass";
+ }
+ return url;
+};
+
+/**
+ * Run the next test. If no test remains, calls onComplete().
+ **/
+TestRunner._haltTests = false;
+async function _runNextTest() {
+ if (
+ TestRunner._currentTest < TestRunner._urls.length &&
+ !TestRunner._haltTests
+ ) {
+ var url = TestRunner.getNextUrl();
+ TestRunner.currentTestURL = url;
+
+ $("current-test-path").innerHTML = url;
+
+ TestRunner._currentTestStartTimestamp = SpecialPowers.Cu.now();
+ TestRunner._currentTestStartTime = new Date().valueOf();
+ TestRunner._timeoutFactor = 1;
+ TestRunner._expectedMinAsserts = 0;
+ TestRunner._expectedMaxAsserts = 0;
+
+ TestRunner.structuredLogger.testStart(url);
+
+ if (TestRunner._urls[TestRunner._currentTest].test.allow_xul_xbl) {
+ await SpecialPowers.pushPermissions([
+ { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" },
+ { type: "allowXULXBL", allow: true, context: "http://example.org" },
+ ]);
+ }
+ TestRunner._makeIframe(url, 0);
+ } else {
+ $("current-test").innerHTML = "<b>Finished</b>";
+ // Only unload the last test to run if we're running more than one test.
+ if (TestRunner._urls.length > 1) {
+ TestRunner._makeIframe("about:blank", 0);
+ }
+
+ var passCount = parseInt($("pass-count").innerHTML, 10);
+ var failCount = parseInt($("fail-count").innerHTML, 10);
+ var todoCount = parseInt($("todo-count").innerHTML, 10);
+
+ if (passCount === 0 && failCount === 0 && todoCount === 0) {
+ // No |$('testframe').contentWindow|, so manually update: ...
+ // ... the log,
+ TestRunner.structuredLogger.error(
+ "TEST-UNEXPECTED-FAIL | SimpleTest/TestRunner.js | No checks actually run"
+ );
+ // ... the count,
+ $("fail-count").innerHTML = 1;
+ // ... the indicator.
+ var indicator = $("indicator");
+ indicator.innerHTML = "Status: Fail (No checks actually run)";
+ indicator.style.backgroundColor = "red";
+ }
+
+ let e10sMode = SpecialPowers.isMainProcess() ? "non-e10s" : "e10s";
+
+ TestRunner.structuredLogger.info("TEST-START | Shutdown");
+ TestRunner.structuredLogger.info("Passed: " + passCount);
+ TestRunner.structuredLogger.info("Failed: " + failCount);
+ TestRunner.structuredLogger.info("Todo: " + todoCount);
+ TestRunner.structuredLogger.info("Mode: " + e10sMode);
+ TestRunner.structuredLogger.info(
+ "Slowest: " +
+ TestRunner.slowestTestTime +
+ "ms - " +
+ TestRunner.slowestTestURL
+ );
+
+ // If we are looping, don't send this cause it closes the log file,
+ // also don't unregister the crash observers until we're done.
+ if (TestRunner.repeat === 0) {
+ SpecialPowers.unregisterProcessCrashObservers();
+ TestRunner.structuredLogger.info("SimpleTest FINISHED");
+ }
+
+ if (TestRunner.repeat === 0 && TestRunner.onComplete) {
+ TestRunner.onComplete();
+ }
+
+ if (
+ TestRunner._currentLoop <= TestRunner.repeat &&
+ !TestRunner._haltTests
+ ) {
+ TestRunner._currentLoop++;
+ TestRunner.resetTests(TestRunner._urls);
+ TestRunner._loopIsRestarting = true;
+ } else {
+ // Loops are finished
+ if (TestRunner.logEnabled) {
+ TestRunner.structuredLogger.info(
+ "TEST-INFO | Ran " + TestRunner._currentLoop + " Loops"
+ );
+ TestRunner.structuredLogger.info("SimpleTest FINISHED");
+ }
+
+ if (TestRunner.onComplete) {
+ TestRunner.onComplete();
+ }
+ }
+ TestRunner.generateFailureList();
+
+ if (TestRunner.jscovDirPrefix != "") {
+ coverageCollector.finalize();
+ }
+ }
+}
+TestRunner.runNextTest = _runNextTest;
+
+TestRunner.expectChildProcessCrash = function () {
+ TestRunner._expectingProcessCrash = true;
+};
+
+/**
+ * This stub is called by SimpleTest when a test is finished.
+ **/
+TestRunner.testFinished = function (tests) {
+ // Need to track subtests recorded here separately or else they'll
+ // trigger the `result after SimpleTest.finish()` error.
+ var extraTests = [];
+ var result = "OK";
+
+ // Prevent a test from calling finish() multiple times before we
+ // have a chance to unload it.
+ if (
+ TestRunner._currentTest == TestRunner._lastTestFinished &&
+ !TestRunner._loopIsRestarting
+ ) {
+ TestRunner.structuredLogger.testEnd(
+ TestRunner.currentTestURL,
+ "ERROR",
+ "OK",
+ "called finish() multiple times"
+ );
+ TestRunner.updateUI([{ result: false }]);
+ return;
+ }
+
+ if (TestRunner.jscovDirPrefix != "") {
+ coverageCollector.recordTestCoverage(TestRunner.currentTestURL);
+ }
+
+ SpecialPowers.requestDumpCoverageCounters().then(() => {
+ TestRunner._lastTestFinished = TestRunner._currentTest;
+ TestRunner._loopIsRestarting = false;
+
+ // TODO : replace this by a function that returns the mem data as an object
+ // that's dumped later with the test_end message
+ MemoryStats.dump(
+ TestRunner._currentTest,
+ TestRunner.currentTestURL,
+ TestRunner.dumpOutputDirectory,
+ TestRunner.dumpAboutMemoryAfterTest,
+ TestRunner.dumpDMDAfterTest
+ );
+
+ async function cleanUpCrashDumpFiles() {
+ if (
+ !(await SpecialPowers.removeExpectedCrashDumpFiles(
+ TestRunner._expectingProcessCrash
+ ))
+ ) {
+ let subtest = "expected-crash-dump-missing";
+ TestRunner.structuredLogger.testStatus(
+ TestRunner.currentTestURL,
+ subtest,
+ "ERROR",
+ "PASS",
+ "This test did not leave any crash dumps behind, but we were expecting some!"
+ );
+ extraTests.push({ name: subtest, result: false });
+ result = "ERROR";
+ }
+
+ var unexpectedCrashDumpFiles =
+ await SpecialPowers.findUnexpectedCrashDumpFiles();
+ TestRunner._expectingProcessCrash = false;
+ if (unexpectedCrashDumpFiles.length) {
+ let subtest = "unexpected-crash-dump-found";
+ TestRunner.structuredLogger.testStatus(
+ TestRunner.currentTestURL,
+ subtest,
+ "ERROR",
+ "PASS",
+ "This test left crash dumps behind, but we " +
+ "weren't expecting it to!",
+ null,
+ { unexpected_crashdump_files: unexpectedCrashDumpFiles }
+ );
+ extraTests.push({ name: subtest, result: false });
+ result = "CRASH";
+ unexpectedCrashDumpFiles.sort().forEach(function (aFilename) {
+ TestRunner.structuredLogger.info(
+ "Found unexpected crash dump file " + aFilename + "."
+ );
+ });
+ }
+
+ if (TestRunner.cleanupCrashes) {
+ if (await SpecialPowers.removePendingCrashDumpFiles()) {
+ TestRunner.structuredLogger.info(
+ "This test left pending crash dumps"
+ );
+ }
+ }
+ }
+
+ function runNextTest() {
+ if (TestRunner.currentTestURL != TestRunner.getLoadedTestURL()) {
+ TestRunner.structuredLogger.testStatus(
+ TestRunner.currentTestURL,
+ TestRunner.getLoadedTestURL(),
+ "FAIL",
+ "PASS",
+ "finished in a non-clean fashion, probably" +
+ " because it didn't call SimpleTest.finish()",
+ { loaded_test_url: TestRunner.getLoadedTestURL() }
+ );
+ extraTests.push({ name: "clean-finish", result: false });
+ result = result != "CRASH" ? "ERROR" : result;
+ }
+
+ SpecialPowers.addProfilerMarker(
+ "TestRunner",
+ { category: "Test", startTime: TestRunner._currentTestStartTimestamp },
+ TestRunner.currentTestURL
+ );
+ var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;
+
+ TestRunner.structuredLogger.testEnd(
+ TestRunner.currentTestURL,
+ result,
+ "OK",
+ "Finished in " + runtime + "ms",
+ { runtime }
+ );
+
+ if (
+ TestRunner.slowestTestTime < runtime &&
+ TestRunner._timeoutFactor >= 1
+ ) {
+ TestRunner.slowestTestTime = runtime;
+ TestRunner.slowestTestURL = TestRunner.currentTestURL;
+ }
+
+ TestRunner.updateUI(tests.concat(extraTests));
+
+ // Don't show the interstitial if we just run one test with no repeats:
+ if (TestRunner._urls.length == 1 && TestRunner.repeat <= 1) {
+ TestRunner.testUnloaded();
+ return;
+ }
+
+ var interstitialURL;
+ if (
+ !testInXOriginFrame() &&
+ $("testframe").contentWindow.location.protocol == "chrome:"
+ ) {
+ interstitialURL = "tests/SimpleTest/iframe-between-tests.html";
+ } else {
+ interstitialURL = "/tests/SimpleTest/iframe-between-tests.html";
+ }
+ // check if there were test run after SimpleTest.finish, which should never happen
+ if (!testInXOriginFrame()) {
+ $("testframe").contentWindow.addEventListener("unload", function () {
+ var testwin = $("testframe").contentWindow;
+ if (
+ testwin.SimpleTest &&
+ testwin.SimpleTest._tests.length != testwin.SimpleTest.testsLength
+ ) {
+ var wrongtestlength =
+ testwin.SimpleTest._tests.length - testwin.SimpleTest.testsLength;
+ var wrongtestname = "";
+ for (var i = 0; i < wrongtestlength; i++) {
+ wrongtestname =
+ testwin.SimpleTest._tests[testwin.SimpleTest.testsLength + i]
+ .name;
+ TestRunner.structuredLogger.error(
+ "TEST-UNEXPECTED-FAIL | " +
+ TestRunner.currentTestURL +
+ " logged result after SimpleTest.finish(): " +
+ wrongtestname
+ );
+ }
+ TestRunner.updateUI([{ result: false }]);
+ }
+ });
+ }
+ TestRunner._makeIframe(interstitialURL, 0);
+ }
+
+ SpecialPowers.executeAfterFlushingMessageQueue(async function () {
+ await SpecialPowers.waitForCrashes(TestRunner._expectingProcessCrash);
+ await cleanUpCrashDumpFiles();
+ await SpecialPowers.flushPermissions();
+ await SpecialPowers.flushPrefEnv();
+ runNextTest();
+ });
+ });
+};
+
+/**
+ * This stub is called by XOrigin Tests to report assertion count.
+ **/
+TestRunner._xoriginAssertionCount = 0;
+TestRunner.addAssertionCount = function (count) {
+ if (!testInXOriginFrame()) {
+ TestRunner.error(
+ `addAssertionCount should only be called by a cross origin test`
+ );
+ return;
+ }
+
+ if (testInDifferentProcess()) {
+ TestRunner._xoriginAssertionCount += count;
+ }
+};
+
+TestRunner.testUnloaded = function () {
+ // If we're in a debug build, check assertion counts. This code is
+ // similar to the code in Tester_nextTest in browser-test.js used
+ // for browser-chrome mochitests.
+ if (SpecialPowers.isDebugBuild) {
+ var newAssertionCount =
+ SpecialPowers.assertionCount() + TestRunner._xoriginAssertionCount;
+ var numAsserts = newAssertionCount - TestRunner._lastAssertionCount;
+ TestRunner._lastAssertionCount = newAssertionCount;
+
+ var max = TestRunner._expectedMaxAsserts;
+ var min = TestRunner._expectedMinAsserts;
+ if (Array.isArray(TestRunner.expected)) {
+ // Accumulate all assertion counts recorded in the failure pattern file.
+ let additionalAsserts = TestRunner.expected.reduce(
+ (acc, [pat, count]) => {
+ return pat == "ASSERTION" ? acc + count : acc;
+ },
+ 0
+ );
+ min += additionalAsserts;
+ max += additionalAsserts;
+ }
+ TestRunner.structuredLogger.assertionCount(
+ TestRunner.currentTestURL,
+ numAsserts,
+ min,
+ max
+ );
+ }
+
+ // Always do this, so we can "reset" preferences between tests
+ SpecialPowers.comparePrefsToBaseline(
+ TestRunner.ignorePrefs,
+ TestRunner.verifyPrefsNextTest
+ );
+};
+
+TestRunner.verifyPrefsNextTest = function (p) {
+ if (TestRunner.comparePrefs) {
+ let prefs = Array.from(SpecialPowers.Cu.waiveXrays(p), x =>
+ SpecialPowers.unwrapIfWrapped(SpecialPowers.Cu.unwaiveXrays(x))
+ );
+ prefs.forEach(pr =>
+ TestRunner.structuredLogger.error(
+ "TEST-UNEXPECTED-FAIL | " +
+ TestRunner.currentTestURL +
+ " | changed preference: " +
+ pr
+ )
+ );
+ }
+ TestRunner.doNextTest();
+};
+
+TestRunner.doNextTest = function () {
+ TestRunner._currentTest++;
+ if (TestRunner.runSlower) {
+ setTimeout(TestRunner.runNextTest, 1000);
+ } else {
+ TestRunner.runNextTest();
+ }
+};
+
+/**
+ * Get the results.
+ */
+TestRunner.countResults = function (tests) {
+ var nOK = 0;
+ var nNotOK = 0;
+ var nTodo = 0;
+ for (var i = 0; i < tests.length; ++i) {
+ var test = tests[i];
+ if (test.todo && !test.result) {
+ nTodo++;
+ } else if (test.result && !test.todo) {
+ nOK++;
+ } else {
+ nNotOK++;
+ }
+ }
+ return { OK: nOK, notOK: nNotOK, todo: nTodo };
+};
+
+/**
+ * Print out table of any error messages found during looped run
+ */
+TestRunner.displayLoopErrors = function (tableName, tests) {
+ if (TestRunner.countResults(tests).notOK > 0) {
+ var table = $(tableName);
+ var curtest;
+ if (!table.rows.length) {
+ //if table headers are not yet generated, make them
+ var row = table.insertRow(table.rows.length);
+ var cell = row.insertCell(0);
+ var textNode = document.createTextNode("Test File Name:");
+ cell.appendChild(textNode);
+ cell = row.insertCell(1);
+ textNode = document.createTextNode("Test:");
+ cell.appendChild(textNode);
+ cell = row.insertCell(2);
+ textNode = document.createTextNode("Error message:");
+ cell.appendChild(textNode);
+ }
+
+ //find the broken test
+ for (var testnum in tests) {
+ curtest = tests[testnum];
+ if (
+ !(
+ (curtest.todo && !curtest.result) ||
+ (curtest.result && !curtest.todo)
+ )
+ ) {
+ //this is a failed test or the result of todo test. Display the related message
+ row = table.insertRow(table.rows.length);
+ cell = row.insertCell(0);
+ textNode = document.createTextNode(TestRunner.currentTestURL);
+ cell.appendChild(textNode);
+ cell = row.insertCell(1);
+ textNode = document.createTextNode(curtest.name);
+ cell.appendChild(textNode);
+ cell = row.insertCell(2);
+ textNode = document.createTextNode(curtest.diag ? curtest.diag : "");
+ cell.appendChild(textNode);
+ }
+ }
+ }
+};
+
+TestRunner.updateUI = function (tests) {
+ var results = TestRunner.countResults(tests);
+ var passCount = parseInt($("pass-count").innerHTML) + results.OK;
+ var failCount = parseInt($("fail-count").innerHTML) + results.notOK;
+ var todoCount = parseInt($("todo-count").innerHTML) + results.todo;
+ $("pass-count").innerHTML = passCount;
+ $("fail-count").innerHTML = failCount;
+ $("todo-count").innerHTML = todoCount;
+
+ // Set the top Green/Red bar
+ var indicator = $("indicator");
+ if (failCount > 0) {
+ indicator.innerHTML = "Status: Fail";
+ indicator.style.backgroundColor = "red";
+ } else if (passCount > 0) {
+ indicator.innerHTML = "Status: Pass";
+ indicator.style.backgroundColor = "#0d0";
+ } else {
+ indicator.innerHTML = "Status: ToDo";
+ indicator.style.backgroundColor = "orange";
+ }
+
+ // Set the table values
+ var trID = "tr-" + $("current-test-path").innerHTML;
+ var row = $(trID);
+
+ // Only update the row if it actually exists (autoUI)
+ if (row != null) {
+ var tds = row.getElementsByTagName("td");
+ tds[0].style.backgroundColor = "#0d0";
+ tds[0].innerHTML = parseInt(tds[0].innerHTML) + parseInt(results.OK);
+ tds[1].style.backgroundColor = results.notOK > 0 ? "red" : "#0d0";
+ tds[1].innerHTML = parseInt(tds[1].innerHTML) + parseInt(results.notOK);
+ tds[2].style.backgroundColor = results.todo > 0 ? "orange" : "#0d0";
+ tds[2].innerHTML = parseInt(tds[2].innerHTML) + parseInt(results.todo);
+ }
+
+ //if we ran in a loop, display any found errors
+ if (TestRunner.repeat > 0) {
+ TestRunner.displayLoopErrors("fail-table", tests);
+ }
+};
+
+// XOrigin Tests
+// If "--enable-xorigin-tests" is set, mochitests are run in a cross origin iframe.
+// The parent process will run at http://mochi.xorigin-test:8888", and individual
+// mochitests will be launched in a cross-origin iframe at http://mochi.test:8888.
+
+var xOriginDispatchMap = {
+ runner: TestRunner,
+ logger: TestRunner.structuredLogger,
+ addFailedTest: TestRunner.addFailedTest,
+ expectAssertions: TestRunner.expectAssertions,
+ expectChildProcessCrash: TestRunner.expectChildProcessCrash,
+ requestLongerTimeout: TestRunner.requestLongerTimeout,
+ "structuredLogger.deactivateBuffering":
+ TestRunner.structuredLogger.deactivateBuffering,
+ "structuredLogger.activateBuffering":
+ TestRunner.structuredLogger.activateBuffering,
+ "structuredLogger.testStatus": TestRunner.structuredLogger.testStatus,
+ "structuredLogger.info": TestRunner.structuredLogger.info,
+ "structuredLogger.warning": TestRunner.structuredLogger.warning,
+ "structuredLogger.error": TestRunner.structuredLogger.error,
+ testFinished: TestRunner.testFinished,
+ addAssertionCount: TestRunner.addAssertionCount,
+};
+
+function xOriginTestRunnerHandler(event) {
+ if (event.data.harnessType != "SimpleTest") {
+ return;
+ }
+ // Handles messages from xOriginRunner in SimpleTest.js.
+ if (event.data.command in xOriginDispatchMap) {
+ xOriginDispatchMap[event.data.command].apply(
+ xOriginDispatchMap[event.data.applyOn],
+ event.data.params
+ );
+ } else {
+ TestRunner.error(`Command ${event.data.command} not found
+ in xOriginDispatchMap`);
+ }
+}
+
+TestRunner.setXOriginEventHandler = function () {
+ window.addEventListener("message", xOriginTestRunnerHandler);
+};
diff --git a/testing/mochitest/tests/SimpleTest/WindowSnapshot.js b/testing/mochitest/tests/SimpleTest/WindowSnapshot.js
new file mode 100644
index 0000000000..d1925f36e6
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/WindowSnapshot.js
@@ -0,0 +1,122 @@
+var gWindowUtils;
+
+try {
+ gWindowUtils = SpecialPowers.getDOMWindowUtils(window);
+ if (gWindowUtils && !gWindowUtils.compareCanvases) {
+ gWindowUtils = null;
+ }
+} catch (e) {
+ gWindowUtils = null;
+}
+
+function snapshotWindow(win, withCaret) {
+ return SpecialPowers.snapshotWindow(win, withCaret);
+}
+
+function snapshotRect(win, rect) {
+ return SpecialPowers.snapshotRect(win, rect);
+}
+
+// If the two snapshots don't compare as expected (true for equal, false for
+// unequal), returns their serializations as data URIs. In all cases, returns
+// whether the comparison was as expected.
+function compareSnapshots(s1, s2, expectEqual, fuzz) {
+ if (s1.width != s2.width || s1.height != s2.height) {
+ ok(
+ false,
+ "Snapshot canvases are not the same size: " +
+ s1.width +
+ "x" +
+ s1.height +
+ " vs. " +
+ s2.width +
+ "x" +
+ s2.height
+ );
+ return [false];
+ }
+ var passed = false;
+ var numDifferentPixels;
+ var maxDifference = { value: undefined };
+ if (gWindowUtils) {
+ var equal;
+ try {
+ numDifferentPixels = gWindowUtils.compareCanvases(s1, s2, maxDifference);
+ if (!fuzz) {
+ equal = numDifferentPixels == 0;
+ } else {
+ equal =
+ numDifferentPixels <= fuzz.numDifferentPixels &&
+ maxDifference.value <= fuzz.maxDifference;
+ }
+ passed = equal == expectEqual;
+ } catch (e) {
+ ok(false, "Exception thrown from compareCanvases: " + e);
+ }
+ }
+
+ var s1DataURI, s2DataURI;
+ if (!passed) {
+ s1DataURI = s1.toDataURL();
+ s2DataURI = s2.toDataURL();
+
+ if (!gWindowUtils) {
+ passed = (s1DataURI == s2DataURI) == expectEqual;
+ }
+ }
+
+ return [
+ passed,
+ s1DataURI,
+ s2DataURI,
+ numDifferentPixels,
+ maxDifference.value,
+ ];
+}
+
+function assertSnapshots(s1, s2, expectEqual, fuzz, s1name, s2name) {
+ var [passed, s1DataURI, s2DataURI, numDifferentPixels, maxDifference] =
+ compareSnapshots(s1, s2, expectEqual, fuzz);
+ var sym = expectEqual ? "==" : "!=";
+ ok(passed, "reftest comparison: " + sym + " " + s1name + " " + s2name);
+ if (!passed) {
+ let status = "TEST-UNEXPECTED-FAIL";
+ if (usesFailurePatterns() && recordIfMatchesFailurePattern(s1name)) {
+ status = "TEST-KNOWN-FAIL";
+ }
+ // The language / format in this message should match the failure messages
+ // displayed by reftest.js's "RecordResult()" method so that log output
+ // can be parsed by reftest-analyzer.xhtml
+ var report =
+ "REFTEST " +
+ status +
+ " | " +
+ s1name +
+ " | image comparison (" +
+ sym +
+ "), max difference: " +
+ maxDifference +
+ ", number of differing pixels: " +
+ numDifferentPixels +
+ "\n";
+ if (expectEqual) {
+ report += "REFTEST IMAGE 1 (TEST): " + s1DataURI + "\n";
+ report += "REFTEST IMAGE 2 (REFERENCE): " + s2DataURI + "\n";
+ } else {
+ report += "REFTEST IMAGE: " + s1DataURI + "\n";
+ }
+ (info || dump)(report);
+ }
+ return passed;
+}
+
+function assertWindowPureColor(win, color) {
+ const snapshot = SpecialPowers.snapshotRect(win);
+ const canvas = document.createElement("canvas");
+ canvas.width = snapshot.width;
+ canvas.height = snapshot.height;
+ const context = canvas.getContext("2d");
+ context.fillStyle = color;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ assertSnapshots(snapshot, canvas, true, null, "snapshot", color);
+}
diff --git a/testing/mochitest/tests/SimpleTest/WorkerHandler.js b/testing/mochitest/tests/SimpleTest/WorkerHandler.js
new file mode 100644
index 0000000000..7794087805
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/WorkerHandler.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Sets the worker message and error event handlers to respond to SimpleTest
+ * style messages.
+ */
+function listenForTests(worker, opts = { verbose: true }) {
+ worker.onerror = function (error) {
+ error.preventDefault();
+ ok(false, "Worker error " + error.message);
+ };
+ worker.onmessage = function (msg) {
+ if (opts && opts.verbose) {
+ ok(true, "MAIN: onmessage " + JSON.stringify(msg.data));
+ }
+ switch (msg.data.kind) {
+ case "is":
+ SimpleTest.ok(
+ msg.data.outcome,
+ msg.data.description + "( " + msg.data.a + " ==? " + msg.data.b + ")"
+ );
+ return;
+ case "isnot":
+ SimpleTest.ok(
+ msg.data.outcome,
+ msg.data.description + "( " + msg.data.a + " !=? " + msg.data.b + ")"
+ );
+ return;
+ case "ok":
+ SimpleTest.ok(msg.data.condition, msg.data.description);
+ return;
+ case "info":
+ SimpleTest.info(msg.data.description);
+ return;
+ case "finish":
+ SimpleTest.finish();
+ return;
+ default:
+ SimpleTest.ok(
+ false,
+ "test_osfile.xul: wrong message " + JSON.stringify(msg.data)
+ );
+ }
+ };
+}
diff --git a/testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js b/testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js
new file mode 100644
index 0000000000..ce4848d7af
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function log(text) {
+ dump("WORKER " + text + "\n");
+}
+
+function send(message) {
+ self.postMessage(message);
+}
+
+function finish() {
+ send({ kind: "finish" });
+}
+
+function ok(condition, description) {
+ send({ kind: "ok", condition: !!condition, description: "" + description });
+}
+
+function is(a, b, description) {
+ let outcome = a == b; // Need to decide outcome here, as not everything can be serialized
+ send({
+ kind: "is",
+ outcome,
+ description: "" + description,
+ a: "" + a,
+ b: "" + b,
+ });
+}
+
+function isnot(a, b, description) {
+ let outcome = a != b; // Need to decide outcome here, as not everything can be serialized
+ send({
+ kind: "isnot",
+ outcome,
+ description: "" + description,
+ a: "" + a,
+ b: "" + b,
+ });
+}
+
+function info(description) {
+ send({ kind: "info", description: "" + description });
+}
diff --git a/testing/mochitest/tests/SimpleTest/iframe-between-tests.html b/testing/mochitest/tests/SimpleTest/iframe-between-tests.html
new file mode 100644
index 0000000000..8de879f205
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/iframe-between-tests.html
@@ -0,0 +1,17 @@
+<title>iframe for between tests</title>
+<!--
+ This page exists so that our accounting for assertions correctly
+ counts assertions that happen while leaving a page. We load this page
+ after a test finishes, check the assertion counts, and then go on to
+ load the next.
+-->
+<script>
+window.addEventListener("load", function() {
+ var runner = (parent.TestRunner || parent.wrappedJSObject.TestRunner);
+ runner.testUnloaded();
+
+ if (SpecialPowers) {
+ SpecialPowers.DOMWindowUtils.runNextCollectorTimer();
+ }
+});
+</script>
diff --git a/testing/mochitest/tests/SimpleTest/moz.build b/testing/mochitest/tests/SimpleTest/moz.build
new file mode 100644
index 0000000000..55c54e6255
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+TEST_HARNESS_FILES.testing.mochitest.tests.SimpleTest += [
+ "/docshell/test/chrome/docshell_helpers.js",
+ "AccessibilityUtils.js",
+ "ChromeTask.js",
+ "EventUtils.js",
+ "ExtensionTestUtils.js",
+ "iframe-between-tests.html",
+ "LogController.js",
+ "MemoryStats.js",
+ "MockObjects.js",
+ "MozillaLogger.js",
+ "NativeKeyCodes.js",
+ "paint_listener.js",
+ "setup.js",
+ "SimpleTest.js",
+ "test.css",
+ "TestRunner.js",
+ "WindowSnapshot.js",
+ "WorkerHandler.js",
+ "WorkerSimpleTest.js",
+]
diff --git a/testing/mochitest/tests/SimpleTest/paint_listener.js b/testing/mochitest/tests/SimpleTest/paint_listener.js
new file mode 100644
index 0000000000..2fc6ab425a
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/paint_listener.js
@@ -0,0 +1,109 @@
+(function () {
+ var accumulatedRect = null;
+ var onpaint = [];
+ var debug = SpecialPowers.getBoolPref("testing.paint_listener.debug", false);
+ const FlushModes = {
+ FLUSH: 0,
+ NOFLUSH: 1,
+ };
+
+ function paintListener(event) {
+ if (event.target != window) {
+ if (debug) {
+ dump("got MozAfterPaint for wrong window\n");
+ }
+ return;
+ }
+ var clientRect = event.boundingClientRect;
+ var eventRect;
+ if (clientRect) {
+ eventRect = [
+ clientRect.left,
+ clientRect.top,
+ clientRect.right,
+ clientRect.bottom,
+ ];
+ } else {
+ eventRect = [0, 0, 0, 0];
+ }
+ if (debug) {
+ dump("got MozAfterPaint: " + eventRect.join(",") + "\n");
+ }
+ accumulatedRect = accumulatedRect
+ ? [
+ Math.min(accumulatedRect[0], eventRect[0]),
+ Math.min(accumulatedRect[1], eventRect[1]),
+ Math.max(accumulatedRect[2], eventRect[2]),
+ Math.max(accumulatedRect[3], eventRect[3]),
+ ]
+ : eventRect;
+ if (debug) {
+ dump("Dispatching " + onpaint.length + " onpaint listeners\n");
+ }
+ while (onpaint.length) {
+ window.setTimeout(onpaint.pop(), 0);
+ }
+ }
+ window.addEventListener("MozAfterPaint", paintListener);
+
+ function waitForPaints(callback, subdoc, flushMode) {
+ // Wait until paint suppression has ended
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ if (utils.paintingSuppressed) {
+ if (debug) {
+ dump("waiting for paint suppression to end...\n");
+ }
+ window.setTimeout(function () {
+ waitForPaints(callback, subdoc, flushMode);
+ }, 0);
+ return;
+ }
+
+ // The call to getBoundingClientRect will flush pending layout
+ // notifications. Sometimes, however, this is undesirable since it can mask
+ // bugs where the code under test should be performing the flush.
+ if (flushMode === FlushModes.FLUSH) {
+ document.documentElement.getBoundingClientRect();
+ if (subdoc) {
+ subdoc.documentElement.getBoundingClientRect();
+ }
+ }
+
+ if (utils.isMozAfterPaintPending) {
+ if (debug) {
+ dump("waiting for paint...\n");
+ }
+ onpaint.push(function () {
+ waitForPaints(callback, subdoc, FlushModes.NOFLUSH);
+ });
+ if (utils.isTestControllingRefreshes) {
+ utils.advanceTimeAndRefresh(0);
+ }
+ return;
+ }
+
+ if (debug) {
+ dump("done...\n");
+ }
+ var result = accumulatedRect || [0, 0, 0, 0];
+ accumulatedRect = null;
+ callback.apply(null, result);
+ }
+
+ window.waitForAllPaintsFlushed = function (callback, subdoc) {
+ waitForPaints(callback, subdoc, FlushModes.FLUSH);
+ };
+
+ window.waitForAllPaints = function (callback) {
+ waitForPaints(callback, null, FlushModes.NOFLUSH);
+ };
+
+ window.promiseAllPaintsDone = function (subdoc = null, flush = false) {
+ var flushmode = flush ? FlushModes.FLUSH : FlushModes.NOFLUSH;
+ return new Promise(function (resolve, reject) {
+ // The callback is given the components of the rect, but resolve() can
+ // only be given one arg, so we turn it back into an array.
+ waitForPaints((l, r, t, b) => resolve([l, r, t, b]), subdoc, flushmode);
+ });
+ };
+})();
diff --git a/testing/mochitest/tests/SimpleTest/setup.js b/testing/mochitest/tests/SimpleTest/setup.js
new file mode 100644
index 0000000000..0dd4770c0b
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/setup.js
@@ -0,0 +1,383 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This file expects the following files to be loaded.
+/* import-globals-from TestRunner.js */
+
+// From the harness:
+/* import-globals-from ../../chrome-harness.js */
+/* import-globals-from ../../chunkifyTests.js */
+
+// It appears we expect these from one of the MochiKit scripts.
+/* global toggleElementClass, removeElementClass, addElementClass,
+ hasElementClass */
+
+TestRunner.logEnabled = true;
+TestRunner.logger = LogController;
+
+if (!("SpecialPowers" in window)) {
+ dump("SimpleTest setup.js found SpecialPowers unavailable: reloading...\n");
+ setTimeout(() => {
+ window.location.reload();
+ }, 1000);
+}
+
+/* Helper function */
+function parseQueryString(encodedString, useArrays) {
+ // strip a leading '?' from the encoded string
+ var qstr =
+ encodedString.length && encodedString[0] == "?"
+ ? encodedString.substring(1)
+ : encodedString;
+ var pairs = qstr.replace(/\+/g, "%20").split(/(\&amp\;|\&\#38\;|\&#x26;|\&)/);
+ var o = {};
+ var decode;
+ if (typeof decodeURIComponent != "undefined") {
+ decode = decodeURIComponent;
+ } else {
+ decode = unescape;
+ }
+ if (useArrays) {
+ for (var i = 0; i < pairs.length; i++) {
+ var pair = pairs[i].split("=");
+ if (pair.length !== 2) {
+ continue;
+ }
+ var name = decode(pair[0]);
+ var arr = o[name];
+ if (!(arr instanceof Array)) {
+ arr = [];
+ o[name] = arr;
+ }
+ arr.push(decode(pair[1]));
+ }
+ } else {
+ for (i = 0; i < pairs.length; i++) {
+ pair = pairs[i].split("=");
+ if (pair.length !== 2) {
+ continue;
+ }
+ o[decode(pair[0])] = decode(pair[1]);
+ }
+ }
+ return o;
+}
+
+/* helper function, specifically for prefs to ignore */
+function loadFile(url, callback) {
+ let req = new XMLHttpRequest();
+ req.open("GET", url);
+ req.onload = function () {
+ if (req.readyState == 4) {
+ if (req.status == 200) {
+ try {
+ let prefs = JSON.parse(req.responseText);
+ callback(prefs);
+ } catch (e) {
+ dump(
+ "TEST-UNEXPECTED-FAIL: setup.js | error parsing " +
+ url +
+ " (" +
+ e +
+ ")\n"
+ );
+ throw e;
+ }
+ } else {
+ dump(
+ "TEST-UNEXPECTED-FAIL: setup.js | error loading " +
+ url +
+ " (HTTP " +
+ req.status +
+ ")\n"
+ );
+ callback({});
+ }
+ }
+ };
+ req.send();
+}
+
+// Check the query string for arguments
+var params = parseQueryString(location.search.substring(1), true);
+
+var config = {};
+if (window.readConfig) {
+ config = readConfig();
+}
+
+if (config.testRoot == "chrome" || config.testRoot == "a11y") {
+ for (var p in params) {
+ // Compare with arrays to find boolean equivalents, since that's what
+ // |parseQueryString| with useArrays returns.
+ if (params[p] == [1]) {
+ config[p] = true;
+ } else if (params[p] == [0]) {
+ config[p] = false;
+ } else {
+ config[p] = params[p];
+ }
+ }
+ params = config;
+ params.baseurl = "chrome://mochitests/content";
+} else if (params.xOriginTests) {
+ params.baseurl = "http://mochi.test:8888/tests/";
+} else {
+ params.baseurl = "";
+}
+
+if (params.testRoot == "browser") {
+ params.testPrefix = "chrome://mochitests/content/browser/";
+} else if (params.testRoot == "chrome") {
+ params.testPrefix = "chrome://mochitests/content/chrome/";
+} else if (params.testRoot == "a11y") {
+ params.testPrefix = "chrome://mochitests/content/a11y/";
+} else if (params.xOriginTests) {
+ params.testPrefix = "http://mochi.test:8888/tests/";
+ params.httpsBaseUrl = "https://example.org:443/tests/";
+} else {
+ params.testPrefix = "/tests/";
+}
+
+// set the per-test timeout if specified in the query string
+if (params.timeout) {
+ TestRunner.timeout = parseInt(params.timeout) * 1000;
+}
+
+// log levels for console and logfile
+var fileLevel = params.fileLevel || null;
+var consoleLevel = params.consoleLevel || null;
+
+// repeat tells us how many times to repeat the tests
+if (params.repeat) {
+ TestRunner.repeat = params.repeat;
+}
+
+if (params.runUntilFailure) {
+ TestRunner.runUntilFailure = true;
+}
+
+// closeWhenDone tells us to close the browser when complete
+if (params.closeWhenDone) {
+ TestRunner.onComplete = SpecialPowers.quit.bind(SpecialPowers);
+}
+
+if (params.failureFile) {
+ TestRunner.setFailureFile(params.failureFile);
+}
+
+// Breaks execution and enters the JS debugger on a test failure
+if (params.debugOnFailure) {
+ TestRunner.debugOnFailure = true;
+}
+
+// logFile to write our results
+if (params.logFile) {
+ var mfl = new MozillaFileLogger(params.logFile);
+ TestRunner.logger.addListener("mozLogger", fileLevel + "", mfl.logCallback);
+}
+
+// A temporary hack for android 4.0 where Fennec utilizes the pandaboard so much it reboots
+if (params.runSlower) {
+ TestRunner.runSlower = true;
+}
+
+if (params.dumpOutputDirectory) {
+ TestRunner.dumpOutputDirectory = params.dumpOutputDirectory;
+}
+
+if (params.dumpAboutMemoryAfterTest) {
+ TestRunner.dumpAboutMemoryAfterTest = true;
+}
+
+if (params.dumpDMDAfterTest) {
+ TestRunner.dumpDMDAfterTest = true;
+}
+
+if (params.interactiveDebugger) {
+ TestRunner.interactiveDebugger = true;
+}
+
+if (params.jscovDirPrefix) {
+ TestRunner.jscovDirPrefix = params.jscovDirPrefix;
+}
+
+if (params.maxTimeouts) {
+ TestRunner.maxTimeouts = params.maxTimeouts;
+}
+
+if (params.cleanupCrashes) {
+ TestRunner.cleanupCrashes = true;
+}
+
+if (params.xOriginTests) {
+ TestRunner.xOriginTests = true;
+ TestRunner.setXOriginEventHandler();
+}
+
+if (params.timeoutAsPass) {
+ TestRunner.timeoutAsPass = true;
+}
+
+if (params.conditionedProfile) {
+ TestRunner.conditionedProfile = true;
+}
+
+if (params.comparePrefs) {
+ TestRunner.comparePrefs = true;
+}
+
+// Log things to the console if appropriate.
+TestRunner.logger.addListener(
+ "dumpListener",
+ consoleLevel + "",
+ function (msg) {
+ dump(msg.info.join(" ") + "\n");
+ }
+);
+
+var gTestList = [];
+var RunSet = {};
+
+RunSet.runall = function (e) {
+ // Filter tests to include|exclude tests based on data in params.filter.
+ // This allows for including or excluding tests from the gTestList
+ // TODO Only used by ipc tests, remove once those are implemented sanely
+ if (params.testManifest) {
+ getTestManifest(
+ getTestManifestURL(params.testManifest),
+ params,
+ function (filter) {
+ gTestList = filterTests(filter, gTestList, params.runOnly);
+ RunSet.runtests();
+ }
+ );
+ } else {
+ RunSet.runtests();
+ }
+};
+
+RunSet.runtests = function (e) {
+ // Which tests we're going to run
+ var my_tests = gTestList;
+
+ if (params.startAt || params.endAt) {
+ my_tests = skipTests(my_tests, params.startAt, params.endAt);
+ }
+
+ if (params.shuffle) {
+ for (var i = my_tests.length - 1; i > 0; --i) {
+ var j = Math.floor(Math.random() * i);
+ var tmp = my_tests[j];
+ my_tests[j] = my_tests[i];
+ my_tests[i] = tmp;
+ }
+ }
+ TestRunner.setParameterInfo(params);
+ TestRunner.runTests(my_tests);
+};
+
+RunSet.reloadAndRunAll = function (e) {
+ e.preventDefault();
+ //window.location.hash = "";
+ if (params.autorun) {
+ window.location.search += "";
+ // eslint-disable-next-line no-self-assign
+ window.location.href = window.location.href;
+ } else if (window.location.search) {
+ window.location.href += "&autorun=1";
+ } else {
+ window.location.href += "?autorun=1";
+ }
+};
+
+// UI Stuff
+function toggleVisible(elem) {
+ toggleElementClass("invisible", elem);
+}
+
+function makeVisible(elem) {
+ removeElementClass(elem, "invisible");
+}
+
+function makeInvisible(elem) {
+ addElementClass(elem, "invisible");
+}
+
+function isVisible(elem) {
+ // you may also want to check for
+ // getElement(elem).style.display == "none"
+ return !hasElementClass(elem, "invisible");
+}
+
+function toggleNonTests(e) {
+ e.preventDefault();
+ var elems = document.getElementsByClassName("non-test");
+ for (var i = "0"; i < elems.length; i++) {
+ toggleVisible(elems[i]);
+ }
+ if (isVisible(elems[0])) {
+ $("toggleNonTests").innerHTML = "Hide Non-Tests";
+ } else {
+ $("toggleNonTests").innerHTML = "Show Non-Tests";
+ }
+}
+
+// hook up our buttons
+function hookup() {
+ if (params.manifestFile) {
+ getTestManifest(
+ getTestManifestURL(params.manifestFile),
+ params,
+ hookupTests
+ );
+ } else {
+ hookupTests(gTestList);
+ }
+}
+
+function getPrefList() {
+ if (params.ignorePrefsFile) {
+ loadFile(getTestManifestURL(params.ignorePrefsFile), function (prefs) {
+ TestRunner.ignorePrefs = prefs;
+ RunSet.runall();
+ });
+ } else {
+ RunSet.runall();
+ }
+}
+
+function hookupTests(testList) {
+ if (testList.length) {
+ gTestList = testList;
+ } else {
+ gTestList = [];
+ for (var obj in testList) {
+ gTestList.push(testList[obj]);
+ }
+ }
+
+ document.getElementById("runtests").onclick = RunSet.reloadAndRunAll;
+ document.getElementById("toggleNonTests").onclick = toggleNonTests;
+ // run automatically if autorun specified
+ if (params.autorun) {
+ getPrefList();
+ }
+}
+
+function getTestManifestURL(path) {
+ // The test manifest url scheme should be the same protocol as the containing
+ // window... unless it's not http(s)
+ if (
+ window.location.protocol == "http:" ||
+ window.location.protocol == "https:"
+ ) {
+ return window.location.protocol + "//" + window.location.host + "/" + path;
+ }
+ return "http://mochi.test:8888/" + path;
+}
diff --git a/testing/mochitest/tests/SimpleTest/test.css b/testing/mochitest/tests/SimpleTest/test.css
new file mode 100644
index 0000000000..357070e8ae
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/test.css
@@ -0,0 +1,39 @@
+.test_ok {
+ color: #0d0;
+ display: none;
+}
+
+.test_not_ok {
+ color: red;
+ display: block;
+}
+
+.test_todo {
+ /* color: orange; */
+ display: block;
+}
+
+.test_ok, .test_not_ok, .test_todo {
+ border-bottom-width: 2px;
+ border-bottom-style: solid;
+ border-bottom-color: black;
+}
+
+.all_pass {
+ background-color: #0d0;
+}
+
+.some_fail {
+ background-color: red;
+}
+
+.todo_only {
+ background-color: orange;
+}
+
+.tests_report {
+ border-width: 2px;
+ border-style: solid;
+ width: 20em;
+ display: table;
+}