From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- .../tests/SimpleTest/AsyncUtilsContent.js | 98 + testing/mochitest/tests/SimpleTest/ChromePowers.js | 124 ++ testing/mochitest/tests/SimpleTest/EventUtils.js | 2143 ++++++++++++++++++++ .../tests/SimpleTest/ExtensionTestUtils.js | 139 ++ .../mochitest/tests/SimpleTest/LICENSE_SpawnTask | 24 + .../mochitest/tests/SimpleTest/LogController.js | 96 + testing/mochitest/tests/SimpleTest/MemoryStats.js | 122 ++ testing/mochitest/tests/SimpleTest/MockObjects.js | 90 + .../mochitest/tests/SimpleTest/NativeKeyCodes.js | 370 ++++ testing/mochitest/tests/SimpleTest/SimpleTest.js | 1639 +++++++++++++++ testing/mochitest/tests/SimpleTest/SpawnTask.js | 296 +++ testing/mochitest/tests/SimpleTest/TestRunner.js | 754 +++++++ .../mochitest/tests/SimpleTest/WindowSnapshot.js | 92 + .../tests/SimpleTest/iframe-between-tests.html | 17 + testing/mochitest/tests/SimpleTest/moz.build | 24 + .../mochitest/tests/SimpleTest/paint_listener.js | 83 + testing/mochitest/tests/SimpleTest/setup.js | 260 +++ testing/mochitest/tests/SimpleTest/test.css | 43 + 18 files changed, 6414 insertions(+) create mode 100644 testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js create mode 100644 testing/mochitest/tests/SimpleTest/ChromePowers.js create mode 100644 testing/mochitest/tests/SimpleTest/EventUtils.js create mode 100644 testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js create mode 100644 testing/mochitest/tests/SimpleTest/LICENSE_SpawnTask create mode 100644 testing/mochitest/tests/SimpleTest/LogController.js create mode 100644 testing/mochitest/tests/SimpleTest/MemoryStats.js create mode 100644 testing/mochitest/tests/SimpleTest/MockObjects.js create mode 100644 testing/mochitest/tests/SimpleTest/NativeKeyCodes.js create mode 100644 testing/mochitest/tests/SimpleTest/SimpleTest.js create mode 100644 testing/mochitest/tests/SimpleTest/SpawnTask.js create mode 100644 testing/mochitest/tests/SimpleTest/TestRunner.js create mode 100644 testing/mochitest/tests/SimpleTest/WindowSnapshot.js create mode 100644 testing/mochitest/tests/SimpleTest/iframe-between-tests.html create mode 100644 testing/mochitest/tests/SimpleTest/moz.build create mode 100644 testing/mochitest/tests/SimpleTest/paint_listener.js create mode 100644 testing/mochitest/tests/SimpleTest/setup.js create mode 100644 testing/mochitest/tests/SimpleTest/test.css (limited to 'testing/mochitest/tests/SimpleTest') diff --git a/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js b/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js new file mode 100644 index 000000000..0f1cc0608 --- /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 000000000..97de57815 --- /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. 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. 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 000000000..17243625d --- /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 000000000..921d1a83f --- /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 000000000..088c54c9d --- /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 <tj@vision-media.ca> + +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 000000000..52fe9eea8 --- /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 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 000000000..d00f5127b --- /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 000000000..8130f3e18 --- /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 000000000..37713737c --- /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 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 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 000000000..7ac598f88 --- /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 000000000..aa0af2f20 --- /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 = "Finished"; + // 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 000000000..c4ced41dd --- /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 000000000..8de879f20 --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/iframe-between-tests.html @@ -0,0 +1,17 @@ +iframe for between tests + + diff --git a/testing/mochitest/tests/SimpleTest/moz.build b/testing/mochitest/tests/SimpleTest/moz.build new file mode 100644 index 000000000..461a6f49b --- /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 000000000..304a0fd62 --- /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 000000000..e6689022b --- /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(/(\&\;|\&\#38\;|\&|\&)/); + 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 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 000000000..e6fe345b9 --- /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"); +} -- cgit v1.2.3