gecko-dev/testing/marionette/event.js

1421 строка
40 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/** Provides functionality for creating and sending DOM events. */
this.event = {};
"use strict";
/* global content, is */
const {interfaces: Ci, utils: Cu, classes: Cc} = Components;
Cu.import("chrome://marionette/content/element.js");
const {ElementNotInteractableError} =
Cu.import("chrome://marionette/content/error.js", {});
this.EXPORTED_SYMBOLS = ["event"];
// 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);
}
/** @namespace */
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,
};
event.MouseButton = {
isPrimary(button) {
return button === 0;
},
isAuxiliary(button) {
return button === 1;
},
isSecondary(button) {
return button === 2;
},
};
/**
* 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.ownerGlobal;
}
let ev = window.document.createEvent("MouseEvent");
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;
if ("isWidgetEventSynthesized" in opts) {
isWidgetEventSynthesized = opts.isWidgetEventSynthesized;
} else {
isWidgetEventSynthesized = false;
}
let buttons;
if ("buttons" in opts) {
buttons = opts.buttons;
} else {
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);
};
/* eslint-disable */
function computeKeyCodeFromChar_(char) {
if (char.length != 1) {
return 0;
}
if (char in VIRTUAL_KEYCODE_LOOKUP) {
return Ci.nsIDOMKeyEvent["DOM_" + VIRTUAL_KEYCODE_LOOKUP[char]];
}
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;
}
}
/* eslint-enable */
/**
* 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) {
let TIP = getTIP_(win);
if (!TIP) {
return;
}
let KeyboardEvent = getKeyboardEvent_(win);
let modifiers = emulateToActivateModifiers_(TIP, event, win);
let keyEventDict = createKeyboardEventDictionary_(key, event, win);
let keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
let dispatchKeydown =
!("type" in event) || event.type === "keydown" || !event.type;
let 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;
let repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary);
for (let i = 1; i < event.repeat; i++) {
TIP.keydown(repeatedKeyEvent, keyEventDict.flags);
}
}
}
if (dispatchKeyup) {
TIP.keyup(keyEvent, keyEventDict.flags);
}
} finally {
emulateToInactivateModifiers_(TIP, modifiers, win);
}
};
const TIPMap = new WeakMap();
function getTIP_(win, callback) {
if (!win) {
win = window;
}
let 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) {
let result = {dictionary: null, flags: 0};
let keyCodeIsDefined = "keyCode" in keyEvent &&
keyEvent.keyCode != undefined;
let keyCode =
(keyCodeIsDefined && keyEvent.keyCode >= 0 && keyEvent.keyCode <= 255) ?
keyEvent.keyCode : 0;
let 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);
if (!isPrintable(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;
}
// only force printable if "raw character" and event key match, like "a"
if (!("key" in keyEvent && key != keyEvent.key)) {
result.flags |= Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
}
}
let locationIsDefined = "location" in keyEvent;
if (locationIsDefined && keyEvent.location === 0) {
result.flags |= Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD;
}
result.dictionary = {
key: "key" in keyEvent ? keyEvent.key : keyName,
code: "code" in keyEvent ? keyEvent.code : "",
location: locationIsDefined ? keyEvent.location : 0,
repeat: "repeat" in keyEvent ? keyEvent.repeat === true : false,
keyCode,
};
return result;
}
function emulateToActivateModifiers_(TIP, keyEvent, win = window) {
if (!keyEvent) {
return null;
}
let KeyboardEvent = getKeyboardEvent_(win);
let 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 (let i = 0; i < modifiers.normal.length; i++) {
if (!keyEvent[modifiers.normal[i].attr]) {
continue;
}
if (TIP.getModifierState(modifiers.normal[i].key)) {
continue; // already activated.
}
let 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 (let j = 0; j < modifiers.lockable.length; j++) {
if (!keyEvent[modifiers.lockable[j].attr]) {
continue;
}
if (TIP.getModifierState(modifiers.lockable[j].key)) {
continue; // already activated.
}
let event = new KeyboardEvent("", {key: modifiers.lockable[j].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[j].activated = true;
}
return modifiers;
}
function emulateToInactivateModifiers_(TIP, modifiers, win = window) {
if (!modifiers) {
return;
}
let KeyboardEvent = getKeyboardEvent_(win);
for (let i = 0; i < modifiers.normal.length; i++) {
if (!modifiers.normal[i].activated) {
continue;
}
let event = new KeyboardEvent("", {key: modifiers.normal[i].key});
TIP.keyup(event,
TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
}
for (let j = 0; j < modifiers.lockable.length; j++) {
if (!modifiers.lockable[j].activated) {
continue;
}
if (!TIP.getModifierState(modifiers.lockable[j].key)) {
continue; // who already inactivated this?
}
let event = new KeyboardEvent("", {key: modifiers.lockable[j].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 isMac_(win = window) {
if (win) {
try {
return win.navigator.platform.indexOf("Mac") > -1;
} catch (ex) {}
}
return navigator.platform.indexOf("Mac") > -1;
}
/* eslint-disable */
function guessKeyNameFromKeyCode_(aKeyCode, win = window) {
let 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";
}
}
/* eslint-enable */
/**
* Indicate that an event with an original target and type is expected
* to be fired, or not expected to be fired.
*/
/* eslint-disable */
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);
return handler;
}
/* eslint-enable */
/**
* 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);
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;
}
function isPrintable(c, win = window) {
let KeyboardEvent = getKeyboardEvent_(win);
let NON_PRINT_KEYS = [
KeyboardEvent.DOM_VK_CANCEL,
KeyboardEvent.DOM_VK_HELP,
KeyboardEvent.DOM_VK_BACK_SPACE,
KeyboardEvent.DOM_VK_TAB,
KeyboardEvent.DOM_VK_CLEAR,
KeyboardEvent.DOM_VK_SHIFT,
KeyboardEvent.DOM_VK_CONTROL,
KeyboardEvent.DOM_VK_ALT,
KeyboardEvent.DOM_VK_PAUSE,
KeyboardEvent.DOM_VK_EISU,
KeyboardEvent.DOM_VK_ESCAPE,
KeyboardEvent.DOM_VK_CONVERT,
KeyboardEvent.DOM_VK_NONCONVERT,
KeyboardEvent.DOM_VK_ACCEPT,
KeyboardEvent.DOM_VK_MODECHANGE,
KeyboardEvent.DOM_VK_PAGE_UP,
KeyboardEvent.DOM_VK_PAGE_DOWN,
KeyboardEvent.DOM_VK_END,
KeyboardEvent.DOM_VK_HOME,
KeyboardEvent.DOM_VK_LEFT,
KeyboardEvent.DOM_VK_UP,
KeyboardEvent.DOM_VK_RIGHT,
KeyboardEvent.DOM_VK_DOWN,
KeyboardEvent.DOM_VK_SELECT,
KeyboardEvent.DOM_VK_PRINT,
KeyboardEvent.DOM_VK_EXECUTE,
KeyboardEvent.DOM_VK_PRINTSCREEN,
KeyboardEvent.DOM_VK_INSERT,
KeyboardEvent.DOM_VK_DELETE,
KeyboardEvent.DOM_VK_WIN,
KeyboardEvent.DOM_VK_CONTEXT_MENU,
KeyboardEvent.DOM_VK_SLEEP,
KeyboardEvent.DOM_VK_F1,
KeyboardEvent.DOM_VK_F2,
KeyboardEvent.DOM_VK_F3,
KeyboardEvent.DOM_VK_F4,
KeyboardEvent.DOM_VK_F5,
KeyboardEvent.DOM_VK_F6,
KeyboardEvent.DOM_VK_F7,
KeyboardEvent.DOM_VK_F8,
KeyboardEvent.DOM_VK_F9,
KeyboardEvent.DOM_VK_F10,
KeyboardEvent.DOM_VK_F11,
KeyboardEvent.DOM_VK_F12,
KeyboardEvent.DOM_VK_F13,
KeyboardEvent.DOM_VK_F14,
KeyboardEvent.DOM_VK_F15,
KeyboardEvent.DOM_VK_F16,
KeyboardEvent.DOM_VK_F17,
KeyboardEvent.DOM_VK_F18,
KeyboardEvent.DOM_VK_F19,
KeyboardEvent.DOM_VK_F20,
KeyboardEvent.DOM_VK_F21,
KeyboardEvent.DOM_VK_F22,
KeyboardEvent.DOM_VK_F23,
KeyboardEvent.DOM_VK_F24,
KeyboardEvent.DOM_VK_NUM_LOCK,
KeyboardEvent.DOM_VK_SCROLL_LOCK,
KeyboardEvent.DOM_VK_VOLUME_MUTE,
KeyboardEvent.DOM_VK_VOLUME_DOWN,
KeyboardEvent.DOM_VK_VOLUME_UP,
KeyboardEvent.DOM_VK_META,
KeyboardEvent.DOM_VK_ALTGR,
KeyboardEvent.DOM_VK_ATTN,
KeyboardEvent.DOM_VK_CRSEL,
KeyboardEvent.DOM_VK_EXSEL,
KeyboardEvent.DOM_VK_EREOF,
KeyboardEvent.DOM_VK_PLAY,
KeyboardEvent.DOM_VK_RETURN,
];
return !(NON_PRINT_KEYS.includes(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 {Object.<string, boolean>} 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 keyName = getKeyCode(keyToSend);
if (keyName 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[keyName];
modifiers[modName] = !modifiers[modName];
} else if (modifiers.shiftKey && keyName != "Shift") {
keyName = keyName.toUpperCase();
}
event.synthesizeKey(keyName, 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 {string} keyString
* @param {Element} element
* @param {Object.<string, boolean>=} opts
* @param {Window=} window
*/
event.sendKeysToElement = function(
keyString, 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;
}
for (let i = 0; i < keyString.length; i++) {
let c = keyString.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);
};