forked from mirrors/gecko-dev
A lone surrogate should not appear in `DOMString` at least when the attribute values of events because of ill-formed UTF-16 string. `TextEventDispatcher` does not handle surrogate pairs correctly. It should not split surrogate pairs when it sets `KeyboardEvent.key` value for avoiding the problem in some DOM API wrappers, e.g., Rust-running-as-wasm. On the other hand, `.charCode` is an unsigned long attribute and web apps may use `String.fromCharCode(event.charCode)` to convert the input to string, and unfortunately, `fromCharCode` does not support Unicode character code points over `0xFFFF`. Therefore, we may need to keep dispatching 2 `keypress` events per surrogate pair for the backward compatibility. Therefore, this patch creates 2 prefs. One is for using single-keypress event model and double-keypress event model. The other is for the latter, whether `.key` value never has ill-formed UTF-16 or it's allowed. If using the single-keypress event model --this is compatible with Safari and Chrome in non-Windows platforms--, one `keypress` event is dispatched for typing a surrogate pair. Then, its `.charCode` is over `0xFFFF` which can work with `String.fromCodePoint()` instead of `String.fromCharCode()` and `.key` value is set to simply the surrogate pair (i.e., its length is 2). If using the double-keypress event model and disallowing ill-formed UTF-16 --this is the new default behavior for both avoiding ill-formed UTF-16 string creation and keeping backward compatibility with not-maintained web apps using `String.fromCharCode`--, 2 `keypress` events are dispatched. `.charCode` for first one is the code of the high-surrogate, but `.key` is the surrogate pair. Then, `.charCode` for second one is the low-surrogate and `.key` is empty string. In this mode, `TextEditor` and `HTMLEditor` ignores the second `keypress`. Therefore, web apps can cancel it only with the first `keypress`, but it indicates the `keypress` introduces a surrogate pair with `.key` attribute. Otherwise, if using the double-keypress event model and allowing ill-formed UTF-16 --this is the traditional our behavior and compatible with Chrome in Windows--, 2 `keypress` events are dispatched with same `.charCode` values as the previous mode, but first `.key` is the high-surrogate and the other's is the low surrogate. Therefore, web apps can cancel either one of them or both of them. Finally, this patch makes `TextEditor` and `HTMLEditor` handle text input with `keypress` events properly. Except in the last mode, `beforeinput` and `input` events are fired once and their `data` values are the surrogate pair. On the other hand, in the last mode, 2 sets of `beforeinput` and `input` are fired and their `.data` values has only the surrogate so that ill-formed UTF-16 values. Note that this patch also fixes an issue on Windows. Windows may send a high surrogate and a low surrogate with 2 sets of `WM_KEYDOWN` and `WM_KEYUP` whose virtual keycode is `VK_PACKET` (typically, this occurs with `SendInput` API). For handling this correctly, this patch changes `NativeKey` class to make it just store the high surrogate for the first `WM_KEYDOWN` and `WM_KEYUP` and use it when it'll receive another `WM_KEYDOWN` for a low surrogate. Differential Revision: https://phabricator.services.mozilla.com/D182142
178 lines
7.9 KiB
HTML
178 lines
7.9 KiB
HTML
<?xml version="1.0"?>
|
|
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
|
|
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
|
|
|
|
<window id="window1" title="Test handling of native key input for surrogate pairs"
|
|
xmlns:html="http://www.w3.org/1999/xhtml"
|
|
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
|
|
|
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
|
|
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
|
|
<script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"/>
|
|
|
|
<!-- test results are displayed in the html:body -->
|
|
<body xmlns="http://www.w3.org/1999/xhtml">
|
|
<p id="display"></p>
|
|
<div id="content" style="display: none"></div>
|
|
<pre id="test"></pre>
|
|
</body>
|
|
|
|
<script type="application/javascript"><![CDATA[
|
|
SimpleTest.waitForExplicitFinish();
|
|
SimpleTest.waitForFocus(async () => {
|
|
function promiseSynthesizeNativeKey(
|
|
aNativeKeyCode,
|
|
aChars,
|
|
) {
|
|
return new Promise(resolve => {
|
|
synthesizeNativeKey(
|
|
KEYBOARD_LAYOUT_EN_US,
|
|
aNativeKeyCode,
|
|
{},
|
|
aChars,
|
|
aChars,
|
|
resolve
|
|
);
|
|
});
|
|
}
|
|
function getEventData(aEvent) {
|
|
return `{ type: "${aEvent.type}", key: "${aEvent.key}", code: "${
|
|
aEvent.code
|
|
}", keyCode: 0x${
|
|
aEvent.keyCode.toString(16).toUpperCase()
|
|
}, charCode: 0x${aEvent.charCode.toString(16).toUpperCase()} }`;
|
|
}
|
|
function getEventArrayData(aEvents) {
|
|
if (!aEvents.length) {
|
|
return "[]";
|
|
}
|
|
let result = "[\n";
|
|
for (const e of aEvents) {
|
|
result += ` ${getEventData(e)}\n`;
|
|
}
|
|
return result + "]";
|
|
}
|
|
let events = [];
|
|
function onKey(aEvent) {
|
|
events.push(aEvent);
|
|
}
|
|
window.addEventListener("keydown", onKey);
|
|
window.addEventListener("keypress", onKey);
|
|
window.addEventListener("keyup", onKey);
|
|
|
|
async function runTests(
|
|
aTestPerSurrogateKeyPress,
|
|
aTestIllFormedUTF16KeyValue = false
|
|
) {
|
|
await SpecialPowers.pushPrefEnv({
|
|
set: [
|
|
["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress],
|
|
["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue],
|
|
],
|
|
});
|
|
const settingDescription =
|
|
`aTestPerSurrogateKeyPress=${
|
|
aTestPerSurrogateKeyPress
|
|
}, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`;
|
|
const allowIllFormedUTF16 =
|
|
aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue;
|
|
|
|
// If the keyboard layout has a key to introduce a surrogate pair,
|
|
// one set of WM_KEYDOWN and WM_KEYUP are generated and it's translated
|
|
// to two WM_CHARs.
|
|
await (async function test_one_key_press() {
|
|
events = [];
|
|
await promiseSynthesizeNativeKey(WIN_VK_A, "\uD83E\uDD14");
|
|
const keyCodeA = "A".charCodeAt(0);
|
|
is(
|
|
getEventArrayData(events),
|
|
getEventArrayData(
|
|
// eslint-disable-next-line no-nested-ternary
|
|
aTestPerSurrogateKeyPress
|
|
? (
|
|
allowIllFormedUTF16
|
|
? [
|
|
{ type: "keydown", key: "\uD83E\uDD14", code: "KeyA", keyCode: keyCodeA, charCode: 0 },
|
|
{ type: "keypress", key: "\uD83E", code: "KeyA", keyCode: 0, charCode: 0xD83E },
|
|
{ type: "keypress", key: "\uDD14", code: "KeyA", keyCode: 0, charCode: 0xDD14 },
|
|
{ type: "keyup", key: "a", code: "KeyA", keyCode: keyCodeA, charCode: 0 }, // Cannot set .key properly without a hack
|
|
]
|
|
: [
|
|
{ type: "keydown", key: "\uD83E\uDD14", code: "KeyA", keyCode: keyCodeA, charCode: 0 },
|
|
{ type: "keypress", key: "\uD83E\uDD14", code: "KeyA", keyCode: 0, charCode: 0xD83E },
|
|
{ type: "keypress", key: "", code: "KeyA", keyCode: 0, charCode: 0xDD14 },
|
|
{ type: "keyup", key: "a", code: "KeyA", keyCode: keyCodeA, charCode: 0 }, // Cannot set .key properly without a hack
|
|
]
|
|
)
|
|
: [
|
|
{ type: "keydown", key: "\uD83E\uDD14", code: "KeyA", keyCode: keyCodeA, charCode: 0 },
|
|
{ type: "keypress", key: "\uD83E\uDD14", code: "KeyA", keyCode: 0, charCode: 0x1F914 },
|
|
{ type: "keyup", key: "a", code: "KeyA", keyCode: keyCodeA, charCode: 0 }, // Cannot set .key properly without a hack
|
|
]
|
|
),
|
|
`test_one_key_press(${
|
|
settingDescription
|
|
}): Typing surrogate pair should cause one set of keydown and keyup events with ${
|
|
aTestPerSurrogateKeyPress ? "2 keypress events" : "a keypress event"
|
|
}`
|
|
);
|
|
})();
|
|
|
|
// If a surrogate pair is sent with SendInput API, 2 sets of keyboard
|
|
// events are generated. E.g., Emojis in the touch keyboard on Win10 or
|
|
// later.
|
|
await (async function test_send_input() {
|
|
events = [];
|
|
// Virtual keycode for the WM_KEYDOWN/WM_KEYUP is VK_PACKET.
|
|
await promiseSynthesizeNativeKey(WIN_VK_PACKET, "\uD83E");
|
|
await promiseSynthesizeNativeKey(WIN_VK_PACKET, "\uDD14");
|
|
// WM_KEYDOWN, WM_CHAR and WM_KEYUP for the high surrogate input
|
|
// shouldn't cause DOM events.
|
|
is(
|
|
getEventArrayData(events),
|
|
getEventArrayData(
|
|
// eslint-disable-next-line no-nested-ternary
|
|
aTestPerSurrogateKeyPress
|
|
? (
|
|
allowIllFormedUTF16
|
|
? [
|
|
{ type: "keydown", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0 },
|
|
{ type: "keypress", key: "\uD83E", code: "", keyCode: 0, charCode: 0xD83E },
|
|
{ type: "keypress", key: "\uDD14", code: "", keyCode: 0, charCode: 0xDD14 },
|
|
{ type: "keyup", key: "", code: "", keyCode: 0, charCode: 0 }, // Cannot set .key properly without a hack
|
|
]
|
|
: [
|
|
{ type: "keydown", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0 },
|
|
{ type: "keypress", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0xD83E },
|
|
{ type: "keypress", key: "", code: "", keyCode: 0, charCode: 0xDD14 },
|
|
{ type: "keyup", key: "", code: "", keyCode: 0, charCode: 0 }, // Cannot set .key properly without a hack
|
|
]
|
|
)
|
|
: [
|
|
{ type: "keydown", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0 },
|
|
{ type: "keypress", key: "\uD83E\uDD14", code: "", keyCode: 0, charCode: 0x1F914 },
|
|
{ type: "keyup", key: "", code: "", keyCode: 0, charCode: 0 }, // Cannot set .key properly without a hack
|
|
]
|
|
),
|
|
`test_send_input(${
|
|
settingDescription
|
|
}): Inputting surrogate pair should cause one set of keydown and keyup events ${
|
|
aTestPerSurrogateKeyPress ? "2 keypress events" : "a keypress event"
|
|
}`
|
|
);
|
|
})();
|
|
}
|
|
|
|
await runTests(true, true);
|
|
await runTests(true, false);
|
|
await runTests(false);
|
|
|
|
window.removeEventListener("keydown", onKey);
|
|
window.removeEventListener("keypress", onKey);
|
|
window.removeEventListener("keyup", onKey);
|
|
|
|
SimpleTest.finish();
|
|
});
|
|
]]></script>
|
|
|
|
</window>
|