summaryrefslogtreecommitdiff
path: root/testing/mochitest/tests/SimpleTest
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/mochitest/tests/SimpleTest
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloaduxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/mochitest/tests/SimpleTest')
-rw-r--r--testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js98
-rw-r--r--testing/mochitest/tests/SimpleTest/ChromePowers.js124
-rw-r--r--testing/mochitest/tests/SimpleTest/EventUtils.js2143
-rw-r--r--testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js139
-rw-r--r--testing/mochitest/tests/SimpleTest/LICENSE_SpawnTask24
-rw-r--r--testing/mochitest/tests/SimpleTest/LogController.js96
-rw-r--r--testing/mochitest/tests/SimpleTest/MemoryStats.js122
-rw-r--r--testing/mochitest/tests/SimpleTest/MockObjects.js90
-rw-r--r--testing/mochitest/tests/SimpleTest/NativeKeyCodes.js370
-rw-r--r--testing/mochitest/tests/SimpleTest/SimpleTest.js1639
-rw-r--r--testing/mochitest/tests/SimpleTest/SpawnTask.js296
-rw-r--r--testing/mochitest/tests/SimpleTest/TestRunner.js754
-rw-r--r--testing/mochitest/tests/SimpleTest/WindowSnapshot.js92
-rw-r--r--testing/mochitest/tests/SimpleTest/iframe-between-tests.html17
-rw-r--r--testing/mochitest/tests/SimpleTest/moz.build24
-rw-r--r--testing/mochitest/tests/SimpleTest/paint_listener.js83
-rw-r--r--testing/mochitest/tests/SimpleTest/setup.js260
-rw-r--r--testing/mochitest/tests/SimpleTest/test.css43
18 files changed, 6414 insertions, 0 deletions
diff --git a/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js b/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js
new file mode 100644
index 0000000000..0f1cc06084
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js
@@ -0,0 +1,98 @@
+/*
+ * This code is used for handling synthesizeMouse in a content process.
+ * Generally it just delegates to EventUtils.js.
+ */
+
+// Set up a dummy environment so that EventUtils works. We need to be careful to
+// pass a window object into each EventUtils method we call rather than having
+// it rely on the |window| global.
+var EventUtils = {};
+EventUtils.window = {};
+EventUtils.parent = EventUtils.window;
+EventUtils._EU_Ci = Components.interfaces;
+EventUtils._EU_Cc = Components.classes;
+// EventUtils' `sendChar` function relies on the navigator to synthetize events.
+EventUtils.navigator = content.document.defaultView.navigator;
+EventUtils.KeyboardEvent = content.document.defaultView.KeyboardEvent;
+
+Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+addMessageListener("Test:SynthesizeMouse", (message) => {
+ let data = message.data;
+ let target = data.target;
+ if (typeof target == "string") {
+ target = content.document.querySelector(target);
+ }
+ else if (typeof data.targetFn == "string") {
+ let runnablestr = `
+ (() => {
+ return (${data.targetFn});
+ })();`
+ target = eval(runnablestr)();
+ }
+ else {
+ target = message.objects.object;
+ }
+
+ let left = data.x;
+ let top = data.y;
+ if (target) {
+ if (target.ownerDocument !== content.document) {
+ // Account for nodes found in iframes.
+ let cur = target;
+ do {
+ let frame = cur.ownerDocument.defaultView.frameElement;
+ let rect = frame.getBoundingClientRect();
+
+ left += rect.left;
+ top += rect.top;
+
+ cur = frame;
+ } while (cur && cur.ownerDocument !== content.document);
+
+ // node must be in this document tree.
+ if (!cur) {
+ sendAsyncMessage("Test:SynthesizeMouseDone",
+ { error: "target must be in the main document tree" });
+ return;
+ }
+ }
+
+ let rect = target.getBoundingClientRect();
+ left += rect.left;
+ top += rect.top;
+
+ if (data.event.centered) {
+ left += rect.width / 2;
+ top += rect.height / 2;
+ }
+ }
+
+ let result;
+ if (data.event && data.event.wheel) {
+ EventUtils.synthesizeWheelAtPoint(left, top, data.event, content);
+ } else {
+ result = EventUtils.synthesizeMouseAtPoint(left, top, data.event, content);
+ }
+ sendAsyncMessage("Test:SynthesizeMouseDone", { defaultPrevented: result });
+});
+
+addMessageListener("Test:SendChar", message => {
+ let result = EventUtils.sendChar(message.data.char, content);
+ sendAsyncMessage("Test:SendCharDone", { result, seq: message.data.seq });
+});
+
+addMessageListener("Test:SynthesizeKey", message => {
+ EventUtils.synthesizeKey(message.data.key, message.data.event || {}, content);
+ sendAsyncMessage("Test:SynthesizeKeyDone", { seq: message.data.seq });
+});
+
+addMessageListener("Test:SynthesizeComposition", message => {
+ let result = EventUtils.synthesizeComposition(message.data.event, content);
+ sendAsyncMessage("Test:SynthesizeCompositionDone", { result, seq: message.data.seq });
+});
+
+addMessageListener("Test:SynthesizeCompositionChange", message => {
+ EventUtils.synthesizeCompositionChange(message.data.event, content);
+ sendAsyncMessage("Test:SynthesizeCompositionChangeDone", { seq: message.data.seq });
+});
diff --git a/testing/mochitest/tests/SimpleTest/ChromePowers.js b/testing/mochitest/tests/SimpleTest/ChromePowers.js
new file mode 100644
index 0000000000..97de578157
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/ChromePowers.js
@@ -0,0 +1,124 @@
+/* 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/. */
+
+function ChromePowers(window) {
+ this.window = Components.utils.getWeakReference(window);
+
+ // In the case of browser-chrome tests, we are running as a [ChromeWindow]
+ // and we have no window.QueryInterface available, content.window is what we need
+ if (typeof(window) == "ChromeWindow" && typeof(content.window) == "Window") {
+ this.DOMWindowUtils = bindDOMWindowUtils(content.window);
+ this.window = Components.utils.getWeakReference(content.window);
+ } else {
+ this.DOMWindowUtils = bindDOMWindowUtils(window);
+ }
+
+ this.spObserver = new SpecialPowersObserverAPI();
+ this.spObserver._sendReply = this._sendReply.bind(this);
+ this.listeners = new Map();
+}
+
+ChromePowers.prototype = new SpecialPowersAPI();
+
+ChromePowers.prototype.toString = function() { return "[ChromePowers]"; };
+ChromePowers.prototype.sanityCheck = function() { return "foo"; };
+
+// This gets filled in in the constructor.
+ChromePowers.prototype.DOMWindowUtils = undefined;
+
+ChromePowers.prototype._sendReply = function(aOrigMsg, aType, aMsg) {
+ var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
+ if (!this.listeners.has(aType)) {
+ throw new Error(`No listener for ${aType}`);
+ }
+ this.listeners.get(aType)(msg);
+};
+
+ChromePowers.prototype._sendSyncMessage = function(aType, aMsg) {
+ var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
+ return [this._receiveMessage(msg)];
+};
+
+ChromePowers.prototype._sendAsyncMessage = function(aType, aMsg) {
+ var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
+ this._receiveMessage(msg);
+};
+
+ChromePowers.prototype._addMessageListener = function(aType, aCallback) {
+ if (this.listeners.has(aType)) {
+ throw new Error(`unable to handle multiple listeners for ${aType}`);
+ }
+ this.listeners.set(aType, aCallback);
+};
+ChromePowers.prototype._removeMessageListener = function(aType, aCallback) {
+ this.listeners.delete(aType);
+};
+
+ChromePowers.prototype.registerProcessCrashObservers = function() {
+ this._sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
+};
+
+ChromePowers.prototype.unregisterProcessCrashObservers = function() {
+ this._sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
+};
+
+ChromePowers.prototype._receiveMessage = function(aMessage) {
+ switch (aMessage.name) {
+ case "SpecialPowers.Quit":
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eForceQuit);
+ break;
+ case "SPProcessCrashService":
+ if (aMessage.json.op == "register-observer" || aMessage.json.op == "unregister-observer") {
+ // Hack out register/unregister specifically for browser-chrome leaks
+ break;
+ } else if (aMessage.type == "crash-observed") {
+ for (let e of msg.dumpIDs) {
+ this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
+ }
+ }
+ default:
+ // All calls go here, because we need to handle SPProcessCrashService calls as well
+ return this.spObserver._receiveMessageAPI(aMessage);
+ }
+ return undefined; // Avoid warning.
+};
+
+ChromePowers.prototype.quit = function() {
+ // We come in here as SpecialPowers.quit, but SpecialPowers is really ChromePowers.
+ // For some reason this.<func> resolves to TestRunner, so using SpecialPowers
+ // allows us to use the ChromePowers object which we defined below.
+ SpecialPowers._sendSyncMessage("SpecialPowers.Quit", {});
+};
+
+ChromePowers.prototype.focus = function(aWindow) {
+ // We come in here as SpecialPowers.focus, but SpecialPowers is really ChromePowers.
+ // For some reason this.<func> resolves to TestRunner, so using SpecialPowers
+ // allows us to use the ChromePowers object which we defined below.
+ if (aWindow)
+ aWindow.focus();
+};
+
+ChromePowers.prototype.executeAfterFlushingMessageQueue = function(aCallback) {
+ aCallback();
+};
+
+// Expose everything but internal APIs (starting with underscores) to
+// web content. We cannot use Object.keys to view SpecialPowers.prototype since
+// we are using the functions from SpecialPowersAPI.prototype
+ChromePowers.prototype.__exposedProps__ = {};
+for (var i in ChromePowers.prototype) {
+ if (i.charAt(0) != "_")
+ ChromePowers.prototype.__exposedProps__[i] = "r";
+}
+
+if ((window.parent !== null) &&
+ (window.parent !== undefined) &&
+ (window.parent.wrappedJSObject.SpecialPowers) &&
+ !(window.wrappedJSObject.SpecialPowers)) {
+ window.wrappedJSObject.SpecialPowers = window.parent.SpecialPowers;
+} else {
+ window.wrappedJSObject.SpecialPowers = new ChromePowers(window);
+}
+
diff --git a/testing/mochitest/tests/SimpleTest/EventUtils.js b/testing/mochitest/tests/SimpleTest/EventUtils.js
new file mode 100644
index 0000000000..17243625db
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/EventUtils.js
@@ -0,0 +1,2143 @@
+/**
+ * EventUtils provides some utility methods for creating and sending DOM events.
+ * Current methods:
+ * sendMouseEvent
+ * sendDragEvent
+ * sendChar
+ * sendString
+ * sendKey
+ * sendWheelAndPaint
+ * synthesizeMouse
+ * synthesizeMouseAtCenter
+ * synthesizePointer
+ * synthesizeWheel
+ * synthesizeWheelAtPoint
+ * synthesizeKey
+ * synthesizeNativeKey
+ * synthesizeMouseExpectEvent
+ * synthesizeKeyExpectEvent
+ * synthesizeNativeClick
+ *
+ * When adding methods to this file, please add a performance test for it.
+ */
+
+// 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.
+window.__defineGetter__('_EU_Ci', function() {
+ // 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.
+ var c = Object.getOwnPropertyDescriptor(window, 'Components');
+ return c.value && !c.writable ? Components.interfaces : SpecialPowers.Ci;
+});
+
+window.__defineGetter__('_EU_Cc', function() {
+ var c = Object.getOwnPropertyDescriptor(window, 'Components');
+ return c.value && !c.writable ? Components.classes : SpecialPowers.Cc;
+});
+
+window.__defineGetter__('_EU_Cu', function() {
+ var c = Object.getOwnPropertyDescriptor(window, 'Components');
+ return c.value && !c.writable ? Components.utils : SpecialPowers.Cu;
+});
+
+window.__defineGetter__("_EU_OS", function() {
+ delete this._EU_OS;
+ try {
+ this._EU_OS = this._EU_Cu.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;
+}
+
+/**
+ * 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.
+ * E.g. to send an click event to the node with id 'node' you might do this:
+ *
+ * sendMouseEvent({type:'click'}, 'node');
+ */
+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 sendMouseEvent(aEvent, aTarget, aWindow) {
+ if (['click', 'contextmenu', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout'].indexOf(aEvent.type) == -1) {
+ 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);
+ }
+
+ var event = aWindow.document.createEvent('MouseEvent');
+
+ var typeArg = aEvent.type;
+ var canBubbleArg = true;
+ var cancelableArg = true;
+ var viewArg = aWindow;
+ var detailArg = aEvent.detail || (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);
+
+ return SpecialPowers.dispatchEvent(aWindow, aTarget, event);
+}
+
+/**
+ * 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'].indexOf(aEvent.type) == -1) {
+ throw new Error("sendDragEvent doesn't know about event type '" + aEvent.type + "'");
+ }
+
+ if (typeof aTarget == "string") {
+ aTarget = aWindow.document.getElementById(aTarget);
+ }
+
+ 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);
+
+ var utils = _getDOMWindowUtils(aWindow);
+ return utils.dispatchDOMEventViaPresShell(aTarget, event, true);
+}
+
+/**
+ * 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 == 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 navigator = _getNavigator(aWindow);
+ 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
+ *
+ * If the type is specified, an mouse event of that type is fired. Otherwise,
+ * a mousedown followed by a mouse up 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();
+ synthesizeTouchAtPoint(rect.left + aOffsetX, rect.top + aOffsetY,
+ aEvent, aWindow);
+}
+function synthesizePointer(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
+{
+ var rect = aTarget.getBoundingClientRect();
+ return synthesizePointerAtPoint(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
+ *
+ * If the type is specified, an mouse event of that type is fired. Otherwise,
+ * a mousedown followed by a mouse up 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;
+ var inputSource = ("inputSource" in aEvent) ? aEvent.inputSource : 0;
+ var isDOMEventSynthesized =
+ ("isSynthesized" in aEvent) ? aEvent.isSynthesized : true;
+ var isWidgetEventSynthesized =
+ ("isWidgetEventSynthesized" in aEvent) ? aEvent.isWidgetEventSynthesized : false;
+ var buttons = ("buttons" in aEvent) ? aEvent.buttons :
+ utils.MOUSE_BUTTONS_NOT_SPECIFIED;
+ if (("type" in aEvent) && aEvent.type) {
+ defaultPrevented = utils.sendMouseEvent(aEvent.type, left, top, button,
+ clickCount, modifiers, false,
+ pressure, inputSource,
+ isDOMEventSynthesized,
+ isWidgetEventSynthesized,
+ buttons);
+ }
+ else {
+ utils.sendMouseEvent("mousedown", left, top, button, clickCount, modifiers,
+ false, pressure, inputSource, isDOMEventSynthesized,
+ isWidgetEventSynthesized, buttons);
+ utils.sendMouseEvent("mouseup", left, top, button, clickCount, modifiers,
+ false, pressure, inputSource, isDOMEventSynthesized,
+ isWidgetEventSynthesized, buttons);
+ }
+ }
+
+ return defaultPrevented;
+}
+
+function synthesizeTouchAtPoint(left, top, aEvent, aWindow = window)
+{
+ var utils = _getDOMWindowUtils(aWindow);
+
+ if (utils) {
+ var id = aEvent.id || 0;
+ var rx = aEvent.rx || 1;
+ var ry = aEvent.rx || 1;
+ var angle = aEvent.angle || 0;
+ var force = aEvent.force || 1;
+ var modifiers = _parseModifiers(aEvent, aWindow);
+
+ if (("type" in aEvent) && aEvent.type) {
+ utils.sendTouchEvent(aEvent.type, [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers);
+ }
+ else {
+ utils.sendTouchEvent("touchstart", [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers);
+ utils.sendTouchEvent("touchend", [id], [left], [top], [rx], [ry], [angle], [force], 1, modifiers);
+ }
+ }
+}
+
+function synthesizePointerAtPoint(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;
+ var inputSource = ("inputSource" in aEvent) ? aEvent.inputSource : 0;
+ var synthesized = ("isSynthesized" in aEvent) ? aEvent.isSynthesized : true;
+ var isPrimary = ("isPrimary" in aEvent) ? aEvent.isPrimary : false;
+
+ if (("type" in aEvent) && aEvent.type) {
+ defaultPrevented = utils.sendPointerEventToWindow(aEvent.type, left, top, button,
+ clickCount, modifiers, false,
+ pressure, inputSource,
+ synthesized, 0, 0, 0, 0, isPrimary);
+ }
+ else {
+ utils.sendPointerEventToWindow("pointerdown", left, top, button, clickCount, modifiers, false, pressure, inputSource);
+ utils.sendPointerEventToWindow("pointerup", left, top, button, clickCount, modifiers, false, pressure, inputSource);
+ }
+ }
+
+ 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();
+ synthesizeTouch(aTarget, rect.width / 2, 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;
+ }
+ }
+ var isNoLineOrPageDelta = aEvent.isNoLineOrPageDelta;
+
+ // 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 =
+ aEvent.lineOrPageDeltaX != null ? aEvent.lineOrPageDeltaX :
+ aEvent.deltaX > 0 ? Math.floor(aEvent.deltaX) :
+ Math.ceil(aEvent.deltaX);
+ var lineOrPageDeltaY =
+ 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);
+}
+
+/**
+ * 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) {
+ 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, 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", false);
+ aWindow.waitForAllPaintsFlushed(function() {
+ utils.restoreNormalRefresh();
+ aCallback();
+ });
+ }
+
+ SpecialPowers.Services.obs.addObserver(waitForPaints, "apz-repaints-flushed", false);
+ 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);
+ synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
+}
+
+function synthesizeNativeMouseMove(aTarget, aOffsetX, aOffsetY, aCallback, aWindow = window) {
+ var utils = _getDOMWindowUtils(aWindow);
+ if (!utils)
+ return;
+
+ var rect = aTarget.getBoundingClientRect();
+ var x = aOffsetX + window.mozInnerScreenX + rect.left;
+ var y = aOffsetY + window.mozInnerScreenY + rect.top;
+ var scale = utils.screenPixelsPerCSSPixel;
+
+ var observer = {
+ observe: (subject, topic, data) => {
+ if (aCallback && topic == "mouseevent") {
+ aCallback(data);
+ }
+ }
+ };
+ utils.sendNativeMouseMove(x * scale, y * scale, null, observer);
+}
+
+function _computeKeyCodeFromChar(aChar)
+{
+ if (aChar.length != 1) {
+ return 0;
+ }
+ var KeyEvent = _EU_Ci.nsIDOMKeyEvent;
+ if (aChar >= 'a' && aChar <= 'z') {
+ return KeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'a'.charCodeAt(0);
+ }
+ if (aChar >= 'A' && aChar <= 'Z') {
+ return KeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'A'.charCodeAt(0);
+ }
+ if (aChar >= '0' && aChar <= '9') {
+ return KeyEvent.DOM_VK_0 + aChar.charCodeAt(0) - '0'.charCodeAt(0);
+ }
+ // returns US keyboard layout's keycode
+ switch (aChar) {
+ case '~':
+ case '`':
+ return KeyEvent.DOM_VK_BACK_QUOTE;
+ case '!':
+ return KeyEvent.DOM_VK_1;
+ case '@':
+ return KeyEvent.DOM_VK_2;
+ case '#':
+ return KeyEvent.DOM_VK_3;
+ case '$':
+ return KeyEvent.DOM_VK_4;
+ case '%':
+ return KeyEvent.DOM_VK_5;
+ case '^':
+ return KeyEvent.DOM_VK_6;
+ case '&':
+ return KeyEvent.DOM_VK_7;
+ case '*':
+ return KeyEvent.DOM_VK_8;
+ case '(':
+ return KeyEvent.DOM_VK_9;
+ case ')':
+ return KeyEvent.DOM_VK_0;
+ case '-':
+ case '_':
+ return KeyEvent.DOM_VK_SUBTRACT;
+ case '+':
+ case '=':
+ return KeyEvent.DOM_VK_EQUALS;
+ case '{':
+ case '[':
+ return KeyEvent.DOM_VK_OPEN_BRACKET;
+ case '}':
+ case ']':
+ return KeyEvent.DOM_VK_CLOSE_BRACKET;
+ case '|':
+ case '\\':
+ return KeyEvent.DOM_VK_BACK_SLASH;
+ case ':':
+ case ';':
+ return KeyEvent.DOM_VK_SEMICOLON;
+ case '\'':
+ case '"':
+ return KeyEvent.DOM_VK_QUOTE;
+ case '<':
+ case ',':
+ return KeyEvent.DOM_VK_COMMA;
+ case '>':
+ case '.':
+ return KeyEvent.DOM_VK_PERIOD;
+ case '?':
+ case '/':
+ return KeyEvent.DOM_VK_SLASH;
+ case '\n':
+ return KeyEvent.DOM_VK_RETURN;
+ case ' ':
+ return KeyEvent.DOM_VK_SPACE;
+ default:
+ return 0;
+ }
+}
+
+/**
+ * Synthesize a key event. It is targeted at whatever would be targeted by an
+ * actual keypress by the user, typically the focused element.
+ *
+ * aKey should be:
+ * - key value (recommended). If you specify a non-printable key name,
+ * append "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.
+ *
+ * aEvent is an object which may contain the properties:
+ * - code: If you emulates a physical keyboard's key event, this should be
+ * specified.
+ * - repeat: If you emulates auto-repeat, you should set the count of repeat.
+ * This method will automatically synthesize keydown (and keypress).
+ * - 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.
+ * - 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.
+ * - 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).
+ * - keyCode: Must be 0 - 255 (0xFF). If this is specified explicitly,
+ * .keyCode value is initialized with this value.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeKey(aKey, aEvent, aWindow = window)
+{
+ var TIP = _getTIP(aWindow);
+ if (!TIP) {
+ return;
+ }
+ var KeyboardEvent = _getKeyboardEvent(aWindow);
+ var modifiers = _emulateToActivateModifiers(TIP, aEvent, aWindow);
+ var keyEventDict = _createKeyboardEventDictionary(aKey, aEvent, aWindow);
+ var keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
+ var dispatchKeydown =
+ !("type" in aEvent) || aEvent.type === "keydown" || !aEvent.type;
+ var dispatchKeyup =
+ !("type" in aEvent) || aEvent.type === "keyup" || !aEvent.type;
+
+ try {
+ if (dispatchKeydown) {
+ TIP.keydown(keyEvent, keyEventDict.flags);
+ if ("repeat" in aEvent && aEvent.repeat > 1) {
+ keyEventDict.dictionary.repeat = true;
+ var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary);
+ for (var i = 1; i < aEvent.repeat; i++) {
+ TIP.keydown(repeatedKeyEvent, keyEventDict.flags);
+ }
+ }
+ }
+ if (dispatchKeyup) {
+ TIP.keyup(keyEvent, keyEventDict.flags);
+ }
+ } finally {
+ _emulateToInactivateModifiers(TIP, modifiers, aWindow);
+ }
+}
+
+function _parseNativeModifiers(aModifiers, aWindow = window)
+{
+ var navigator = _getNavigator(aWindow);
+ var modifiers;
+ if (aModifiers.capsLockKey) {
+ modifiers |= 0x00000001;
+ }
+ if (aModifiers.numLockKey) {
+ modifiers |= 0x00000002;
+ }
+ if (aModifiers.shiftKey) {
+ modifiers |= 0x00000100;
+ }
+ if (aModifiers.shiftRightKey) {
+ modifiers |= 0x00000200;
+ }
+ if (aModifiers.ctrlKey) {
+ modifiers |= 0x00000400;
+ }
+ if (aModifiers.ctrlRightKey) {
+ modifiers |= 0x00000800;
+ }
+ if (aModifiers.altKey) {
+ modifiers |= 0x00001000;
+ }
+ if (aModifiers.altRightKey) {
+ modifiers |= 0x00002000;
+ }
+ if (aModifiers.metaKey) {
+ modifiers |= 0x00004000;
+ }
+ if (aModifiers.metaRightKey) {
+ modifiers |= 0x00008000;
+ }
+ if (aModifiers.helpKey) {
+ modifiers |= 0x00010000;
+ }
+ if (aModifiers.fnKey) {
+ modifiers |= 0x00100000;
+ }
+ if (aModifiers.numericKeyPadKey) {
+ modifiers |= 0x01000000;
+ }
+
+ if (aModifiers.accelKey) {
+ modifiers |= _EU_isMac(aWindow) ? 0x00004000 : 0x00000400;
+ }
+ if (aModifiers.accelRightKey) {
+ modifiers |= _EU_isMac(aWindow) ? 0x00008000 : 0x00000800;
+ }
+ if (aModifiers.altGrKey) {
+ modifiers |= _EU_isWin(aWindow) ? 0x00002800 : 0x00001000;
+ }
+ 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 };
+const KEYBOARD_LAYOUT_ARABIC_PC =
+ { name: "Arabic - PC", Mac: 7, Win: null };
+const KEYBOARD_LAYOUT_BRAZILIAN_ABNT =
+ { name: "Brazilian ABNT", Mac: null, Win: 0x00000416 };
+const KEYBOARD_LAYOUT_DVORAK_QWERTY =
+ { name: "Dvorak-QWERTY", Mac: 4, Win: null };
+const KEYBOARD_LAYOUT_EN_US =
+ { name: "US", Mac: 0, Win: 0x00000409 };
+const KEYBOARD_LAYOUT_FRENCH =
+ { name: "French", Mac: 8, Win: 0x0000040C };
+const KEYBOARD_LAYOUT_GREEK =
+ { name: "Greek", Mac: 1, Win: 0x00000408 };
+const KEYBOARD_LAYOUT_GERMAN =
+ { name: "German", Mac: 2, Win: 0x00000407 };
+const KEYBOARD_LAYOUT_HEBREW =
+ { name: "Hebrew", Mac: 9, Win: 0x0000040D };
+const KEYBOARD_LAYOUT_JAPANESE =
+ { name: "Japanese", Mac: null, Win: 0x00000411 };
+const KEYBOARD_LAYOUT_KHMER =
+ { name: "Khmer", Mac: null, Win: 0x00000453 }; // available on Win7 or later.
+const KEYBOARD_LAYOUT_LITHUANIAN =
+ { name: "Lithuanian", Mac: 10, Win: 0x00010427 };
+const KEYBOARD_LAYOUT_NORWEGIAN =
+ { name: "Norwegian", Mac: 11, Win: 0x00000414 };
+const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC =
+ { name: "Russian - Mnemonic", Mac: null, Win: 0x00020419 }; // available on Win8 or later.
+const KEYBOARD_LAYOUT_SPANISH =
+ { name: "Spanish", Mac: 12, Win: 0x0000040A };
+const KEYBOARD_LAYOUT_SWEDISH =
+ { name: "Swedish", Mac: 3, Win: 0x0000041D };
+const KEYBOARD_LAYOUT_THAI =
+ { name: "Thai", Mac: 5, Win: 0x0002041E };
+
+/**
+ * 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 navigator = _getNavigator(aWindow);
+ 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: function(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, false);
+ 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, false);
+ 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;
+ }
+
+ // 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
+ .QueryInterface(_EU_Ci.nsIInterfaceRequestor)
+ .getInterface(_EU_Ci.nsIDOMWindowUtils);
+}
+
+function _defineConstant(name, value) {
+ Object.defineProperty(this, name, {
+ value: 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;
+}
+
+function _getNavigator(aWindow = window)
+{
+ if (typeof navigator != "undefined") {
+ return navigator;
+ }
+ return aWindow.navigator;
+}
+
+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_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, 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";
+ if (aKey.indexOf("KEY_") == 0) {
+ keyName = aKey.substr("KEY_".length);
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
+ } else if (aKey.indexOf("VK_") == 0) {
+ keyCode = _EU_Ci.nsIDOMKeyEvent["DOM_" + aKey];
+ if (!keyCode) {
+ throw "Unknown key: " + aKey;
+ }
+ keyName = _guessKeyNameFromKeyCode(keyCode, aWindow);
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
+ } else if (aKey != "") {
+ keyName = aKey;
+ if (!keyCodeIsDefined) {
+ keyCode = _computeKeyCodeFromChar(aKey.charAt(0));
+ }
+ if (!keyCode) {
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
+ }
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
+ }
+ var locationIsDefined = "location" in aKeyEvent;
+ if (locationIsDefined && aKeyEvent.location === 0) {
+ result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD;
+ }
+ result.dictionary = {
+ key: keyName,
+ code: "code" in aKeyEvent ? aKeyEvent.code : "",
+ location: locationIsDefined ? aKeyEvent.location : 0,
+ repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false,
+ keyCode: keyCode,
+ };
+ return result;
+}
+
+function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window)
+{
+ if (!aKeyEvent) {
+ return null;
+ }
+ var KeyboardEvent = _getKeyboardEvent(aWindow);
+ var navigator = _getNavigator(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 (var i = 0; i < modifiers.normal.length; i++) {
+ if (!aKeyEvent[modifiers.normal[i].attr]) {
+ continue;
+ }
+ if (aTIP.getModifierState(modifiers.normal[i].key)) {
+ continue; // already activated.
+ }
+ var 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 (var i = 0; i < modifiers.lockable.length; i++) {
+ if (!aKeyEvent[modifiers.lockable[i].attr]) {
+ continue;
+ }
+ if (aTIP.getModifierState(modifiers.lockable[i].key)) {
+ continue; // already activated.
+ }
+ var 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 (var i = 0; i < aModifiers.normal.length; i++) {
+ if (!aModifiers.normal[i].activated) {
+ continue;
+ }
+ var event = new KeyboardEvent("", { key: aModifiers.normal[i].key });
+ aTIP.keyup(event,
+ aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
+ }
+ for (var 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?
+ }
+ var 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.
+ *
+ * @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 specified, the key event may be
+ * dispatched. This can emulates changing
+ * composition state caused by key operation.
+ * Its key value should start with "KEY_" if the
+ * value is non-printable key name defined in D3E.
+ * @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 false;
+ }
+ var KeyboardEvent = _getKeyboardEvent(aWindow);
+ var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
+ var ret = false;
+ var keyEventDict =
+ "key" in aEvent ?
+ _createKeyboardEventDictionary(aEvent.key.key, aEvent.key, aWindow) :
+ { dictionary: null, flags: 0 };
+ var keyEvent =
+ "key" in aEvent ?
+ new KeyboardEvent(aEvent.type === "keydown" ? "keydown" : "",
+ keyEventDict.dictionary) :
+ null;
+ try {
+ switch (aEvent.type) {
+ case "compositionstart":
+ ret = TIP.startComposition(keyEvent, keyEventDict.flags);
+ break;
+ case "compositioncommitasis":
+ ret = TIP.commitComposition(keyEvent, keyEventDict.flags);
+ break;
+ case "compositioncommit":
+ ret = TIP.commitCompositionWith(aEvent.data, keyEvent,
+ keyEventDict.flags);
+ break;
+ }
+ } finally {
+ _emulateToInactivateModifiers(TIP, modifiers, aWindow);
+ }
+}
+/**
+ * Synthesize a compositionchange event which causes a DOM text event and
+ * compositionupdate event if it's necessary.
+ *
+ * @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 specified, the key event may be dispatched.
+ * This can emulates changing composition state caused by key
+ * operation. Its key value should start with "KEY_" if the
+ * value is non-printable key name defined in D3E.
+ *
+ * @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");
+ break;
+ }
+ }
+ }
+
+ if (aEvent.caret) {
+ TIP.setCaretInPendingComposition(aEvent.caret.start);
+ }
+
+ var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
+ try {
+ var keyEventDict =
+ "key" in aEvent ?
+ _createKeyboardEventDictionary(aEvent.key.key, aEvent.key, aWindow) :
+ { dictionary: null, flags: 0 };
+ var keyEvent =
+ "key" in aEvent ?
+ new KeyboardEvent(aEvent.type === "keydown" ? "keydown" : "",
+ keyEventDict.dictionary) :
+ null;
+ 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 nullptr;
+ }
+ 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);
+ if (!utils) {
+ return null;
+ }
+
+ 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.
+ */
+function synthesizeSelectionSet(aOffset, aLength, aReverse, aWindow)
+{
+ var utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return false;
+ }
+ var flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0;
+ return utils.sendSelectionSetEvent(aOffset, aLength, flags);
+}
+
+/*
+ * Synthesize a native mouse click event at a particular point in screen.
+ * This function should be used only for testing native event loop.
+ * Use synthesizeMouse instead for most case.
+ *
+ * This works only on OS X. Throws an error on other OS. Also throws an error
+ * when the library or any of function are not found, or something goes wrong
+ * in native functions.
+ */
+function synthesizeNativeOSXClick(x, y)
+{
+ var { ctypes } = _EU_Cu.import("resource://gre/modules/ctypes.jsm", {});
+
+ // Library
+ var CoreFoundation = ctypes.open("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation");
+ var CoreGraphics = ctypes.open("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics");
+
+ // Contants
+ var kCGEventLeftMouseDown = 1;
+ var kCGEventLeftMouseUp = 2;
+ var kCGEventSourceStateHIDSystemState = 1;
+ var kCGHIDEventTap = 0;
+ var kCGMouseButtonLeft = 0;
+ var kCGMouseEventClickState = 1;
+
+ // Types
+ var CGEventField = ctypes.uint32_t;
+ var CGEventRef = ctypes.voidptr_t;
+ var CGEventSourceRef = ctypes.voidptr_t;
+ var CGEventSourceStateID = ctypes.uint32_t;
+ var CGEventTapLocation = ctypes.uint32_t;
+ var CGEventType = ctypes.uint32_t;
+ var CGFloat = ctypes.voidptr_t.size == 4 ? ctypes.float : ctypes.double;
+ var CGMouseButton = ctypes.uint32_t;
+
+ var CGPoint = new ctypes.StructType(
+ "CGPoint",
+ [ { "x" : CGFloat },
+ { "y" : CGFloat } ]);
+
+ // Functions
+ var CGEventSourceCreate = CoreGraphics.declare(
+ "CGEventSourceCreate",
+ ctypes.default_abi,
+ CGEventSourceRef, CGEventSourceStateID);
+ var CGEventCreateMouseEvent = CoreGraphics.declare(
+ "CGEventCreateMouseEvent",
+ ctypes.default_abi,
+ CGEventRef,
+ CGEventSourceRef, CGEventType, CGPoint, CGMouseButton);
+ var CGEventSetIntegerValueField = CoreGraphics.declare(
+ "CGEventSetIntegerValueField",
+ ctypes.default_abi,
+ ctypes.void_t,
+ CGEventRef, CGEventField, ctypes.int64_t);
+ var CGEventPost = CoreGraphics.declare(
+ "CGEventPost",
+ ctypes.default_abi,
+ ctypes.void_t,
+ CGEventTapLocation, CGEventRef);
+ var CFRelease = CoreFoundation.declare(
+ "CFRelease",
+ ctypes.default_abi,
+ ctypes.void_t,
+ CGEventRef);
+
+ var source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
+ if (!source) {
+ throw new Error("CGEventSourceCreate returns null");
+ }
+
+ var loc = new CGPoint({ x: x, y: y });
+ var event = CGEventCreateMouseEvent(source, kCGEventLeftMouseDown, loc,
+ kCGMouseButtonLeft);
+ if (!event) {
+ throw new Error("CGEventCreateMouseEvent returns null");
+ }
+ CGEventSetIntegerValueField(event, kCGMouseEventClickState,
+ new ctypes.Int64(1));
+ CGEventPost(kCGHIDEventTap, event);
+ CFRelease(event);
+
+ event = CGEventCreateMouseEvent(source, kCGEventLeftMouseUp, loc,
+ kCGMouseButtonLeft);
+ if (!event) {
+ throw new Error("CGEventCreateMouseEvent returns null");
+ }
+ CGEventSetIntegerValueField(event, kCGMouseEventClickState,
+ new ctypes.Int64(1));
+ CGEventPost(kCGHIDEventTap, event);
+ CFRelease(event);
+
+ CFRelease(source);
+
+ CoreFoundation.close();
+ CoreGraphics.close();
+}
+
+/**
+ * Emulate a dragstart event.
+ * element - element to fire the dragstart event on
+ * expectedDragData - the data you expect the data transfer to contain afterwards
+ * This data is in the format:
+ * [ [ {type: value, data: value, test: function}, ... ], ... ]
+ * can be null
+ * aWindow - optional; defaults to the current window object.
+ * x - optional; initial x coordinate
+ * y - optional; initial y coordinate
+ * Returns null if data matches.
+ * Returns the event.dataTransfer if data does not match
+ *
+ * eqTest is an optional function if comparison can't be done with x == y;
+ * function (actualData, expectedData) {return boolean}
+ * @param actualData from dataTransfer
+ * @param expectedData from expectedDragData
+ * see bug 462172 for example of use
+ *
+ */
+function synthesizeDragStart(element, expectedDragData, aWindow, x, y)
+{
+ if (!aWindow)
+ aWindow = window;
+ x = x || 2;
+ y = y || 2;
+ const step = 9;
+
+ var result = "trapDrag was not called";
+ var trapDrag = function(event) {
+ try {
+ // We must wrap only in plain mochitests, not chrome
+ var c = Object.getOwnPropertyDescriptor(window, 'Components');
+ var dataTransfer = c.value && !c.writable
+ ? event.dataTransfer : SpecialPowers.wrap(event.dataTransfer);
+ result = null;
+ if (!dataTransfer)
+ throw "no dataTransfer";
+ if (expectedDragData == null ||
+ dataTransfer.mozItemCount != expectedDragData.length)
+ throw dataTransfer;
+ for (var i = 0; i < dataTransfer.mozItemCount; i++) {
+ var dtTypes = dataTransfer.mozTypesAt(i);
+ if (dtTypes.length != expectedDragData[i].length)
+ throw dataTransfer;
+ for (var j = 0; j < dtTypes.length; j++) {
+ if (dtTypes[j] != expectedDragData[i][j].type)
+ throw dataTransfer;
+ var dtData = dataTransfer.mozGetDataAt(dtTypes[j],i);
+ if (expectedDragData[i][j].eqTest) {
+ if (!expectedDragData[i][j].eqTest(dtData, expectedDragData[i][j].data))
+ throw dataTransfer;
+ }
+ else if (expectedDragData[i][j].data != dtData)
+ throw dataTransfer;
+ }
+ }
+ } catch(ex) {
+ result = ex;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ aWindow.addEventListener("dragstart", trapDrag, false);
+ synthesizeMouse(element, x, y, { type: "mousedown" }, aWindow);
+ x += step; y += step;
+ synthesizeMouse(element, x, y, { type: "mousemove" }, aWindow);
+ x += step; y += step;
+ synthesizeMouse(element, x, y, { type: "mousemove" }, aWindow);
+ aWindow.removeEventListener("dragstart", trapDrag, false);
+ synthesizeMouse(element, x, y, { type: "mouseup" }, aWindow);
+ return result;
+}
+
+/**
+ * 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 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, aWindow)
+{
+ var utils = _getDOMWindowUtils(aWindow);
+ if (!utils) {
+ return nullptr;
+ }
+ return utils.sendQueryContentEvent(utils.QUERY_TEXT_RECT,
+ aOffset, aLength, 0, 0,
+ QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK);
+}
+
+/**
+ * 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);
+ if (!utils) {
+ return nullptr;
+ }
+ 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);
+ if (!utils) {
+ return nullptr;
+ }
+ 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);
+ if (!utils) {
+ return nullptr;
+ }
+ 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;
+ }
+ return Object.assign({ type: aType,
+ screenX: destScreenX, screenY: destScreenY,
+ clientX: destClientX, clientY: destClientY,
+ dataTransfer: aDataTransfer }, aDragEvent);
+}
+
+/**
+ * Emulate a event sequence of dragstart, dragenter, and dragover.
+ *
+ * @param aSrcElement The element to use to start the drag.
+ * @param aDestElement The element to fire the dragover, dragenter events
+ * @param 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 aDropEffect The drop effect to set during the dragstart event, or
+ * 'move' if null.
+ * @param aWindow Optional; Defaults to the current window object.
+ * @param aDestWindow Optional; Defaults to aWindow.
+ * Used when aDestElement is in a different window than
+ * aSrcElement.
+ * @param aDragEvent Optional; Defaults to empty object. Overwrites an object
+ * passed to sendDragEvent.
+ * @return 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;
+ }
+
+ var dataTransfer;
+ var trapDrag = function(event) {
+ dataTransfer = event.dataTransfer;
+ if (aDragData) {
+ for (var i = 0; i < aDragData.length; i++) {
+ var item = aDragData[i];
+ for (var j = 0; j < item.length; j++) {
+ dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
+ }
+ }
+ }
+ dataTransfer.dropEffect = aDropEffect || "move";
+ event.preventDefault();
+ };
+
+ // need to use real mouse action
+ aWindow.addEventListener("dragstart", trapDrag, true);
+ 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", trapDrag, true);
+
+ var event = createDragEventObject("dragenter", aDestElement, aDestWindow,
+ dataTransfer, aDragEvent);
+ sendDragEvent(event, aDestElement, aDestWindow);
+
+ 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 aDataTransfer The second element of the array returned from
+ * synthesizeDragOver.
+ * @param aDestElement The element to fire the drop event.
+ * @param aDestWindow Optional; Defaults to the current window object.
+ * @param aDragEvent Optional; Defaults to empty object. Overwrites an
+ * object passed to sendDragEvent.
+ * @return "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);
+ }
+
+ synthesizeMouseAtCenter(aDestElement, { type: "mouseup" }, aDestWindow);
+
+ return effect;
+}
+
+/**
+ * Emulate a drag and drop by emulating a dragstart and firing events dragenter,
+ * dragover, and drop.
+ *
+ * @param aSrcElement The element to use to start the drag.
+ * @param aDestElement The element to fire the dragover, dragenter events
+ * @param 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 aDropEffect The drop effect to set during the dragstart event, or
+ * 'move' if null.
+ * @param aWindow Optional; Defaults to the current window object.
+ * @param aDestWindow Optional; Defaults to aWindow.
+ * Used when aDestElement is in a different window than
+ * aSrcElement.
+ * @param aDragEvent Optional; Defaults to empty object. Overwrites an object
+ * passed to sendDragEvent.
+ * @return 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);
+
+ ds.startDragSession();
+
+ try {
+ var [result, dataTransfer] = synthesizeDragOver(aSrcElement, aDestElement,
+ aDragData, aDropEffect,
+ aWindow, aDestWindow,
+ aDragEvent);
+ return synthesizeDropAfterDragOver(result, dataTransfer, aDestElement,
+ aDestWindow, aDragEvent);
+ } finally {
+ ds.endDragSession(true);
+ }
+}
+
+var PluginUtils =
+{
+ withTestPlugin : function(callback)
+ {
+ var ph = _EU_Cc["@mozilla.org/plugin/host;1"]
+ .getService(_EU_Ci.nsIPluginHost);
+ var tags = ph.getPluginTags();
+
+ // Find the test plugin
+ for (var i = 0; i < tags.length; i++) {
+ if (tags[i].name == "Test Plug-in") {
+ callback(tags[i]);
+ return true;
+ }
+ }
+ todo(false, "Need a test plugin on this platform");
+ return false;
+ }
+};
diff --git a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
new file mode 100644
index 0000000000..921d1a83f5
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
@@ -0,0 +1,139 @@
+var ExtensionTestUtils = {};
+
+ExtensionTestUtils.loadExtension = function(ext)
+{
+ // Cleanup functions need to be registered differently depending on
+ // whether we're in browser chrome or plain mochitests.
+ var registerCleanup;
+ 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, stack] = args;
+ SimpleTest.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`, undefined, stack);
+ } else if (kind == "test-log") {
+ SimpleTest.info(msg);
+ } else if (kind == "test-result") {
+ SimpleTest.ok(pass, msg, undefined, args[0]);
+ }
+ }
+
+ var handler = {
+ testResult(kind, pass, msg, ...args) {
+ if (kind == "test-done") {
+ SimpleTest.ok(pass, msg, undefined, args[0]);
+ return testResolve(msg);
+ }
+ testHandler(kind, pass, msg, ...args);
+ },
+
+ testMessage(msg, ...args) {
+ var handler = messageHandler.get(msg);
+ if (handler) {
+ handler(...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") {
+ ext.files[filename] = `(${file})();`
+ }
+ }
+ }
+ if (typeof ext.background == "function") {
+ ext.background = `(${ext.background})();`
+ }
+
+ var extension = SpecialPowers.loadExtension(ext, handler);
+
+ registerCleanup(() => {
+ if (extension.state == "pending" || extension.state == "running") {
+ SimpleTest.ok(false, "Extension left running at test shutdown")
+ return 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;
+}
diff --git a/testing/mochitest/tests/SimpleTest/LICENSE_SpawnTask b/testing/mochitest/tests/SimpleTest/LICENSE_SpawnTask
new file mode 100644
index 0000000000..088c54c9d3
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/LICENSE_SpawnTask
@@ -0,0 +1,24 @@
+LICENSE for SpawnTask.js (the co library):
+
+(The MIT License)
+
+Copyright (c) 2014 TJ Holowaychuk &lt;tj@vision-media.ca&gt;
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/testing/mochitest/tests/SimpleTest/LogController.js b/testing/mochitest/tests/SimpleTest/LogController.js
new file mode 100644
index 0000000000..52fe9eea82
--- /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..2af971184c
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/MemoryStats.js
@@ -0,0 +1,122 @@
+/* -*- 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 {
+ var value = 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 > 0) {
+ info("MEMORY STAT" + statMessage);
+ }
+
+ if (dumpAboutMemory) {
+ var basename = "about-memory-" + testNumber + ".json.gz";
+ var dumpfile = MemoryStats.constructPathname(dumpOutputDirectory,
+ basename);
+ info(testURL + " | MEMDUMP-START " + dumpfile);
+ var md = MemoryStats._getService("@mozilla.org/memory-info-dumper;1",
+ "nsIMemoryInfoDumper");
+ md.dumpMemoryReportsToNamedFile(dumpfile, function () {
+ info("TEST-INFO | " + testURL + " | MEMDUMP-END");
+ }, null, /* anonymize = */ false);
+ }
+
+ // This is the old, deprecated function.
+ if (dumpDMD && typeof(DMDReportAndDump) != undefined) {
+ var basename = "dmd-" + testNumber + "-deprecated.txt";
+ var dumpfile = MemoryStats.constructPathname(dumpOutputDirectory,
+ basename);
+ info(testURL + " | DMD-DUMP-deprecated " + dumpfile);
+ DMDReportAndDump(dumpfile);
+ }
+
+ if (dumpDMD && typeof(DMDAnalyzeReports) != undefined) {
+ var basename = "dmd-" + testNumber + ".txt";
+ var dumpfile = MemoryStats.constructPathname(dumpOutputDirectory,
+ basename);
+ info(testURL + " | DMD-DUMP " + dumpfile);
+ DMDAnalyzeReports(dumpfile);
+ }
+};
diff --git a/testing/mochitest/tests/SimpleTest/MockObjects.js b/testing/mochitest/tests/SimpleTest/MockObjects.js
new file mode 100644
index 0000000000..d00f5127b3
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/MockObjects.js
@@ -0,0 +1,90 @@
+/* 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._originalFactory)
+ throw new Exception("Invalid object state when calling register()");
+
+ // Define a factory that creates a new object using the given constructor.
+ var providedConstructor = this._replacementCtor;
+ this._mockFactory = {
+ createInstance: function MF_createInstance(aOuter, aIid) {
+ if (aOuter != null)
+ throw SpecialPowers.Cr.NS_ERROR_NO_AGGREGATION;
+ return new providedConstructor().QueryInterface(aIid);
+ }
+ };
+
+ var retVal = SpecialPowers.swapFactoryRegistration(this._cid, this._contractID, this._mockFactory, this._originalFactory);
+ if ('error' in retVal) {
+ throw new Exception("ERROR: " + retVal.error);
+ } else {
+ this._cid = retVal.cid;
+ this._originalFactory = retVal.originalFactory;
+ }
+ },
+
+ /**
+ * Restores the original factory.
+ */
+ unregister: function MOR_unregister() {
+ if (!this._originalFactory)
+ throw new Exception("Invalid object state when calling unregister()");
+
+ // Free references to the mock factory.
+ SpecialPowers.swapFactoryRegistration(this._cid, this._contractID, this._mockFactory, this._originalFactory);
+
+ // Allow registering a mock factory again later.
+ this._cid = null;
+ this._originalFactory = null;
+ this._mockFactory = null;
+ },
+
+ // --- Private methods and properties ---
+
+ /**
+ * The factory of the component being replaced.
+ */
+ _originalFactory: null,
+
+ /**
+ * The CID under which the mock contractID was registered.
+ */
+ _cid: null,
+
+ /**
+ * The nsIFactory that was automatically generated by this object.
+ */
+ _mockFactory: null
+}
diff --git a/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js
new file mode 100644
index 0000000000..8130f3e18d
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/NativeKeyCodes.js
@@ -0,0 +1,370 @@
+/**
+ * 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..37713737c8
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js
@@ -0,0 +1,1639 @@
+/* -*- js-indent-level: 4; tab-width: 4; indent-tabs-mode: nil -*- */
+/* vim:set ts=4 sw=4 sts=4 et: */
+/**
+ * SimpleTest, a partial Test.Simple/Test.More API compatible test library.
+ *
+ * Why?
+ *
+ * Test.Simple doesn't work on IE < 6.
+ * TODO:
+ * * Support the Test.Simple API used by MochiKit, to be able to test MochiKit
+ * itself against IE 5.5
+ *
+ * NOTE: Pay attention to cross-browser compatibility in this file. For
+ * instance, do not use const or JS > 1.5 features which are not yet
+ * implemented everywhere.
+ *
+**/
+
+var SimpleTest = { };
+var parentRunner = null;
+
+// 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)
+try {
+ var isPrimaryTestWindow = !!parent.TestRunner || isSingleTestRun;
+} catch(e) {
+ dump("TEST-UNEXPECTED-FAIL, Exception caught: " + e.message +
+ ", at: " + e.fileName + " (" + e.lineNumber +
+ "), location: " + window.location.href + "\n");
+}
+// 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.xul and we need SpecialPowers in the iframe, and also for tests
+// like test_focus.xul 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;
+ }
+
+ var w = ancestor(window);
+ while (w && (!parentRunner || !window.SpecialPowers)) {
+ if (!parentRunner) {
+ parentRunner = w.TestRunner;
+ if (!parentRunner && w.wrappedJSObject) {
+ parentRunner = w.wrappedJSObject.TestRunner;
+ }
+ }
+ if (!window.SpecialPowers) {
+ window.SpecialPowers = w.SpecialPowers;
+ }
+ w = ancestor(w);
+ }
+
+ if (parentRunner) {
+ SimpleTest.harnessParameters = parentRunner.getParameterInfo();
+ }
+})();
+
+/* Helper functions pulled out of various MochiKit modules */
+if (typeof(repr) == 'undefined') {
+ this.repr = function(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 != arguments.callee) {
+ 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 (var i = 1; i < arguments.length; i++) {
+ args.push(arguments[i]);
+ }
+ return function() {
+ if (arguments.length > 0) {
+ for (var 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 () {
+ var callStack = arguments.callee.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, null);
+ 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.expected = 'pass';
+SimpleTest.num_failed = 0;
+SimpleTest._inChaosMode = false;
+
+SimpleTest.setExpected = function () {
+ if (parent.TestRunner) {
+ SimpleTest.expected = parent.TestRunner.expected;
+ }
+}
+SimpleTest.setExpected();
+
+/**
+ * Something like assert.
+**/
+SimpleTest.ok = function (condition, name, diag, stack = null) {
+
+ var test = {'result': !!condition, 'name': name, 'diag': diag};
+ if (SimpleTest.expected == 'fail') {
+ if (!test.result) {
+ SimpleTest.num_failed++;
+ test.result = !test.result;
+ }
+ var successInfo = {status:"FAIL", expected:"FAIL", message:"TEST-KNOWN-FAIL"};
+ var failureInfo = {status:"PASS", expected:"FAIL", message:"TEST-UNEXPECTED-PASS"};
+ } else {
+ var successInfo = {status:"PASS", expected:"PASS", message:"TEST-PASS"};
+ var 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.ok(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.ok(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.ok(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': name, 'diag': diag, todo: true};
+ 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 lastSlashIdx = path.lastIndexOf("/") + 1;
+ var filename = path.substr(lastSlashIdx);
+ 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 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 = 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.
+ */
+SimpleTest.requestLongerTimeout = function (factor) {
+ if (parentRunner) {
+ parentRunner.requestLongerTimeout(factor);
+ }
+}
+
+/**
+ * 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:
+ *
+ * // 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.
+ *
+ * The reason parameter should be a string representation of the
+ * reason why using such flaky timeouts.
+ *
+ * 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!
+ */
+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;
+}
+
+SimpleTest._pendingWaitForFocusCount = 0;
+
+/**
+ * Version of waitForFocus that returns a promise. The Promise will
+ * not resolve to the focused window, as it might be a CPOW (and Promises
+ * cannot be resolved with CPOWs). If you require the focused window,
+ * you should use waitForFocus instead.
+ */
+SimpleTest.promiseFocus = function *(targetWindow, expectBlankPage)
+{
+ return new Promise(function (resolve, reject) {
+ SimpleTest.waitForFocus(win => {
+ // Just resolve, without passing the window (see bug 1233497)
+ resolve();
+ }, targetWindow, expectBlankPage);
+ });
+}
+
+/**
+ * If the page is not yet loaded, waits for the load event. In addition, if
+ * the page is not yet focused, focuses and waits for the window to be
+ * focused. Calls the callback when completed. 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.
+ *
+ * targetWindow should be specified if it is different than 'window'. The actual
+ * focused window may be a descendant of targetWindow.
+ *
+ * @param callback
+ * function called when load and focus are complete
+ * @param targetWindow
+ * optional window to be loaded and focused, defaults to 'window'.
+ * This may also be a <browser> element, in which case the window within
+ * that browser will be focused.
+ * @param expectBlankPage
+ * true if targetWindow.location is 'about:blank'. Defaults to false
+ */
+SimpleTest.waitForFocus = function (callback, targetWindow, expectBlankPage) {
+ // A separate method is used that is serialized and passed to the child
+ // process via loadFrameScript. Once the child window is focused, the
+ // child will send the WaitForFocus:ChildFocused notification to the parent.
+ // If a child frame in a child process must be focused, a
+ // WaitForFocus:FocusChild message is then sent to the child to focus that
+ // child. This message is used so that the child frame can be passed to it.
+ function waitForFocusInner(targetWindow, isChildProcess, expectBlankPage)
+ {
+ /* Indicates whether the desired targetWindow has loaded or focused. The
+ finished flag is set when the callback has been called and is used to
+ reject extraneous events from invoking the callback again. */
+ var loaded = false, focused = false, finished = false;
+
+ function info(msg) {
+ if (!isChildProcess) {
+ SimpleTest.info(msg);
+ }
+ }
+
+ function focusedWindow() {
+ if (isChildProcess) {
+ return Components.classes["@mozilla.org/focus-manager;1"].
+ getService(Components.interfaces.nsIFocusManager).focusedWindow;
+ }
+ return SpecialPowers.focusedWindow();
+ }
+
+ function getHref(aWindow) {
+ return isChildProcess ? aWindow.location.href :
+ SpecialPowers.getPrivilegedProps(aWindow, 'location.href');
+ }
+
+ /* Event listener for the load or focus events. It will also be called with
+ event equal to null to check if the page is already focused and loaded. */
+ function focusedOrLoaded(event) {
+ try {
+ if (event) {
+ if (event.type == "load") {
+ if (expectBlankPage != (event.target.location == "about:blank")) {
+ return;
+ }
+
+ loaded = true;
+ } else if (event.type == "focus") {
+ focused = true;
+ }
+
+ event.currentTarget.removeEventListener(event.type, focusedOrLoaded, true);
+ }
+
+ if (loaded && focused && !finished) {
+ finished = true;
+ if (isChildProcess) {
+ sendAsyncMessage("WaitForFocus:ChildFocused", {}, null);
+ } else {
+ SimpleTest._pendingWaitForFocusCount--;
+ SimpleTest.executeSoon(function() { callback(targetWindow) });
+ }
+ }
+ } catch (e) {
+ if (!isChildProcess) {
+ SimpleTest.ok(false, "Exception caught in focusedOrLoaded: " + e.message +
+ ", at: " + e.fileName + " (" + e.lineNumber + ")");
+ }
+ }
+ }
+
+ function waitForLoadAndFocusOnWindow(desiredWindow) {
+ /* If the current document is about:blank and we are not expecting a blank
+ page (or vice versa), and the document has not yet loaded, wait for the
+ page to load. A common situation is to wait for a newly opened window
+ to load its content, and we want to skip over any intermediate blank
+ pages that load. This issue is described in bug 554873. */
+ loaded = expectBlankPage ?
+ getHref(desiredWindow) == "about:blank" :
+ getHref(desiredWindow) != "about:blank" &&
+ desiredWindow.document.readyState == "complete";
+ if (!loaded) {
+ info("must wait for load");
+ desiredWindow.addEventListener("load", focusedOrLoaded, true);
+ }
+
+ var childDesiredWindow = { };
+ if (isChildProcess) {
+ var fm = Components.classes["@mozilla.org/focus-manager;1"].
+ getService(Components.interfaces.nsIFocusManager);
+ fm.getFocusedElementForWindow(desiredWindow, true, childDesiredWindow);
+ childDesiredWindow = childDesiredWindow.value;
+ } else {
+ childDesiredWindow = SpecialPowers.getFocusedElementForWindow(desiredWindow, true);
+ }
+
+ /* If this is a child frame, ensure that the frame is focused. */
+ focused = (focusedWindow() == childDesiredWindow);
+ if (!focused) {
+ info("must wait for focus");
+ childDesiredWindow.addEventListener("focus", focusedOrLoaded, true);
+ if (isChildProcess) {
+ childDesiredWindow.focus();
+ }
+ else {
+ SpecialPowers.focus(childDesiredWindow);
+ }
+ }
+
+ focusedOrLoaded(null);
+ }
+
+ if (isChildProcess) {
+ /* This message is used when an inner child frame must be focused. */
+ addMessageListener("WaitForFocus:FocusChild", function focusChild(msg) {
+ removeMessageListener("WaitForFocus:FocusChild", focusChild);
+ finished = false;
+ waitForLoadAndFocusOnWindow(msg.objects.child);
+ });
+ }
+
+ waitForLoadAndFocusOnWindow(targetWindow);
+ }
+
+ SimpleTest._pendingWaitForFocusCount++;
+ if (!targetWindow) {
+ targetWindow = window;
+ }
+
+ expectBlankPage = !!expectBlankPage;
+
+ // If this is a request to focus a remote child window, the request must
+ // be forwarded to the child process.
+ // XXXndeakin now sure what this issue with Components.utils is about, but
+ // browser tests require the former and plain tests require the latter.
+ var Cu = Components.utils || SpecialPowers.Cu;
+ var Ci = Components.interfaces || SpecialPowers.Ci;
+
+ var browser = null;
+ if (typeof(XULElement) != "undefined" &&
+ targetWindow instanceof XULElement &&
+ targetWindow.localName == "browser") {
+ browser = targetWindow;
+ }
+
+ var isWrapper = Cu.isCrossProcessWrapper(targetWindow);
+ if (isWrapper || (browser && browser.isRemoteBrowser)) {
+ var mustFocusSubframe = false;
+ if (isWrapper) {
+ // Look for a tabbrowser and see if targetWindow corresponds to one
+ // within that tabbrowser. If not, just return.
+ var tabBrowser = document.getElementsByTagName("tabbrowser")[0] || null;
+ browser = tabBrowser ? tabBrowser.getBrowserForContentWindow(targetWindow.top) : null;
+ if (!browser) {
+ SimpleTest.info("child process window cannot be focused");
+ return;
+ }
+
+ mustFocusSubframe = (targetWindow != targetWindow.top);
+ }
+
+ // If a subframe in a child process needs to be focused, first focus the
+ // parent frame, then send a WaitForFocus:FocusChild message to the child
+ // containing the subframe to focus.
+ browser.messageManager.addMessageListener("WaitForFocus:ChildFocused", function waitTest(msg) {
+ if (mustFocusSubframe) {
+ mustFocusSubframe = false;
+ var mm = gBrowser.selectedBrowser.messageManager;
+ mm.sendAsyncMessage("WaitForFocus:FocusChild", {}, { child: targetWindow } );
+ }
+ else {
+ browser.messageManager.removeMessageListener("WaitForFocus:ChildFocused", waitTest);
+ SimpleTest._pendingWaitForFocusCount--;
+ setTimeout(callback, 0, browser ? browser.contentWindowAsCPOW : targetWindow);
+ }
+ });
+
+ // Serialize the waitForFocusInner function and run it in the child process.
+ var frameScript = "data:,(" + waitForFocusInner.toString() +
+ ")(content, true, " + expectBlankPage + ");";
+ browser.messageManager.loadFrameScript(frameScript, true);
+ browser.focus();
+ }
+ else {
+ // Otherwise, this is an attempt to focus a single process or parent window,
+ // so pass false for isChildProcess.
+ if (browser) {
+ targetWindow = browser.contentWindow;
+ }
+
+ waitForFocusInner(targetWindow, false, expectBlankPage);
+ }
+};
+
+SimpleTest.waitForClipboard_polls = 0;
+
+/*
+ * 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/unicode
+ * values.
+ *
+ * @param aExpectedStringOrValidatorFn
+ * The string value that is expected to be on the clipboard or a
+ * validator function getting cripboard data and returning a bool.
+ * @param aSetupFn
+ * A function responsible for setting the clipboard to the expected value,
+ * called after the known value setting succeeds.
+ * @param aSuccessFn
+ * A function called when the expected value is found on the clipboard.
+ * @param 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 aFlavor [optional] The flavor to look for. Defaults to "text/unicode".
+ * @param aTimeout [optional]
+ * The timeout (in milliseconds) to wait for a clipboard change.
+ * Defaults to 5000.
+ * @param aExpectFailure [optional]
+ * 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.
+ * Defaults to false.
+ */
+SimpleTest.__waitForClipboardMonotonicCounter = 0;
+SimpleTest.__defineGetter__("_waitForClipboardMonotonicCounter", function () {
+ return SimpleTest.__waitForClipboardMonotonicCounter++;
+});
+SimpleTest.waitForClipboard = function(aExpectedStringOrValidatorFn, aSetupFn,
+ aSuccessFn, aFailureFn, aFlavor, aTimeout, aExpectFailure) {
+ var requestedFlavor = aFlavor || "text/unicode";
+
+ // The known value we put on the clipboard before running aSetupFn
+ var initialVal = SimpleTest._waitForClipboardMonotonicCounter +
+ "-waitForClipboard-known-value";
+
+ var 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;
+ };
+ } else {
+ // Build a default validator function for common string input.
+ inputValidatorFn = typeof(aExpectedStringOrValidatorFn) == "string"
+ ? function(aData) { return aData == aExpectedStringOrValidatorFn; }
+ : aExpectedStringOrValidatorFn;
+ }
+
+ var maxPolls = aTimeout ? aTimeout / 100 : 50;
+
+ // reset for the next use
+ function reset() {
+ SimpleTest.waitForClipboard_polls = 0;
+ }
+
+ var lastValue;
+ function wait(validatorFn, successFn, failureFn, flavor) {
+ if (SimpleTest.waitForClipboard_polls == 0) {
+ lastValue = undefined;
+ }
+
+ if (++SimpleTest.waitForClipboard_polls > maxPolls) {
+ // Log the failure.
+ SimpleTest.ok(aExpectFailure, "Timed out while polling clipboard for pasted data");
+ dump("Got this value: " + lastValue);
+ reset();
+ failureFn();
+ return;
+ }
+
+ var data = SpecialPowers.getClipboardData(flavor);
+
+ if (validatorFn(data)) {
+ // Don't show the success message when waiting for preExpectedVal
+ if (preExpectedVal)
+ preExpectedVal = null;
+ else
+ SimpleTest.ok(!aExpectFailure, "Clipboard has the given value");
+ reset();
+ successFn();
+ } else {
+ lastValue = data;
+ SimpleTest._originalSetTimeout.apply(window, [function() { return wait(validatorFn, successFn, failureFn, flavor); }, 100]);
+ }
+ }
+
+ // First we wait for a known value different from the expected one.
+ var preExpectedVal = initialVal;
+ SpecialPowers.clipboardCopyString(preExpectedVal);
+ wait(function(aData) { return aData == preExpectedVal; },
+ function() {
+ // Call the original setup fn
+ aSetupFn();
+ wait(inputValidatorFn, aSuccessFn, aFailureFn, requestedFlavor);
+ }, aFailureFn, "text/unicode");
+}
+
+/**
+ * Wait for a condition for a while (actually up to 3s here).
+ *
+ * @param aCond
+ * A function returns the result of the condition
+ * @param aCallback
+ * A function called after the condition is passed or timeout.
+ * @param aErrorMsg
+ * The message displayed when the condition failed to pass
+ * before timeout.
+ */
+SimpleTest.waitForCondition = function (aCond, aCallback, aErrorMsg) {
+ var tries = 0;
+ var interval = setInterval(() => {
+ if (tries >= 30) {
+ ok(false, aErrorMsg);
+ moveOn();
+ return;
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = aCond();
+ } catch (e) {
+ ok(false, `${e}\n${e.stack}`);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ var moveOn = () => { clearInterval(interval); aCallback(); };
+};
+SimpleTest.promiseWaitForCondition = function (aCond, aErrorMsg) {
+ return new Promise(resolve => {
+ this.waitForCondition(aCond, resolve, aErrorMsg);
+ });
+};
+
+/**
+ * Executes a function shortly after the call, but lets the caller continue
+ * working (or finish).
+ */
+SimpleTest.executeSoon = function(aFunc) {
+ if ("SpecialPowers" in window) {
+ return SpecialPowers.executeSoon(aFunc, window);
+ }
+ setTimeout(aFunc, 0);
+ return null; // Avoid warning.
+};
+
+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;
+};
+
+SimpleTest.timeout = function() {
+ for (let func of SimpleTest._timeoutFunctions) {
+ func();
+ }
+ SimpleTest._timeoutFunctions = [];
+}
+
+/**
+ * Finishes the tests. This is automatically called, except when
+ * SimpleTest.waitForExplicitFinish() has been invoked.
+**/
+SimpleTest.finish = function() {
+ if (SimpleTest._alreadyFinished) {
+ var err = "[SimpleTest.finish()] this test already called finish!";
+ if (parentRunner) {
+ parentRunner.structuredLogger.error(err);
+ } else {
+ dump(err + '\n');
+ }
+ }
+
+ if (SimpleTest.expected == 'fail' && SimpleTest.num_failed <= 0) {
+ msg = 'We expected at least one failure';
+ var test = {'result': false, 'name': 'fail-if condition in manifest', 'diag': msg};
+ var successInfo = {status:"FAIL", expected:"FAIL", message:"TEST-KNOWN-FAIL"};
+ var failureInfo = {status:"PASS", expected:"FAIL", message:"TEST-UNEXPECTED-PASS"};
+
+ 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 = 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._pendingWaitForFocusCount != 0) {
+ SimpleTest.is(SimpleTest._pendingWaitForFocusCount, 0,
+ "[SimpleTest.finish()] waitForFocus() was called a "
+ + "different number of times from the number of "
+ + "callbacks run. Maybe the test terminated "
+ + "prematurely -- be sure to use "
+ + "SimpleTest.waitForExplicitFinish().");
+ }
+ if (SimpleTest._tests.length == 0) {
+ 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.)");
+ }
+ if (SimpleTest._expectingRegisteredServiceWorker) {
+ if (!SpecialPowers.isServiceWorkerRegistered()) {
+ SimpleTest.ok(false, "This test is expected to leave a service worker registered");
+ }
+ } else {
+ if (SpecialPowers.isServiceWorkerRegistered()) {
+ SimpleTest.ok(false, "This test left a service worker registered without cleaning it up");
+ }
+ }
+
+ 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();
+ });
+ });
+ }
+ }
+
+ 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();
+};
+
+/**
+ * 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, isException, isStrict: 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) {
+ var 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 = msgs.toSource();
+ function listener(msg) {
+ if (msg.message === "SENTINEL" && !msg.isScriptError) {
+ is(counter, msgs.length,
+ "monitorConsole | number of messages " + assertionLabel);
+ SimpleTest.executeSoon(continuation);
+ return;
+ }
+ for (var 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 (var 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; for (var i in o1) o1Size++;
+ var o2Size = 0; for (var i in o2) o2Size++;
+ var bigger = o1Size > o2Size ? o1 : o2;
+ for (var 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 (var 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 (var 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.ok(true, name);
+ } else {
+ SimpleTest.ok(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 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.ok(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.indexOf("Mobile") == -1 &&
+ nav.userAgent.indexOf("Tablet") == -1) {
+ 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.indexOf("Android") != -1 ?
+ 'version' : 'sdk_version';
+ gAndroidSdk = SpecialPowers.Cc['@mozilla.org/system-info;1']
+ .getService(SpecialPowers.Ci.nsIPropertyBag2)
+ .getProperty(versionString);
+ }
+ document.documentElement.removeChild(iframe);
+ }
+ return gAndroidSdk;
+}
diff --git a/testing/mochitest/tests/SimpleTest/SpawnTask.js b/testing/mochitest/tests/SimpleTest/SpawnTask.js
new file mode 100644
index 0000000000..7ac598f880
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/SpawnTask.js
@@ -0,0 +1,296 @@
+// # SpawnTask.js
+// Directly copied from the "co" library by TJ Holowaychuk.
+// See https://github.com/tj/co/tree/4.6.0
+// For use with mochitest-plain and mochitest-chrome.
+
+// spawn_task(generatorFunction):
+// Expose only the `co` function, which is very similar to Task.spawn in Task.jsm.
+// We call this function spawn_task to make its purpose more plain, and to
+// reduce the chance of name collisions.
+var spawn_task = (function () {
+
+/**
+ * slice() reference.
+ */
+
+var slice = Array.prototype.slice;
+
+/**
+ * Wrap the given generator `fn` into a
+ * function that returns a promise.
+ * This is a separate function so that
+ * every `co()` call doesn't create a new,
+ * unnecessary closure.
+ *
+ * @param {GeneratorFunction} fn
+ * @return {Function}
+ * @api public
+ */
+
+co.wrap = function (fn) {
+ createPromise.__generatorFunction__ = fn;
+ return createPromise;
+ function createPromise() {
+ return co.call(this, fn.apply(this, arguments));
+ }
+};
+
+/**
+ * Execute the generator function or a generator
+ * and return a promise.
+ *
+ * @param {Function} fn
+ * @return {Promise}
+ * @api public
+ */
+
+function co(gen) {
+ var ctx = this;
+ var args = slice.call(arguments, 1)
+
+ // we wrap everything in a promise to avoid promise chaining,
+ // which leads to memory leak errors.
+ // see https://github.com/tj/co/issues/180
+ return new Promise(function(resolve, reject) {
+ if (typeof gen === 'function') gen = gen.apply(ctx, args);
+ if (!gen || typeof gen.next !== 'function') return resolve(gen);
+
+ onFulfilled();
+
+ /**
+ * @param {Mixed} res
+ * @return {Promise}
+ * @api private
+ */
+
+ function onFulfilled(res) {
+ var ret;
+ try {
+ ret = gen.next(res);
+ } catch (e) {
+ return reject(e);
+ }
+ next(ret);
+ }
+
+ /**
+ * @param {Error} err
+ * @return {Promise}
+ * @api private
+ */
+
+ function onRejected(err) {
+ var ret;
+ try {
+ ret = gen.throw(err);
+ } catch (e) {
+ return reject(e);
+ }
+ next(ret);
+ }
+
+ /**
+ * Get the next value in the generator,
+ * return a promise.
+ *
+ * @param {Object} ret
+ * @return {Promise}
+ * @api private
+ */
+
+ function next(ret) {
+ if (ret.done) return resolve(ret.value);
+ var value = toPromise.call(ctx, ret.value);
+ if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
+ return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ + 'but the following object was passed: "' + String(ret.value) + '"'));
+ }
+ });
+}
+
+/**
+ * Convert a `yield`ed value into a promise.
+ *
+ * @param {Mixed} obj
+ * @return {Promise}
+ * @api private
+ */
+
+function toPromise(obj) {
+ if (!obj) return obj;
+ if (isPromise(obj)) return obj;
+ if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
+ if ('function' == typeof obj) return thunkToPromise.call(this, obj);
+ if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
+ if (isObject(obj)) return objectToPromise.call(this, obj);
+ return obj;
+}
+
+/**
+ * Convert a thunk to a promise.
+ *
+ * @param {Function}
+ * @return {Promise}
+ * @api private
+ */
+
+function thunkToPromise(fn) {
+ var ctx = this;
+ return new Promise(function (resolve, reject) {
+ fn.call(ctx, function (err, res) {
+ if (err) return reject(err);
+ if (arguments.length > 2) res = slice.call(arguments, 1);
+ resolve(res);
+ });
+ });
+}
+
+/**
+ * Convert an array of "yieldables" to a promise.
+ * Uses `Promise.all()` internally.
+ *
+ * @param {Array} obj
+ * @return {Promise}
+ * @api private
+ */
+
+function arrayToPromise(obj) {
+ return Promise.all(obj.map(toPromise, this));
+}
+
+/**
+ * Convert an object of "yieldables" to a promise.
+ * Uses `Promise.all()` internally.
+ *
+ * @param {Object} obj
+ * @return {Promise}
+ * @api private
+ */
+
+function objectToPromise(obj){
+ var results = new obj.constructor();
+ var keys = Object.keys(obj);
+ var promises = [];
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ var promise = toPromise.call(this, obj[key]);
+ if (promise && isPromise(promise)) defer(promise, key);
+ else results[key] = obj[key];
+ }
+ return Promise.all(promises).then(function () {
+ return results;
+ });
+
+ function defer(promise, key) {
+ // predefine the key in the result
+ results[key] = undefined;
+ promises.push(promise.then(function (res) {
+ results[key] = res;
+ }));
+ }
+}
+
+/**
+ * Check if `obj` is a promise.
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ * @api private
+ */
+
+function isPromise(obj) {
+ return 'function' == typeof obj.then;
+}
+
+/**
+ * Check if `obj` is a generator.
+ *
+ * @param {Mixed} obj
+ * @return {Boolean}
+ * @api private
+ */
+
+function isGenerator(obj) {
+ return 'function' == typeof obj.next && 'function' == typeof obj.throw;
+}
+
+/**
+ * Check if `obj` is a generator function.
+ *
+ * @param {Mixed} obj
+ * @return {Boolean}
+ * @api private
+ */
+function isGeneratorFunction(obj) {
+ var constructor = obj.constructor;
+ if (!constructor) return false;
+ if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
+ return isGenerator(constructor.prototype);
+}
+
+/**
+ * Check for plain object.
+ *
+ * @param {Mixed} val
+ * @return {Boolean}
+ * @api private
+ */
+
+function isObject(val) {
+ return Object == val.constructor;
+}
+
+return co;
+})();
+
+// 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 = [];
+ // The "add_task" function
+ return function (generatorFunction) {
+ 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 () {
+ spawn_task(function* () {
+ // 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 || "";
+ info("SpawnTask.js | Entering test " + name);
+ yield task();
+ info("SpawnTask.js | Leaving test " + name);
+ }
+ } catch (ex) {
+ try {
+ ok(false, "" + ex);
+ } catch (ex2) {
+ ok(false, "(The exception cannot be converted to string.)");
+ }
+ }
+ // All tasks are finished.
+ SimpleTest.finish();
+ });
+ });
+ }
+ // Add the task to the list of tasks to run after
+ // the main thread is finished.
+ task_list.push(generatorFunction);
+ };
+})();
diff --git a/testing/mochitest/tests/SimpleTest/TestRunner.js b/testing/mochitest/tests/SimpleTest/TestRunner.js
new file mode 100644
index 0000000000..aa0af2f20d
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/TestRunner.js
@@ -0,0 +1,754 @@
+/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */
+/*
+ * e10s event dispatcher from content->chrome
+ *
+ * type = eventName (QuitApplication)
+ * data = json object {"filename":filename} <- for LoggerInit
+ */
+
+"use strict";
+
+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": sync,
+ "type": 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;
+}
+
+/**
+ * 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 = 5 * 60 * 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._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;
+
+TestRunner._checkForHangs = function() {
+ function reportError(win, msg) {
+ if ("SimpleTest" in win) {
+ win.SimpleTest.ok(false, msg);
+ } else if ("W3CTest" in win) {
+ win.W3CTest.logFailure(msg);
+ }
+ }
+
+ function killTest(win) {
+ if ("SimpleTest" in win) {
+ win.SimpleTest.timeout();
+ win.SimpleTest.finish();
+ } else if ("W3CTest" in win) {
+ win.W3CTest.timeout();
+ }
+ }
+
+ if (TestRunner._currentTest < TestRunner._urls.length) {
+ var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;
+ if (runtime >= TestRunner.timeout * TestRunner._timeoutFactor) {
+ var frameWindow = $('testframe').contentWindow.wrappedJSObject ||
+ $('testframe').contentWindow;
+ // TODO : Do this in a way that reports that the test ended with a status "TIMEOUT"
+ reportError(frameWindow, "Test timed out.");
+
+ // 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(function delayedKillTest() { killTest(frameWindow); }, 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 "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 SpecialPowersLogger(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 = String.fromCharCode(0xe175) + String.fromCharCode(0xee31) + String.fromCharCode(0x2c32) + String.fromCharCode(0xacbf);
+
+// A log callback for StructuredLog.jsm
+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://dxr.mozilla.org/mozilla-central/source/testing/modules/StructuredLog.jsm
+TestRunner.structuredLogger = new StructuredLogger('mochitest', TestRunner._dumpMessage);
+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.
+ 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('TestRunner._makeIframe("'+url+'", '+(retry+1)+')', 1000);
+ return;
+ }
+
+ TestRunner.structuredLogger.info("Error: Unable to restore focus, expect failures and timeouts.");
+ }
+ window.scrollTo(0, $('indicator').offsetTop);
+ iframe.src = url;
+ iframe.name = url;
+ iframe.width = "500";
+ return iframe;
+};
+
+/**
+ * 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 () {
+ var prefix = "";
+ // handle mochitest-chrome URIs
+ if ($('testframe').contentWindow.location.protocol == "chrome:") {
+ prefix = "chrome://mochitests";
+ }
+ return prefix + $('testframe').contentWindow.location.pathname;
+};
+
+TestRunner.setParameterInfo = function (params) {
+ this._params = params;
+};
+
+TestRunner.getParameterInfo = function() {
+ return this._params;
+};
+
+/**
+ * TestRunner entry point.
+ *
+ * The arguments are the URLs of the test to be ran.
+ *
+**/
+TestRunner.runTests = function (/*url...*/) {
+ TestRunner.structuredLogger.info("SimpleTest START");
+ TestRunner.originalTestURL = $("current-test").innerHTML;
+
+ SpecialPowers.registerProcessCrashObservers();
+
+ 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;
+TestRunner.runNextTest = function() {
+ if (TestRunner._currentTest < TestRunner._urls.length &&
+ !TestRunner._haltTests)
+ {
+ var url = TestRunner.getNextUrl();
+ TestRunner.currentTestURL = url;
+
+ $("current-test-path").innerHTML = url;
+
+ TestRunner._currentTestStartTime = new Date().valueOf();
+ TestRunner._timeoutFactor = 1;
+ TestRunner._expectedMinAsserts = 0;
+ TestRunner._expectedMaxAsserts = 0;
+
+ TestRunner.structuredLogger.testStart(url);
+
+ 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.testEnd('SimpleTest/TestRunner.js',
+ "ERROR",
+ "OK",
+ "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";
+ }
+
+ SpecialPowers.unregisterProcessCrashObservers();
+
+ 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
+ if (TestRunner.repeat === 0) {
+ 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();
+ }
+};
+
+TestRunner.expectChildProcessCrash = function() {
+ TestRunner._expectingProcessCrash = true;
+};
+
+/**
+ * This stub is called by SimpleTest when a test is finished.
+**/
+TestRunner.testFinished = function(tests) {
+ // 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;
+ }
+ 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);
+
+ function cleanUpCrashDumpFiles() {
+ if (!SpecialPowers.removeExpectedCrashDumpFiles(TestRunner._expectingProcessCrash)) {
+ TestRunner.structuredLogger.testEnd(TestRunner.currentTestURL,
+ "ERROR",
+ "OK",
+ "This test did not leave any crash dumps behind, but we were expecting some!");
+ tests.push({ result: false });
+ }
+ var unexpectedCrashDumpFiles =
+ SpecialPowers.findUnexpectedCrashDumpFiles();
+ TestRunner._expectingProcessCrash = false;
+ if (unexpectedCrashDumpFiles.length) {
+ TestRunner.structuredLogger.testEnd(TestRunner.currentTestURL,
+ "ERROR",
+ "OK",
+ "This test left crash dumps behind, but we " +
+ "weren't expecting it to!",
+ {unexpected_crashdump_files: unexpectedCrashDumpFiles});
+ tests.push({ result: false });
+ unexpectedCrashDumpFiles.sort().forEach(function(aFilename) {
+ TestRunner.structuredLogger.info("Found unexpected crash dump file " +
+ aFilename + ".");
+ });
+ }
+ }
+
+ 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()});
+ tests.push({ result: false });
+ }
+
+ var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;
+
+ TestRunner.structuredLogger.testEnd(TestRunner.currentTestURL,
+ "OK",
+ undefined,
+ "Finished in " + runtime + "ms",
+ {runtime: runtime}
+ );
+
+ if (TestRunner.slowestTestTime < runtime && TestRunner._timeoutFactor >= 1) {
+ TestRunner.slowestTestTime = runtime;
+ TestRunner.slowestTestURL = TestRunner.currentTestURL;
+ }
+
+ TestRunner.updateUI(tests);
+
+ // 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 ($('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
+ $('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.testStatus(TestRunner.currentTestURL, wrongtestname, 'FAIL', 'PASS', "Result logged after SimpleTest.finish()");
+ }
+ TestRunner.updateUI([{ result: false }]);
+ }
+ } , false);
+ TestRunner._makeIframe(interstitialURL, 0);
+ }
+
+ SpecialPowers.executeAfterFlushingMessageQueue(function() {
+ cleanUpCrashDumpFiles();
+ SpecialPowers.flushPermissions(function () { SpecialPowers.flushPrefEnv(runNextTest); });
+ });
+};
+
+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();
+ var numAsserts = newAssertionCount - TestRunner._lastAssertionCount;
+ TestRunner._lastAssertionCount = newAssertionCount;
+
+ var url = TestRunner.getNextUrl();
+ var max = TestRunner._expectedMaxAsserts;
+ var min = TestRunner._expectedMinAsserts;
+ if (numAsserts > max) {
+ TestRunner.structuredLogger.testEnd(url,
+ "ERROR",
+ "OK",
+ "Assertion count " + numAsserts + " is greater than expected range " +
+ min + "-" + max + " assertions.",
+ {assertions: numAsserts, min_asserts: min, max_asserts: max});
+ TestRunner.updateUI([{ result: false }]);
+ } else if (numAsserts < min) {
+ TestRunner.structuredLogger.testEnd(url,
+ "OK",
+ "ERROR",
+ "Assertion count " + numAsserts + " is less than expected range " +
+ min + "-" + max + " assertions.",
+ {assertions: numAsserts, min_asserts: min, max_asserts: max});
+ TestRunner.updateUI([{ result: false }]);
+ } else if (numAsserts > 0) {
+ TestRunner.structuredLogger.testEnd(url,
+ "ERROR",
+ "ERROR",
+ "Assertion count " + numAsserts + " within expected range " +
+ min + "-" + max + " assertions.",
+ {assertions: numAsserts, min_asserts: min, max_asserts: max});
+ }
+ }
+ 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 == 0) {
+ //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);
+ }
+}
diff --git a/testing/mochitest/tests/SimpleTest/WindowSnapshot.js b/testing/mochitest/tests/SimpleTest/WindowSnapshot.js
new file mode 100644
index 0000000000..c4ced41dd3
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/WindowSnapshot.js
@@ -0,0 +1,92 @@
+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 - comparing them makes no sense");
+ 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) {
+ // 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 TEST-UNEXPECTED-FAIL | " + 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";
+ }
+ 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/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..461a6f49b7
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/moz.build
@@ -0,0 +1,24 @@
+# -*- 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',
+ '/testing/specialpowers/content/MozillaLogger.js',
+ 'EventUtils.js',
+ 'ExtensionTestUtils.js',
+ 'iframe-between-tests.html',
+ 'LogController.js',
+ 'MemoryStats.js',
+ 'MockObjects.js',
+ 'NativeKeyCodes.js',
+ 'paint_listener.js',
+ 'setup.js',
+ 'SimpleTest.js',
+ 'SpawnTask.js',
+ 'test.css',
+ 'TestRunner.js',
+ 'WindowSnapshot.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..304a0fd629
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/paint_listener.js
@@ -0,0 +1,83 @@
+(function() {
+ var accumulatedRect = null;
+ var onpaint = new Array();
+ var debug = false;
+ const FlushModes = {
+ FLUSH: 0,
+ NOFLUSH: 1
+ };
+
+ function paintListener(event) {
+ if (event.target != window)
+ return;
+ var eventRect =
+ [ event.boundingClientRect.left,
+ event.boundingClientRect.top,
+ event.boundingClientRect.right,
+ event.boundingClientRect.bottom ];
+ 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;
+ while (onpaint.length > 0) {
+ window.setTimeout(onpaint.pop(), 0);
+ }
+ }
+ window.addEventListener("MozAfterPaint", paintListener, false);
+
+ 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);
+ };
+})();
diff --git a/testing/mochitest/tests/SimpleTest/setup.js b/testing/mochitest/tests/SimpleTest/setup.js
new file mode 100644
index 0000000000..e6689022b2
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/setup.js
@@ -0,0 +1,260 @@
+/* -*- 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";
+
+TestRunner.logEnabled = true;
+TestRunner.logger = LogController;
+
+/* Helper function */
+function parseQueryString(encodedString, useArrays) {
+ // strip a leading '?' from the encoded string
+ var qstr = (encodedString.length > 0 && 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;
+};
+
+// 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 {
+ 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 {
+ 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;
+}
+
+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 spl = new SpecialPowersLogger(params.logFile);
+ TestRunner.logger.addListener("mozLogger", fileLevel + "", spl.getLogCallback());
+}
+
+// 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.maxTimeouts) {
+ TestRunner.maxTimeouts = params.maxTimeouts;
+}
+
+// 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("http://mochi.test:8888/" + 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 = "";
+ var addParam = "";
+ if (params.autorun) {
+ window.location.search += "";
+ 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("http://mochi.test:8888/" + params.manifestFile, params, hookupTests);
+ } else {
+ hookupTests(gTestList);
+ }
+}
+
+function hookupTests(testList) {
+ if (testList.length > 0) {
+ 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) {
+ RunSet.runall();
+ }
+}
diff --git a/testing/mochitest/tests/SimpleTest/test.css b/testing/mochitest/tests/SimpleTest/test.css
new file mode 100644
index 0000000000..e6fe345b9d
--- /dev/null
+++ b/testing/mochitest/tests/SimpleTest/test.css
@@ -0,0 +1,43 @@
+.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;
+}
+
+browser[remote="true"] {
+ -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser");
+}