diff options
Diffstat (limited to 'testing/marionette/event.js')
-rw-r--r-- | testing/marionette/event.js | 1365 |
1 files changed, 1365 insertions, 0 deletions
diff --git a/testing/marionette/event.js b/testing/marionette/event.js new file mode 100644 index 0000000000..c60ca306b1 --- /dev/null +++ b/testing/marionette/event.js @@ -0,0 +1,1365 @@ +/* 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/. */ + +// Provides functionality for creating and sending DOM events. + +"use strict"; + +const {interfaces: Ci, utils: Cu, classes: Cc} = Components; + +Cu.import("resource://gre/modules/Log.jsm"); +const logger = Log.repository.getLogger("Marionette"); + +Cu.import("chrome://marionette/content/element.js"); +Cu.import("chrome://marionette/content/error.js"); + +this.EXPORTED_SYMBOLS = ["event"]; + +// must be synchronised with nsIDOMWindowUtils +const COMPOSITION_ATTR_RAWINPUT = 0x02; +const COMPOSITION_ATTR_SELECTEDRAWTEXT = 0x03; +const COMPOSITION_ATTR_CONVERTEDTEXT = 0x04; +const COMPOSITION_ATTR_SELECTEDCONVERTEDTEXT = 0x05; + +// TODO(ato): Document! +let seenEvent = false; + +function getDOMWindowUtils(win) { + if (!win) { + win = window; + } + + // this assumes we are operating in chrome space + return win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); +} + +this.event = {}; + +event.MouseEvents = { + click: 0, + dblclick: 1, + mousedown: 2, + mouseup: 3, + mouseover: 4, + mouseout: 5, +}; + +event.Modifiers = { + shiftKey: 0, + ctrlKey: 1, + altKey: 2, + metaKey: 3, +}; + +/** + * Sends a mouse event to given target. + * + * @param {nsIDOMMouseEvent} mouseEvent + * Event to send. + * @param {(DOMElement|string)} target + * Target of event. Can either be an element or the ID of an element. + * @param {Window=} window + * Window object. Defaults to the current window. + * + * @throws {TypeError} + * If the event is unsupported. + */ +event.sendMouseEvent = function (mouseEvent, target, window = undefined) { + if (!event.MouseEvents.hasOwnProperty(mouseEvent.type)) { + throw new TypeError("Unsupported event type: " + mouseEvent.type); + } + + if (!target.nodeType && typeof target != "string") { + throw new TypeError("Target can only be a DOM element or a string: " + target); + } + + if (!target.nodeType) { + target = window.document.getElementById(target); + } else { + window = window || target.ownerDocument.defaultView; + } + + let ev = window.document.createEvent("MouseEvent"); + + let type = mouseEvent.type; + let view = window; + + let detail = mouseEvent.detail; + if (!detail) { + if (mouseEvent.type in ["click", "mousedown", "mouseup"]) { + detail = 1; + } else if (mouseEvent.type == "dblclick") { + detail = 2; + } else { + detail = 0; + } + } + + let screenX = mouseEvent.screenX || 0; + let screenY = mouseEvent.screenY || 0; + let clientX = mouseEvent.clientX || 0; + let clientY = mouseEvent.clientY || 0; + let ctrlKey = mouseEvent.ctrlKey || false; + let altKey = mouseEvent.altKey || false; + let shiftKey = mouseEvent.shiftKey || false; + let metaKey = mouseEvent.metaKey || false; + let button = mouseEvent.button || 0; + let relatedTarget = mouseEvent.relatedTarget || null; + + ev.initMouseEvent( + mouseEvent.type, + /* canBubble */ true, + /* cancelable */ true, + view, + detail, + screenX, + screenY, + clientX, + clientY, + ctrlKey, + altKey, + shiftKey, + metaKey, + button, + relatedTarget); +}; + +/** + * Send character to the currently focused element. + * + * This function handles casing of characters (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 English letters (lower and upper + * case) and the digits 0-9. + */ +event.sendChar = function (char, window = undefined) { + // DOM event charcodes match ASCII (JS charcodes) for a-zA-Z0-9 + let hasShift = (char == char.toUpperCase()); + event.synthesizeKey(char, {shiftKey: hasShift}, window); +}; + +/** + * Send string to the focused element. + * + * For now this method only works for English letters (lower and upper + * case) and the digits 0-9. + */ +event.sendString = function (string, window = undefined) { + for (let i = 0; i < string.length; ++i) { + event.sendChar(string.charAt(i), window); + } +}; + +/** + * Send the non-character key to the focused element. + * + * The name of the key should be the part that comes after "DOM_VK_" + * in the nsIDOMKeyEvent constant name for this key. No modifiers are + * handled at this point. + */ +event.sendKey = function (key, window = undefined) { + let keyName = "VK_" + key.toUpperCase(); + event.synthesizeKey(keyName, {shiftKey: false}, window); +}; + +// TODO(ato): Unexpose this when action.Chain#emitMouseEvent +// no longer emits its own events +event.parseModifiers_ = function (modifiers) { + let mval = 0; + if (modifiers.shiftKey) { + mval |= Ci.nsIDOMNSEvent.SHIFT_MASK; + } + if (modifiers.ctrlKey) { + mval |= Ci.nsIDOMNSEvent.CONTROL_MASK; + } + if (modifiers.altKey) { + mval |= Ci.nsIDOMNSEvent.ALT_MASK; + } + if (modifiers.metaKey) { + mval |= Ci.nsIDOMNSEvent.META_MASK; + } + if (modifiers.accelKey) { + if (navigator.platform.indexOf("Mac") >= 0) { + mval |= Ci.nsIDOMNSEvent.META_MASK; + } else { + mval |= Ci.nsIDOMNSEvent.CONTROL_MASK; + } + } + return mval; +}; + +/** + * Synthesise a mouse event on a target. + * + * The actual client point is determined by taking the aTarget's client + * box and offseting it by offsetX and offsetY. This allows mouse clicks + * to be simulated by calling this method. + * + * If the type is specified, an mouse event of that type is + * fired. Otherwise, a mousedown followed by a mouse up is performed. + * + * @param {Element} element + * Element to click. + * @param {number} offsetX + * Horizontal offset to click from the target's bounding box. + * @param {number} offsetY + * Vertical offset to click from the target's bounding box. + * @param {Object.<string, ?>} opts + * Object which may contain the properties "shiftKey", "ctrlKey", + * "altKey", "metaKey", "accessKey", "clickCount", "button", and + * "type". + * @param {Window=} window + * Window object. Defaults to the current window. + */ +event.synthesizeMouse = function ( + element, offsetX, offsetY, opts, window = undefined) { + let rect = element.getBoundingClientRect(); + event.synthesizeMouseAtPoint( + rect.left + offsetX, rect.top + offsetY, opts, window); +}; + +/* + * Synthesize a mouse event at a particular point in a window. + * + * If the type of the event is specified, a mouse event of that type is + * fired. Otherwise, a mousedown followed by a mouse up is performed. + * + * @param {number} left + * CSS pixels from the left document margin. + * @param {number} top + * CSS pixels from the top document margin. + * @param {Object.<string, ?>} opts + * Object which may contain the properties "shiftKey", "ctrlKey", + * "altKey", "metaKey", "accessKey", "clickCount", "button", and + * "type". + * @param {Window=} window + * Window object. Defaults to the current window. + */ +event.synthesizeMouseAtPoint = function ( + left, top, opts, window = undefined) { + + let domutils = getDOMWindowUtils(window); + + let button = opts.button || 0; + let clickCount = opts.clickCount || 1; + let modifiers = event.parseModifiers_(opts); + let pressure = ("pressure" in opts) ? opts.pressure : 0; + let inputSource = ("inputSource" in opts) ? opts.inputSource : + Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE; + let isDOMEventSynthesized = + ("isSynthesized" in opts) ? opts.isSynthesized : true; + let isWidgetEventSynthesized = + ("isWidgetEventSynthesized" in opts) ? opts.isWidgetEventSynthesized : false; + let buttons = ("buttons" in opts) ? opts.buttons : domutils.MOUSE_BUTTONS_NOT_SPECIFIED; + + if (("type" in opts) && opts.type) { + domutils.sendMouseEvent( + opts.type, left, top, button, clickCount, modifiers, false, pressure, inputSource, + isDOMEventSynthesized, isWidgetEventSynthesized, buttons); + } else { + domutils.sendMouseEvent( + "mousedown", left, top, button, clickCount, modifiers, false, pressure, inputSource, + isDOMEventSynthesized, isWidgetEventSynthesized, buttons); + domutils.sendMouseEvent( + "mouseup", left, top, button, clickCount, modifiers, false, pressure, inputSource, + isDOMEventSynthesized, isWidgetEventSynthesized, buttons); + } +}; + +/** + * Call event.synthesizeMouse with coordinates at the centre of the + * target. + */ +event.synthesizeMouseAtCenter = function (element, event, window) { + let rect = element.getBoundingClientRect(); + event.synthesizeMouse( + element, + rect.width / 2, + rect.height / 2, + event, + window); +}; + +/** + * Synthesise a mouse scroll event on a target. + * + * The actual client point is determined by taking the target's client + * box and offseting it by |offsetX| and |offsetY|. + * + * If the |type| property is specified for the |event| argument, a mouse + * scroll event of that type is fired. Otherwise, DOMMouseScroll is used. + * + * If the |axis| is specified, it must be one of "horizontal" or + * "vertical". If not specified, "vertical" is used. + * + * |delta| is the amount to scroll by (can be positive or negative). + * It must be specified. + * + * |hasPixels| specifies whether kHasPixels should be set in the + * |scrollFlags|. + * + * |isMomentum| specifies whether kIsMomentum should be set in the + * |scrollFlags|. + * + * @param {Element} target + * @param {number} offsetY + * @param {number} offsetY + * @param {Object.<string, ?>} event + * Object which may contain the properties shiftKey, ctrlKey, altKey, + * metaKey, accessKey, button, type, axis, delta, and hasPixels. + * @param {Window=} window + * Window object. Defaults to the current window. + */ +event.synthesizeMouseScroll = function ( + target, offsetX, offsetY, ev, window = undefined) { + + let domutils = getDOMWindowUtils(window); + + // see nsMouseScrollFlags in nsGUIEvent.h + const kIsVertical = 0x02; + const kIsHorizontal = 0x04; + const kHasPixels = 0x08; + const kIsMomentum = 0x40; + + let button = ev.button || 0; + let modifiers = event.parseModifiers_(ev); + + let rect = target.getBoundingClientRect(); + let left = rect.left; + let top = rect.top; + + let type = (("type" in ev) && ev.type) || "DOMMouseScroll"; + let axis = ev.axis || "vertical"; + let scrollFlags = (axis == "horizontal") ? kIsHorizontal : kIsVertical; + if (ev.hasPixels) { + scrollFlags |= kHasPixels; + } + if (ev.isMomentum) { + scrollFlags |= kIsMomentum; + } + + domutils.sendMouseScrollEvent( + type, + left + offsetX, + top + offsetY, + button, + scrollFlags, + ev.delta, + modifiers); +}; + +function computeKeyCodeFromChar_(char) { + if (char.length != 1) { + return 0; + } + + if (char >= "a" && char <= "z") { + return Ci.nsIDOMKeyEvent.DOM_VK_A + char.charCodeAt(0) - "a".charCodeAt(0); + } + if (char >= "A" && char <= "Z") { + return Ci.nsIDOMKeyEvent.DOM_VK_A + char.charCodeAt(0) - "A".charCodeAt(0); + } + if (char >= "0" && char <= "9") { + return Ci.nsIDOMKeyEvent.DOM_VK_0 + char.charCodeAt(0) - "0".charCodeAt(0); + } + + // returns US keyboard layout's keycode + switch (char) { + case "~": + case "`": + return Ci.nsIDOMKeyEvent.DOM_VK_BACK_QUOTE; + + case "!": + return Ci.nsIDOMKeyEvent.DOM_VK_1; + + case "@": + return Ci.nsIDOMKeyEvent.DOM_VK_2; + + case "#": + return Ci.nsIDOMKeyEvent.DOM_VK_3; + + case "$": + return Ci.nsIDOMKeyEvent.DOM_VK_4; + + case "%": + return Ci.nsIDOMKeyEvent.DOM_VK_5; + + case "^": + return Ci.nsIDOMKeyEvent.DOM_VK_6; + + case "&": + return Ci.nsIDOMKeyEvent.DOM_VK_7; + + case "*": + return Ci.nsIDOMKeyEvent.DOM_VK_8; + + case "(": + return Ci.nsIDOMKeyEvent.DOM_VK_9; + + case ")": + return Ci.nsIDOMKeyEvent.DOM_VK_0; + + case "-": + case "_": + return Ci.nsIDOMKeyEvent.DOM_VK_SUBTRACT; + + case "+": + case "=": + return Ci.nsIDOMKeyEvent.DOM_VK_EQUALS; + + case "{": + case "[": + return Ci.nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET; + + case "}": + case "]": + return Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET; + + case "|": + case "\\": + return Ci.nsIDOMKeyEvent.DOM_VK_BACK_SLASH; + + case ":": + case ";": + return Ci.nsIDOMKeyEvent.DOM_VK_SEMICOLON; + + case "'": + case "\"": + return Ci.nsIDOMKeyEvent.DOM_VK_QUOTE; + + case "<": + case ",": + return Ci.nsIDOMKeyEvent.DOM_VK_COMMA; + + case ">": + case ".": + return Ci.nsIDOMKeyEvent.DOM_VK_PERIOD; + + case "?": + case "/": + return Ci.nsIDOMKeyEvent.DOM_VK_SLASH; + + case "\n": + return Ci.nsIDOMKeyEvent.DOM_VK_RETURN; + + default: + return 0; + } +} + +/** + * Returns true if the given key should cause keypress event when widget + * handles the native key event. Otherwise, false. + * + * The key code should be one of consts of nsIDOMKeyEvent.DOM_VK_*, + * or a key name begins with "VK_", or a character. + */ +event.isKeypressFiredKey = function (key) { + if (typeof key == "string") { + if (key.indexOf("VK_") === 0) { + key = Ci.nsIDOMKeyEvent["DOM_" + key]; + if (!key) { + throw new TypeError("Unknown key: " + key); + } + + // if key generates a character, it must cause a keypress event + } else { + return true; + } + } + + switch (key) { + case Ci.nsIDOMKeyEvent.DOM_VK_SHIFT: + case Ci.nsIDOMKeyEvent.DOM_VK_CONTROL: + case Ci.nsIDOMKeyEvent.DOM_VK_ALT: + case Ci.nsIDOMKeyEvent.DOM_VK_CAPS_LOCK: + case Ci.nsIDOMKeyEvent.DOM_VK_NUM_LOCK: + case Ci.nsIDOMKeyEvent.DOM_VK_SCROLL_LOCK: + case Ci.nsIDOMKeyEvent.DOM_VK_META: + return false; + + default: + return true; + } +}; + +/** + * Synthesise a key event. + * + * It is targeted at whatever would be targeted by an actual keypress + * by the user, typically the focused element. + * + * @param {string} key + * Key to synthesise. Should either be a character or a key code + * starting with "VK_" such as VK_RETURN, or a normalized key value. + * @param {Object.<string, ?>} event + * Object which may contain the properties shiftKey, ctrlKey, altKey, + * metaKey, accessKey, type. If the type is specified (keydown or keyup), + * a key event of that type is fired. Otherwise, a keydown, a keypress, + * and then a keyup event are fired in sequence. + * @param {Window=} window + * Window object. Defaults to the current window. + * + * @throws {TypeError} + * If unknown key. + */ +event.synthesizeKey = function (key, event, win = undefined) +{ + var TIP = getTIP_(win); + if (!TIP) { + return; + } + var KeyboardEvent = getKeyboardEvent_(win); + var modifiers = emulateToActivateModifiers_(TIP, event, win); + var keyEventDict = createKeyboardEventDictionary_(key, event, win); + var keyEvent = new KeyboardEvent("", keyEventDict.dictionary); + var dispatchKeydown = + !("type" in event) || event.type === "keydown" || !event.type; + var dispatchKeyup = + !("type" in event) || event.type === "keyup" || !event.type; + + try { + if (dispatchKeydown) { + TIP.keydown(keyEvent, keyEventDict.flags); + if ("repeat" in event && event.repeat > 1) { + keyEventDict.dictionary.repeat = true; + var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary); + for (var i = 1; i < event.repeat; i++) { + TIP.keydown(repeatedKeyEvent, keyEventDict.flags); + } + } + } + if (dispatchKeyup) { + TIP.keyup(keyEvent, keyEventDict.flags); + } + } finally { + emulateToInactivateModifiers_(TIP, modifiers, win); + } +}; + +var TIPMap = new WeakMap(); + +function getTIP_(win, callback) +{ + if (!win) { + win = window; + } + var tip; + if (TIPMap.has(win)) { + tip = TIPMap.get(win); + } else { + tip = + Cc["@mozilla.org/text-input-processor;1"]. + createInstance(Ci.nsITextInputProcessor); + TIPMap.set(win, tip); + } + if (!tip.beginInputTransactionForTests(win, callback)) { + tip = null; + TIPMap.delete(win); + } + return tip; +} + +function getKeyboardEvent_(win = 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 win.KeyboardEvent; +} + +function createKeyboardEventDictionary_(key, keyEvent, win = window) { + var result = { dictionary: null, flags: 0 }; + var keyCodeIsDefined = "keyCode" in keyEvent; + var keyCode = + (keyCodeIsDefined && keyEvent.keyCode >= 0 && keyEvent.keyCode <= 255) ? + keyEvent.keyCode : 0; + var keyName = "Unidentified"; + if (key.indexOf("KEY_") == 0) { + keyName = key.substr("KEY_".length); + result.flags |= Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; + } else if (key.indexOf("VK_") == 0) { + keyCode = Ci.nsIDOMKeyEvent["DOM_" + key]; + if (!keyCode) { + throw "Unknown key: " + key; + } + keyName = guessKeyNameFromKeyCode_(keyCode, win); + result.flags |= Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; + } else if (key != "") { + keyName = key; + if (!keyCodeIsDefined) { + keyCode = computeKeyCodeFromChar_(key.charAt(0)); + } + if (!keyCode) { + result.flags |= Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO; + } + // keyName was already determined in keyEvent so no fall-back needed + if (!("key" in keyEvent && keyName == keyEvent.key)) { + result.flags |= Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY; + } + } + var locationIsDefined = "location" in keyEvent; + if (locationIsDefined && keyEvent.location === 0) { + result.flags |= Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD; + } + result.dictionary = { + key: keyName, + code: "code" in keyEvent ? keyEvent.code : "", + location: locationIsDefined ? keyEvent.location : 0, + repeat: "repeat" in keyEvent ? keyEvent.repeat === true : false, + keyCode: keyCode, + }; + return result; +} + +function emulateToActivateModifiers_(TIP, keyEvent, win = window) +{ + if (!keyEvent) { + return null; + } + var KeyboardEvent = getKeyboardEvent_(win); + var navigator = getNavigator_(win); + + 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: isMac_(win) ? "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 (!keyEvent[modifiers.normal[i].attr]) { + continue; + } + if (TIP.getModifierState(modifiers.normal[i].key)) { + continue; // already activated. + } + var event = new KeyboardEvent("", { key: modifiers.normal[i].key }); + TIP.keydown(event, + TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + modifiers.normal[i].activated = true; + } + for (var i = 0; i < modifiers.lockable.length; i++) { + if (!keyEvent[modifiers.lockable[i].attr]) { + continue; + } + if (TIP.getModifierState(modifiers.lockable[i].key)) { + continue; // already activated. + } + var event = new KeyboardEvent("", { key: modifiers.lockable[i].key }); + TIP.keydown(event, + TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + TIP.keyup(event, + TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + modifiers.lockable[i].activated = true; + } + return modifiers; +} + +function emulateToInactivateModifiers_(TIP, modifiers, win = window) +{ + if (!modifiers) { + return; + } + var KeyboardEvent = getKeyboardEvent_(win); + for (var i = 0; i < modifiers.normal.length; i++) { + if (!modifiers.normal[i].activated) { + continue; + } + var event = new KeyboardEvent("", { key: modifiers.normal[i].key }); + TIP.keyup(event, + TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + } + for (var i = 0; i < modifiers.lockable.length; i++) { + if (!modifiers.lockable[i].activated) { + continue; + } + if (!TIP.getModifierState(modifiers.lockable[i].key)) { + continue; // who already inactivated this? + } + var event = new KeyboardEvent("", { key: modifiers.lockable[i].key }); + TIP.keydown(event, + TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + TIP.keyup(event, + TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); + } +} + +function getNavigator_(win = window) +{ + if (typeof navigator != "undefined") { + return navigator; + } + return win.navigator; +} + +function isMac_(win = window) { + if (win) { + try { + return win.navigator.platform.indexOf("Mac") > -1; + } catch (ex) {} + } + return navigator.platform.indexOf("Mac") > -1; +} + +function guessKeyNameFromKeyCode_(aKeyCode, win = window) +{ + var KeyboardEvent = getKeyboardEvent_(win); + 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"; + } +} + +/** + * Indicate that an event with an original target and type is expected + * to be fired, or not expected to be fired. + */ +function expectEvent_(expectedTarget, expectedEvent, testName) { + if (!expectedTarget || !expectedEvent) { + return null; + } + + seenEvent = false; + + let type; + if (expectedEvent.charAt(0) == "!") { + type = expectedEvent.substring(1); + } else { + type = expectedEvent; + } + + let handler = ev => { + let pass = (!seenEvent && ev.originalTarget == expectedTarget && ev.type == type); + is(pass, true, `${testName} ${type} event target ${seenEvent ? "twice" : ""}`); + seenEvent = true; + }; + + expectedTarget.addEventListener(type, handler, false); + return handler; +} + +/** + * Check if the event was fired or not. The provided event handler will + * be removed. + */ +function checkExpectedEvent_( + expectedTarget, expectedEvent, eventHandler, testName) { + + if (eventHandler) { + let expectEvent = (expectedEvent.charAt(0) != "!"); + let type = expectEvent; + if (!type) { + type = expectedEvent.substring(1); + } + expectedTarget.removeEventListener(type, eventHandler, false); + + let desc = `${type} event`; + if (!expectEvent) { + desc += " not"; + } + is(seenEvent, expectEvent, `${testName} ${desc} fired`); + } + + seenEvent = false; +} + +/** + * Similar to event.synthesizeMouse except that a test is performed to + * see if an event is fired at the right target as a result. + * + * 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. + * + * @param {Element} target + * Synthesise the mouse event on this target. + * @param {number} offsetX + * Horizontal offset from the target's bounding box. + * @param {number} offsetY + * Vertical offset from the target's bounding box. + * @param {Object.<string, ?>} ev + * Object which may contain the properties shiftKey, ctrlKey, altKey, + * metaKey, accessKey, type. + * @param {Element} expectedTarget + * Expected originalTarget of the event. + * @param {DOMEvent} expectedEvent + * Expected type of the event, such as "select". + * @param {string} testName + * Test name when outputing results. + * @param {Window=} window + * Window object. Defaults to the current window. + */ +event.synthesizeMouseExpectEvent = function ( + target, offsetX, offsetY, ev, expectedTarget, expectedEvent, + testName, window = undefined) { + + let eventHandler = expectEvent_( + expectedTarget, + expectedEvent, + testName); + event.synthesizeMouse(target, offsetX, offsetY, ev, window); + checkExpectedEvent_( + expectedTarget, + expectedEvent, + eventHandler, + testName); +}; + +/** + * Similar to synthesizeKey except that a test is performed to see if + * an event is fired at the right target as a result. + * + * @param {string} key + * Key to synthesise. + * @param {Object.<string, ?>} ev + * Object which may contain the properties shiftKey, ctrlKey, altKey, + * metaKey, accessKey, type. + * @param {Element} expectedTarget + * Expected originalTarget of the event. + * @param {DOMEvent} expectedEvent + * Expected type of the event, such as "select". + * @param {string} testName + * Test name when outputing results + * @param {Window=} window + * Window object. Defaults to the current window. + * + * 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. + */ +event.synthesizeKeyExpectEvent = function ( + key, ev, expectedTarget, expectedEvent, testName, + window = undefined) { + + let eventHandler = expectEvent_( + expectedTarget, + expectedEvent, + testName); + event.synthesizeKey(key, ev, window); + checkExpectedEvent_( + expectedTarget, + expectedEvent, + eventHandler, + testName); +}; + +/** + * Synthesize a composition event. + * + * @param {DOMEvent} ev + * The composition event information. This must have |type| + * member. The value must be "compositionstart", "compositionend" or + * "compositionupdate". 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 would be ignored if the + * event type were "compositionstart". + * @param {Window=} window + * Window object. Defaults to the current window. + */ +event.synthesizeComposition = function (ev, window = undefined) { + let domutils = getDOMWindowUtils(window); + domutils.sendCompositionEvent(ev.type, ev.data || "", ev.locale || ""); +}; + +/** + * Synthesize a text event. + * + * The text 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. + * + * ev + * +-- composition + * | +-- string + * | +-- clauses[] + * | +-- length + * | +-- attr + * +-- caret + * +-- start + * +-- length + * + * 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 nsIDOMWindowUtils.COMPOSITION_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|. Its 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. + * + * @param {Object.<string, ?>} ev + * The text event's information, + * @param {Window=} window + * Window object. Defaults to the current window. + */ +event.synthesizeText = function (ev, window = undefined) { + let domutils = getDOMWindowUtils(window); + + if (!ev.composition || + !ev.composition.clauses || + !ev.composition.clauses[0]) { + return; + } + + let firstClauseLength = ev.composition.clauses[0].length; + let firstClauseAttr = ev.composition.clauses[0].attr; + let secondClauseLength = 0; + let secondClauseAttr = 0; + let thirdClauseLength = 0; + let thirdClauseAttr = 0; + if (ev.composition.clauses[1]) { + secondClauseLength = ev.composition.clauses[1].length; + secondClauseAttr = ev.composition.clauses[1].attr; + if (event.composition.clauses[2]) { + thirdClauseLength = ev.composition.clauses[2].length; + thirdClauseAttr = ev.composition.clauses[2].attr; + } + } + + let caretStart = -1; + let caretLength = 0; + if (event.caret) { + caretStart = ev.caret.start; + caretLength = ev.caret.length; + } + + domutils.sendTextEvent( + ev.composition.string, + firstClauseLength, + firstClauseAttr, + secondClauseLength, + secondClauseAttr, + thirdClauseLength, + thirdClauseAttr, + caretStart, + caretLength); +}; + +/** + * Synthesize a query selected text event. + * + * @param {Window=} + * Window object. Defaults to the current window. + * + * @return {(nsIQueryContentEventResult|null)} + * Event's result, or null if it failed. + */ +event.synthesizeQuerySelectedText = function (window = undefined) { + let domutils = getDOMWindowUtils(window); + return domutils.sendQueryContentEvent( + domutils.QUERY_SELECTED_TEXT, 0, 0, 0, 0); +}; + +/** + * Synthesize a selection set event. + * + * @param {number} offset + * Character offset. 0 means the first character in the selection + * root. + * @param {number} length + * Length of the text. If the length is too long, the extra length + * is ignored. + * @param {boolean} reverse + * If true, the selection is from |aOffset + aLength| to |aOffset|. + * Otherwise, from |aOffset| to |aOffset + aLength|. + * @param {Window=} window + * Window object. Defaults to the current window. + * + * @return True, if succeeded. Otherwise false. + */ +event.synthesizeSelectionSet = function ( + offset, length, reverse, window = undefined) { + let domutils = getDOMWindowUtils(window); + return domutils.sendSelectionSetEvent(offset, length, reverse); +}; + +const KEYCODES_LOOKUP = { + "VK_SHIFT": "shiftKey", + "VK_CONTROL": "ctrlKey", + "VK_ALT": "altKey", + "VK_META": "metaKey", +}; + +const VIRTUAL_KEYCODE_LOOKUP = { + "\uE001": "VK_CANCEL", + "\uE002": "VK_HELP", + "\uE003": "VK_BACK_SPACE", + "\uE004": "VK_TAB", + "\uE005": "VK_CLEAR", + "\uE006": "VK_RETURN", + "\uE007": "VK_RETURN", + "\uE008": "VK_SHIFT", + "\uE009": "VK_CONTROL", + "\uE00A": "VK_ALT", + "\uE03D": "VK_META", + "\uE00B": "VK_PAUSE", + "\uE00C": "VK_ESCAPE", + "\uE00D": "VK_SPACE", // printable + "\uE00E": "VK_PAGE_UP", + "\uE00F": "VK_PAGE_DOWN", + "\uE010": "VK_END", + "\uE011": "VK_HOME", + "\uE012": "VK_LEFT", + "\uE013": "VK_UP", + "\uE014": "VK_RIGHT", + "\uE015": "VK_DOWN", + "\uE016": "VK_INSERT", + "\uE017": "VK_DELETE", + "\uE018": "VK_SEMICOLON", + "\uE019": "VK_EQUALS", + "\uE01A": "VK_NUMPAD0", + "\uE01B": "VK_NUMPAD1", + "\uE01C": "VK_NUMPAD2", + "\uE01D": "VK_NUMPAD3", + "\uE01E": "VK_NUMPAD4", + "\uE01F": "VK_NUMPAD5", + "\uE020": "VK_NUMPAD6", + "\uE021": "VK_NUMPAD7", + "\uE022": "VK_NUMPAD8", + "\uE023": "VK_NUMPAD9", + "\uE024": "VK_MULTIPLY", + "\uE025": "VK_ADD", + "\uE026": "VK_SEPARATOR", + "\uE027": "VK_SUBTRACT", + "\uE028": "VK_DECIMAL", + "\uE029": "VK_DIVIDE", + "\uE031": "VK_F1", + "\uE032": "VK_F2", + "\uE033": "VK_F3", + "\uE034": "VK_F4", + "\uE035": "VK_F5", + "\uE036": "VK_F6", + "\uE037": "VK_F7", + "\uE038": "VK_F8", + "\uE039": "VK_F9", + "\uE03A": "VK_F10", + "\uE03B": "VK_F11", + "\uE03C": "VK_F12", +}; + +function getKeyCode(c) { + if (c in VIRTUAL_KEYCODE_LOOKUP) { + return VIRTUAL_KEYCODE_LOOKUP[c]; + } + return c; +} + +event.sendKeyDown = function (keyToSend, modifiers, document) { + modifiers.type = "keydown"; + event.sendSingleKey(keyToSend, modifiers, document); + // TODO This doesn't do anything since |synthesizeKeyEvent| ignores explicit + // keypress request, and instead figures out itself when to send keypress + if (["VK_SHIFT", "VK_CONTROL", "VK_ALT", "VK_META"].indexOf(getKeyCode(keyToSend)) < 0) { + modifiers.type = "keypress"; + event.sendSingleKey(keyToSend, modifiers, document); + } + delete modifiers.type; +}; + +event.sendKeyUp = function (keyToSend, modifiers, window = undefined) { + modifiers.type = "keyup"; + event.sendSingleKey(keyToSend, modifiers, window); + delete modifiers.type; +}; + +/** + * Synthesize a key event for a single key. + * + * @param {string} keyToSend + * Code point or normalized key value + * @param {?} modifiers + * Object with properties used in KeyboardEvent (shiftkey, repeat, ...) + * as well as, the event |type| such as keydown. All properties are optional. + * @param {Window=} window + * Window object. If |window| is undefined, the event is synthesized in + * current window. + */ +event.sendSingleKey = function (keyToSend, modifiers, window = undefined) { + let keyCode = getKeyCode(keyToSend); + if (keyCode in KEYCODES_LOOKUP) { + // We assume that if |keyToSend| is a raw code point (like "\uE009") then + // |modifiers| does not already have correct value for corresponding + // |modName| attribute (like ctrlKey), so that value needs to be flipped + let modName = KEYCODES_LOOKUP[keyCode]; + modifiers[modName] = !modifiers[modName]; + } else if (modifiers.shiftKey && keyCode != "Shift") { + keyCode = keyCode.toUpperCase(); + } + event.synthesizeKey(keyCode, modifiers, window); +}; + +/** + * Focus element and, if a textual input field and no previous selection + * state exists, move the caret to the end of the input field. + * + * @param {Element} element + * Element to focus. + */ +function focusElement(element) { + let t = element.type; + if (t && (t == "text" || t == "textarea")) { + if (element.selectionEnd == 0) { + let len = element.value.length; + element.setSelectionRange(len, len); + } + } + element.focus(); +} + +/** + * @param {Array.<string>} keySequence + * @param {Element} element + * @param {Object.<string, boolean>=} opts + * @param {Window=} window + */ +event.sendKeysToElement = function ( + keySequence, el, opts = {}, window = undefined) { + + if (opts.ignoreVisibility || element.isVisible(el)) { + focusElement(el); + + // make Object.<modifier, false> map + let modifiers = Object.create(event.Modifiers); + for (let modifier in event.Modifiers) { + modifiers[modifier] = false; + } + + let value = keySequence.join(""); + for (let i = 0; i < value.length; i++) { + let c = value.charAt(i); + event.sendSingleKey(c, modifiers, window); + } + + } else { + throw new ElementNotInteractableError("Element is not visible"); + } +}; + +event.sendEvent = function (eventType, el, modifiers = {}, opts = {}) { + opts.canBubble = opts.canBubble || true; + + let doc = el.ownerDocument || el.document; + let ev = doc.createEvent("Event"); + + ev.shiftKey = modifiers["shift"]; + ev.metaKey = modifiers["meta"]; + ev.altKey = modifiers["alt"]; + ev.ctrlKey = modifiers["ctrl"]; + + ev.initEvent(eventType, opts.canBubble, true); + el.dispatchEvent(ev); +}; + +event.focus = function (el, opts = {}) { + opts.canBubble = opts.canBubble || true; + let doc = el.ownerDocument || el.document; + let win = doc.defaultView; + + let ev = new win.FocusEvent(el); + ev.initEvent("focus", opts.canBubble, true); + el.dispatchEvent(ev); +}; + +event.mouseover = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("mouseover", el, modifiers, opts); +}; + +event.mousemove = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("mousemove", el, modifiers, opts); +}; + +event.mousedown = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("mousedown", el, modifiers, opts); +}; + +event.mouseup = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("mouseup", el, modifiers, opts); +}; + +event.click = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("click", el, modifiers, opts); +}; + +event.change = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("change", el, modifiers, opts); +}; + +event.input = function (el, modifiers = {}, opts = {}) { + return event.sendEvent("input", el, modifiers, opts); +}; |