/* 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); };