Bug 1093153 - Introduce 'BrowserTestUtils#synthesizeKey', 'BrowserTestUtils#synthesizeComposition' and 'BrowserTestUtils#synthesizeCompositionChange' to allow mochitests to remotely invoke EventUtils' text composition utilities. Changes to EventUtils include 1) removed dependency on the 'navigator' object when 'nsIXULRuntime' is available and 2) make '_getKeyboardEvent' more robust when used in frame scripts. r=Enn, a=test-only

This commit is contained in:
Mike de Boer 2016-03-15 11:09:31 +01:00
parent 40e492b522
commit b6b5962e27
3 changed files with 164 additions and 17 deletions

View File

@ -38,6 +38,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
Cu.permitCPOWsInScope(this); Cu.permitCPOWsInScope(this);
var gSendCharCount = 0; var gSendCharCount = 0;
var gSynthesizeKeyCount = 0;
var gSynthesizeCompositionCount = 0;
var gSynthesizeCompositionChangeCount = 0;
this.BrowserTestUtils = { this.BrowserTestUtils = {
/** /**
@ -827,7 +830,7 @@ this.BrowserTestUtils = {
/** /**
* Version of EventUtils' `sendChar` function; it will synthesize a keypress * Version of EventUtils' `sendChar` function; it will synthesize a keypress
* event in a child process and returns a Promise that will result when the * event in a child process and returns a Promise that will resolve when the
* event was fired. Instead of a Window, a Browser object is required to be * event was fired. Instead of a Window, a Browser object is required to be
* passed to this function. * passed to this function.
* *
@ -849,7 +852,7 @@ this.BrowserTestUtils = {
return; return;
mm.removeMessageListener("Test:SendCharDone", charMsg); mm.removeMessageListener("Test:SendCharDone", charMsg);
resolve(message.data.sendCharResult); resolve(message.data.result);
}); });
mm.sendAsyncMessage("Test:SendChar", { mm.sendAsyncMessage("Test:SendChar", {
@ -859,6 +862,99 @@ this.BrowserTestUtils = {
}); });
}, },
/**
* Version of EventUtils' `synthesizeKey` function; it will synthesize a key
* event in a child process and returns a Promise that will resolve when the
* event was fired. Instead of a Window, a Browser object is required to be
* passed to this function.
*
* @param {String} key
* See the documentation available for EventUtils#synthesizeKey.
* @param {Object} event
* See the documentation available for EventUtils#synthesizeKey.
* @param {Browser} browser
* Browser element, must not be null.
*
* @returns {Promise}
*/
synthesizeKey(key, event, browser) {
return new Promise(resolve => {
let seq = ++gSynthesizeKeyCount;
let mm = browser.messageManager;
mm.addMessageListener("Test:SynthesizeKeyDone", function keyMsg(message) {
if (message.data.seq != seq)
return;
mm.removeMessageListener("Test:SynthesizeKeyDone", keyMsg);
resolve();
});
mm.sendAsyncMessage("Test:SynthesizeKey", { key, event, seq });
});
},
/**
* Version of EventUtils' `synthesizeComposition` function; it will synthesize
* a composition event in a child process and returns a Promise that will
* resolve when the event was fired. Instead of a Window, a Browser object is
* required to be passed to this function.
*
* @param {Object} event
* See the documentation available for EventUtils#synthesizeComposition.
* @param {Browser} browser
* Browser element, must not be null.
*
* @returns {Promise}
* @resolves False if the composition event could not be synthesized.
*/
synthesizeComposition(event, browser) {
return new Promise(resolve => {
let seq = ++gSynthesizeCompositionCount;
let mm = browser.messageManager;
mm.addMessageListener("Test:SynthesizeCompositionDone", function compMsg(message) {
if (message.data.seq != seq)
return;
mm.removeMessageListener("Test:SynthesizeCompositionDone", compMsg);
resolve(message.data.result);
});
mm.sendAsyncMessage("Test:SynthesizeComposition", { event, seq });
});
},
/**
* Version of EventUtils' `synthesizeCompositionChange` function; it will
* synthesize a compositionchange event in a child process and returns a
* Promise that will resolve when the event was fired. Instead of a Window, a
* Browser object is required to be passed to this function.
*
* @param {Object} event
* See the documentation available for EventUtils#synthesizeCompositionChange.
* @param {Browser} browser
* Browser element, must not be null.
*
* @returns {Promise}
*/
synthesizeCompositionChange(event, browser) {
return new Promise(resolve => {
let seq = ++gSynthesizeCompositionChangeCount;
let mm = browser.messageManager;
mm.addMessageListener("Test:SynthesizeCompositionChangeDone", function compMsg(message) {
if (message.data.seq != seq)
return;
mm.removeMessageListener("Test:SynthesizeCompositionChangeDone", compMsg);
resolve();
});
mm.sendAsyncMessage("Test:SynthesizeCompositionChange", { event, seq });
});
},
/** /**
* Will poll a condition function until it returns true. * Will poll a condition function until it returns true.
* *

View File

@ -58,8 +58,20 @@ addMessageListener("Test:SynthesizeMouse", (message) => {
addMessageListener("Test:SendChar", message => { addMessageListener("Test:SendChar", message => {
let result = EventUtils.sendChar(message.data.char, content); let result = EventUtils.sendChar(message.data.char, content);
sendAsyncMessage("Test:SendCharDone", { sendAsyncMessage("Test:SendCharDone", { result, seq: message.data.seq });
sendCharResult: result, });
seq: message.data.seq
}); addMessageListener("Test:SynthesizeKey", message => {
EventUtils.synthesizeKey(message.data.key, message.data.event || {}, content);
sendAsyncMessage("Test:SynthesizeKeyDone", { seq: message.data.seq });
});
addMessageListener("Test:SynthesizeComposition", message => {
let result = EventUtils.synthesizeComposition(message.data.event, content);
sendAsyncMessage("Test:SynthesizeCompositionDone", { result, seq: message.data.seq });
});
addMessageListener("Test:SynthesizeCompositionChange", message => {
EventUtils.synthesizeCompositionChange(message.data.event, content);
sendAsyncMessage("Test:SynthesizeCompositionChangeDone", { seq: message.data.seq });
}); });

View File

@ -43,6 +43,40 @@ window.__defineGetter__('_EU_Cu', function() {
return c.value && !c.writable ? Components.utils : SpecialPowers.Cu; return c.value && !c.writable ? Components.utils : SpecialPowers.Cu;
}); });
window.__defineGetter__("_EU_OS", function() {
delete this._EU_OS;
try {
this._EU_OS = this._EU_Cu.import("resource://gre/modules/AppConstants.jsm", {}).platform;
} catch (ex) {
this._EU_OS = null;
}
return this._EU_OS;
});
function _EU_isMac(aWindow = window) {
if (window._EU_OS) {
return window._EU_OS == "macosx";
}
if (aWindow) {
try {
return aWindow.navigator.platform.indexOf("Mac") > -1;
} catch (ex) {}
}
return navigator.platform.indexOf("Mac") > -1;
}
function _EU_isWin(aWindow = window) {
if (window._EU_OS) {
return window._EU_OS == "win";
}
if (aWindow) {
try {
return aWindow.navigator.platform.indexOf("Win") > -1;
} catch (ex) {}
}
return navigator.platform.indexOf("Win") > -1;
}
/** /**
* Send a mouse event to the node aTarget (aTarget can be an id, or an * Send a mouse event to the node aTarget (aTarget can be an id, or an
* actual node) . The "event" passed in to aEvent is just a JavaScript * actual node) . The "event" passed in to aEvent is just a JavaScript
@ -237,7 +271,7 @@ function _parseModifiers(aEvent, aWindow = window)
mval |= nsIDOMWindowUtils.MODIFIER_META; mval |= nsIDOMWindowUtils.MODIFIER_META;
} }
if (aEvent.accelKey) { if (aEvent.accelKey) {
mval |= (navigator.platform.indexOf("Mac") >= 0) ? mval |= _EU_isMac(aWindow) ?
nsIDOMWindowUtils.MODIFIER_META : nsIDOMWindowUtils.MODIFIER_CONTROL; nsIDOMWindowUtils.MODIFIER_META : nsIDOMWindowUtils.MODIFIER_CONTROL;
} }
if (aEvent.altGrKey) { if (aEvent.altGrKey) {
@ -789,16 +823,13 @@ function _parseNativeModifiers(aModifiers, aWindow = window)
} }
if (aModifiers.accelKey) { if (aModifiers.accelKey) {
modifiers |= modifiers |= _EU_isMac(aWindow) ? 0x00004000 : 0x00000400;
(navigator.platform.indexOf("Mac") == 0) ? 0x00004000 : 0x00000400;
} }
if (aModifiers.accelRightKey) { if (aModifiers.accelRightKey) {
modifiers |= modifiers |= _EU_isMac(aWindow) ? 0x00008000 : 0x00000800;
(navigator.platform.indexOf("Mac") == 0) ? 0x00008000 : 0x00000800;
} }
if (aModifiers.altGrKey) { if (aModifiers.altGrKey) {
modifiers |= modifiers |= _EU_isWin(aWindow) ? 0x00002800 : 0x00001000;
(navigator.platform.indexOf("Win") == 0) ? 0x00002800 : 0x00001000;
} }
return modifiers; return modifiers;
} }
@ -873,9 +904,9 @@ function synthesizeNativeKey(aKeyboardLayout, aNativeKeyCode, aModifiers,
} }
var navigator = _getNavigator(aWindow); var navigator = _getNavigator(aWindow);
var nativeKeyboardLayout = null; var nativeKeyboardLayout = null;
if (navigator.platform.indexOf("Mac") == 0) { if (_EU_isMac(aWindow)) {
nativeKeyboardLayout = aKeyboardLayout.Mac; nativeKeyboardLayout = aKeyboardLayout.Mac;
} else if (navigator.platform.indexOf("Win") == 0) { } else if (_EU_isWin(aWindow)) {
nativeKeyboardLayout = aKeyboardLayout.Win; nativeKeyboardLayout = aKeyboardLayout.Win;
} }
if (nativeKeyboardLayout === null) { if (nativeKeyboardLayout === null) {
@ -1063,7 +1094,15 @@ function _getTIP(aWindow, aCallback)
function _getKeyboardEvent(aWindow = window) function _getKeyboardEvent(aWindow = window)
{ {
if (typeof KeyboardEvent != "undefined") { if (typeof KeyboardEvent != "undefined") {
return KeyboardEvent; try {
// See if the object can be instantiated; sometimes this yields
// 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'.
new KeyboardEvent("", {});
return KeyboardEvent;
} catch (ex) {}
}
if (typeof content != "undefined" && ("KeyboardEvent" in content)) {
return content.KeyboardEvent;
} }
return aWindow.KeyboardEvent; return aWindow.KeyboardEvent;
} }
@ -1282,7 +1321,7 @@ function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window)
{ key: "OS", attr: "osKey" }, { key: "OS", attr: "osKey" },
{ key: "Shift", attr: "shiftKey" }, { key: "Shift", attr: "shiftKey" },
{ key: "Symbol", attr: "symbolKey" }, { key: "Symbol", attr: "symbolKey" },
{ key: (navigator.platform.indexOf("Mac") >= 0) ? "Meta" : "Control", { key: _EU_isMac(aWindow) ? "Meta" : "Control",
attr: "accelKey" }, attr: "accelKey" },
], ],
lockable: [ lockable: [