fune/widget/tests/test_surrogate_pair_native_key_handling.xhtml
Masayuki Nakano 8224e1138c Bug 1840519 - Make typing surrogate pair behavior switchable with prefs r=m_kato,smaug
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
2023-08-23 01:16:59 +00:00

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>