From 8224e1138ce9e1808863c08226471a04bdb7d7df Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Wed, 23 Aug 2023 01:16:59 +0000 Subject: [PATCH] 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 --- .../chrome/window_nsITextInputProcessor.xhtml | 125 +++++++++++- editor/libeditor/HTMLEditor.cpp | 16 +- editor/libeditor/TextEditor.cpp | 32 +++- .../test_htmleditor_keyevent_handling.html | 170 +++++++++++++++-- .../test_texteditor_keyevent_handling.html | 163 +++++++++++++++- modules/libpref/init/StaticPrefList.yaml | 23 +++ .../mochitest/tests/SimpleTest/EventUtils.js | 21 ++- widget/TextEventDispatcher.cpp | 56 +++++- widget/tests/chrome.ini | 2 + ...t_surrogate_pair_native_key_handling.xhtml | 178 ++++++++++++++++++ widget/windows/KeyboardLayout.cpp | 76 ++++++++ widget/windows/KeyboardLayout.h | 5 + 12 files changed, 835 insertions(+), 32 deletions(-) create mode 100644 widget/tests/test_surrogate_pair_native_key_handling.xhtml diff --git a/dom/base/test/chrome/window_nsITextInputProcessor.xhtml b/dom/base/test/chrome/window_nsITextInputProcessor.xhtml index 38326173a666..44e4c897fad9 100644 --- a/dom/base/test/chrome/window_nsITextInputProcessor.xhtml +++ b/dom/base/test/chrome/window_nsITextInputProcessor.xhtml @@ -2444,7 +2444,7 @@ function runConsumingKeydownBeforeCompositionTests() window.removeEventListener("keyup", handler, false); } -function runKeyTests() +async function runKeyTests() { var description = "runKeyTests(): "; const kModifiers = @@ -2637,6 +2637,127 @@ function runKeyTests() is(input.value, "abc", description + "input.value should be \"abc\""); + // Emulates pressing and releasing a key which introduces a surrogate pair. + async function test_press_and_release_surrogate_pair_key( + 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; + const keySurrogatePair = new KeyboardEvent("", { key: "\uD842\uDFB7", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + + input.value = ""; + reset(); + doDefaultKeydown = TIP.keydown(keySurrogatePair); + doDefaultKeyup = TIP.keyup(keySurrogatePair); + + is( + doDefaultKeydown, + TIP.KEYPRESS_IS_CONSUMED, + `${ + description + }TIP.keydown(keySurrogatePair), ${ + settingDescription + }, should return 0x02 because the keypress event should be consumed by the input element` + ); + is( + doDefaultKeyup, + true, + `${description}TIP.keyup(keySurrogatePair) should return true` + ); + is( + events.length, + aTestPerSurrogateKeyPress ? 4 : 3, + `${description}TIP.keydown(keySurrogatePair), ${ + settingDescription + }, should cause keydown, keypress${ + aTestPerSurrogateKeyPress ? ", keypress" : "" + } and keyup event` + ); + checkKeyAttrs( + `${description}TIP.keydown(keySurrogatePair), ${settingDescription}`, + events[0], + { + type: "keydown", + key: "\uD842\uDFB7", + code: "KeyA", + keyCode: KeyboardEvent.DOM_VK_A, + charCode: 0, + } + ); + if (aTestPerSurrogateKeyPress) { + checkKeyAttrs( + `${description}TIP.keydown(keySurrogatePair), ${settingDescription}`, + events[1], + { + type: "keypress", + key: allowIllFormedUTF16 + ? "\uD842" + : "\uD842\uDFB7", // First keypress should have the surrogate pair + code: "KeyA", + keyCode: 0, + charCode: "\uD842\uDFB7".charCodeAt(0), + defaultPrevented: true, + } + ); + checkKeyAttrs( + `${description}TIP.keydown(keySurrogatePair), ${settingDescription}`, + events[2], + { + type: "keypress", + key: allowIllFormedUTF16 + ? "\uDFB7" + : "", // But the following keypress should have empty string, instead + code: "KeyA", + keyCode: 0, + charCode: "\uD842\uDFB7".charCodeAt(1), + defaultPrevented: true, + } + ); + } else { + checkKeyAttrs( + `${description}TIP.keydown(keySurrogatePair), ${settingDescription}`, + events[1], + { + type: "keypress", + key: "\uD842\uDFB7", + code: "KeyA", + keyCode: 0, + charCode: 0x20BB7, + defaultPrevented: true, + } + ); + } + checkKeyAttrs( + `${description}TIP.keyup(keySurrogatePair), ${settingDescription}`, + events[aTestPerSurrogateKeyPress ? 3 : 2], + { + type: "keyup", + key: "\uD842\uDFB7", + code: "KeyA", + keyCode: KeyboardEvent.DOM_VK_A, + charCode: 0, + } + ); + is( + input.value, + "\uD842\uDFB7", + `${description}${settingDescription}, input.value should be the surrogate pair` + ); + }; + + await test_press_and_release_surrogate_pair_key(true, true); + await test_press_and_release_surrogate_pair_key(true, false); + await test_press_and_release_surrogate_pair_key(false); + // If KEY_FORCE_PRINTABLE_KEY is specified, registered key names can be a printable key which inputs the specified value. input.value = ""; var keyEnterPrintable = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); @@ -4730,7 +4851,7 @@ async function runTests() runCompositionTests(); runCompositionWithKeyEventTests(); runConsumingKeydownBeforeCompositionTests(); - runKeyTests(); + await runKeyTests(); runErrorTests(); runCommitCompositionTests(); await runCallbackTests(false); diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp index 5f6f10efc333..3fdd199f3236 100644 --- a/editor/libeditor/HTMLEditor.cpp +++ b/editor/libeditor/HTMLEditor.cpp @@ -1448,7 +1448,21 @@ nsresult HTMLEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) { return NS_OK; } aKeyboardEvent->PreventDefault(); - nsAutoString str(aKeyboardEvent->mCharCode); + // If we dispatch 2 keypress events for a surrogate pair and we set only + // first `.key` value to the surrogate pair, the preceding one has it and the + // other has empty string. In this case, we should handle only the first one + // with the key value. + if (!StaticPrefs::dom_event_keypress_dispatch_once_per_surrogate_pair() && + !StaticPrefs::dom_event_keypress_key_allow_lone_surrogate() && + aKeyboardEvent->mKeyValue.IsEmpty() && + IS_SURROGATE(aKeyboardEvent->mCharCode)) { + return NS_OK; + } + nsAutoString str(aKeyboardEvent->mKeyValue); + if (str.IsEmpty()) { + str.Assign(static_cast(aKeyboardEvent->mCharCode)); + } + // FYI: DIfferent from TextEditor, we can treat \r (CR) as-is in HTMLEditor. nsresult rv = OnInputText(str); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed"); return rv; diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp index 43ad3fc3af81..2f6ed65f2b72 100644 --- a/editor/libeditor/TextEditor.cpp +++ b/editor/libeditor/TextEditor.cpp @@ -24,6 +24,7 @@ #include "mozilla/mozalloc.h" #include "mozilla/Preferences.h" #include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_editor.h" #include "mozilla/TextComposition.h" #include "mozilla/TextEvents.h" @@ -317,18 +318,35 @@ nsresult TextEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) { // we don't PreventDefault() here or keybindings like control-x won't work return NS_OK; } - // Our widget shouldn't set `\r` to `mCharCode`, but it may be synthesized + aKeyboardEvent->PreventDefault(); + // If we dispatch 2 keypress events for a surrogate pair and we set only + // first `.key` value to the surrogate pair, the preceding one has it and the + // other has empty string. In this case, we should handle only the first one + // with the key value. + if (!StaticPrefs::dom_event_keypress_dispatch_once_per_surrogate_pair() && + !StaticPrefs::dom_event_keypress_key_allow_lone_surrogate() && + aKeyboardEvent->mKeyValue.IsEmpty() && + IS_SURROGATE(aKeyboardEvent->mCharCode)) { + return NS_OK; + } + // Our widget shouldn't set `\r` to `mKeyValue`, but it may be synthesized // keyboard event and its value may be `\r`. In such case, we should treat // it as `\n` for the backward compatibility because we stopped converting // `\r` and `\r\n` to `\n` at getting `HTMLInputElement.value` and // `HTMLTextAreaElement.value` for the performance (i.e., we don't need to // take care in `HTMLEditor`). - char16_t charCode = - static_cast(aKeyboardEvent->mCharCode) == nsCRT::CR - ? nsCRT::LF - : static_cast(aKeyboardEvent->mCharCode); - aKeyboardEvent->PreventDefault(); - nsAutoString str(charCode); + nsAutoString str(aKeyboardEvent->mKeyValue); + if (str.IsEmpty()) { + MOZ_ASSERT(aKeyboardEvent->mCharCode <= 0xFFFF, + "Non-BMP character needs special handling"); + str.Assign(aKeyboardEvent->mCharCode == nsCRT::CR + ? static_cast(nsCRT::LF) + : static_cast(aKeyboardEvent->mCharCode)); + } else { + MOZ_ASSERT(str.Find(u"\r\n"_ns) == kNotFound, + "This assumes that typed text does not include CRLF"); + str.ReplaceChar('\r', '\n'); + } nsresult rv = OnInputText(str); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed"); return rv; diff --git a/editor/libeditor/tests/test_htmleditor_keyevent_handling.html b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html index 803b40b8bc38..58666beb3512 100644 --- a/editor/libeditor/tests/test_htmleditor_keyevent_handling.html +++ b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html @@ -29,7 +29,7 @@ const kIsMac = navigator.platform.includes("Mac"); const kIsWin = navigator.platform.includes("Win"); const kIsLinux = navigator.platform.includes("Linux") || navigator.platform.includes("SunOS"); -function runTests() { +async function runTests() { document.execCommand("stylewithcss", false, "true"); document.execCommand("defaultParagraphSeparator", false, "div"); @@ -83,8 +83,13 @@ function runTests() { SpecialPowers.addSystemEventListener(window, "keypress", listener, false); // eslint-disable-next-line complexity - function doTest(aElement, aDescription, - aIsReadonly, aIsTabbable, aIsPlaintext) { + async function doTest( + aElement, + aDescription, + aIsReadonly, + aIsTabbable, + aIsPlaintext + ) { function reset(aText) { capturingPhase.fired = false; capturingPhase.prevented = false; @@ -560,50 +565,193 @@ function runTests() { : "Mozilla
"; })(), aDescription + "typed \"Mozilla \""); + + // typing non-BMP character: + async function test_typing_surrogate_pair( + aTestPerSurrogateKeyPress, + aTestIllFormedUTF16KeyValue = false + ) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress], + ["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue], + ], + }); + reset(""); + let events = []; + function pushIntoEvents(aEvent) { + events.push(aEvent); + } + function getEventData(aKeyboardEventOrInputEvent) { + if (!aKeyboardEventOrInputEvent) { + return "{}"; + } + switch (aKeyboardEventOrInputEvent.type) { + case "keydown": + case "keypress": + case "keyup": + return `{ type: "${aKeyboardEventOrInputEvent.type}", key="${ + aKeyboardEventOrInputEvent.key + }", charCode=0x${ + aKeyboardEventOrInputEvent.charCode.toString(16).toUpperCase() + } }`; + default: + return `{ type: "${aKeyboardEventOrInputEvent.type}", inputType="${ + aKeyboardEventOrInputEvent.inputType + }", data="${aKeyboardEventOrInputEvent.data}" }`; + } + } + function getEventArrayData(aEvents) { + if (!aEvents.length) { + return "[]"; + } + let result = "[\n"; + for (const e of aEvents) { + result += ` ${getEventData(e)}\n`; + } + return result + "]"; + } + aElement.addEventListener("keydown", pushIntoEvents); + aElement.addEventListener("keypress", pushIntoEvents); + aElement.addEventListener("keyup", pushIntoEvents); + aElement.addEventListener("beforeinput", pushIntoEvents); + aElement.addEventListener("input", pushIntoEvents); + synthesizeKey("\uD842\uDFB7"); + aElement.removeEventListener("keydown", pushIntoEvents); + aElement.removeEventListener("keypress", pushIntoEvents); + aElement.removeEventListener("keyup", pushIntoEvents); + aElement.removeEventListener("beforeinput", pushIntoEvents); + aElement.removeEventListener("input", pushIntoEvents); + const settingDescription = + `aTestPerSurrogateKeyPress=${ + aTestPerSurrogateKeyPress + }, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`; + const allowIllFormedUTF16 = + aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue; + + check(`${aDescription}, ${settingDescription}a surrogate pair`, true, true, !aIsReadonly); + is( + aElement.textContent, + !aIsReadonly ? "\uD842\uDFB7" : "", + `${aDescription}, ${settingDescription}, The typed surrogate pair should've been inserted` + ); + if (aIsReadonly) { + is( + getEventArrayData(events), + getEventArrayData( + aTestPerSurrogateKeyPress + ? ( + allowIllFormedUTF16 + ? [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842", charCode: 0xD842 }, + { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, + { type: "keypress", key: "", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ) + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ), + `${aDescription}, ${ + settingDescription + }, Typing a surrogate pair in readonly editor should not cause input events` + ); + } else { + is( + getEventArrayData(events), + getEventArrayData( + aTestPerSurrogateKeyPress + ? ( + allowIllFormedUTF16 + ? [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842", charCode: 0xD842 }, + { type: "beforeinput", data: "\uD842", inputType: "insertText" }, + { type: "input", data: "\uD842", inputType: "insertText" }, + { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, + { type: "beforeinput", data: "\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uDFB7", inputType: "insertText" }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, + { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "keypress", key: "", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ) + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, + { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ), + `${aDescription}, ${ + settingDescription + }, Typing a surrogate pair in editor should cause input events` + ); + } + } + await test_typing_surrogate_pair(true, true); + await test_typing_surrogate_pair(true, false); + await test_typing_surrogate_pair(false); } - doTest(htmlEditor, "contenteditable=\"true\"", false, true, false); + await doTest(htmlEditor, "contenteditable=\"true\"", false, true, false); const nsIEditor = SpecialPowers.Ci.nsIEditor; var editor = SpecialPowers.wrap(window).docShell.editor; var flags = editor.flags; // readonly editor.flags = flags | nsIEditor.eEditorReadonlyMask; - doTest(htmlEditor, "readonly HTML editor", true, true, false); + await doTest(htmlEditor, "readonly HTML editor", true, true, false); // non-tabbable editor.flags = flags & ~(nsIEditor.eEditorAllowInteraction); - doTest(htmlEditor, "non-tabbable HTML editor", false, false, false); + await doTest(htmlEditor, "non-tabbable HTML editor", false, false, false); // readonly and non-tabbable editor.flags = (flags | nsIEditor.eEditorReadonlyMask) & ~(nsIEditor.eEditorAllowInteraction); - doTest(htmlEditor, "readonly and non-tabbable HTML editor", + await doTest(htmlEditor, "readonly and non-tabbable HTML editor", true, false, false); // plaintext editor.flags = flags | nsIEditor.eEditorPlaintextMask; - doTest(htmlEditor, "HTML editor but plaintext mode", false, true, true); + await doTest(htmlEditor, "HTML editor but plaintext mode", false, true, true); // plaintext and non-tabbable editor.flags = (flags | nsIEditor.eEditorPlaintextMask) & ~(nsIEditor.eEditorAllowInteraction); - doTest(htmlEditor, "non-tabbable HTML editor but plaintext mode", + await doTest(htmlEditor, "non-tabbable HTML editor but plaintext mode", false, false, true); // readonly and plaintext editor.flags = flags | nsIEditor.eEditorPlaintextMask | nsIEditor.eEditorReadonlyMask; - doTest(htmlEditor, "readonly HTML editor but plaintext mode", + await doTest(htmlEditor, "readonly HTML editor but plaintext mode", true, true, true); // readonly, plaintext and non-tabbable editor.flags = (flags | nsIEditor.eEditorPlaintextMask | nsIEditor.eEditorReadonlyMask) & ~(nsIEditor.eEditorAllowInteraction); - doTest(htmlEditor, "readonly and non-tabbable HTML editor but plaintext mode", + await doTest(htmlEditor, "readonly and non-tabbable HTML editor but plaintext mode", true, false, true); SpecialPowers.removeSystemEventListener(window, "keypress", listener, true); diff --git a/editor/libeditor/tests/test_texteditor_keyevent_handling.html b/editor/libeditor/tests/test_texteditor_keyevent_handling.html index c87caa31d441..2c80181b3c70 100644 --- a/editor/libeditor/tests/test_texteditor_keyevent_handling.html +++ b/editor/libeditor/tests/test_texteditor_keyevent_handling.html @@ -31,7 +31,7 @@ const kIsMac = navigator.platform.includes("Mac"); const kIsWin = navigator.platform.includes("Win"); const kIsLinux = navigator.platform.includes("Linux"); -function runTests() { +async function runTests() { var fm = SpecialPowers.Services.focus; var capturingPhase = { fired: false, prevented: false }; @@ -85,7 +85,7 @@ function runTests() { SpecialPowers.addSystemEventListener(parentElement, "keypress", listener, false); - function doTest(aElement, aDescription, aIsSingleLine, aIsReadonly) { + async function doTest(aElement, aDescription, aIsSingleLine, aIsReadonly) { function reset(aText) { capturingPhase.fired = false; capturingPhase.prevented = false; @@ -293,22 +293,169 @@ function runTests() { check(aDescription + "' '", true, true, !aIsReadonly); is(aElement.value, !aIsReadonly ? "Mozilla " : "", aDescription + "typed \"Mozilla \""); + + // typing non-BMP character: + async function test_typing_surrogate_pair( + aTestPerSurrogateKeyPress, + aTestIllFormedUTF16KeyValue = false + ) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress], + ["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue], + ], + }); + reset(""); + let events = []; + function pushIntoEvents(aEvent) { + events.push(aEvent); + } + function getEventData(aKeyboardEventOrInputEvent) { + if (!aKeyboardEventOrInputEvent) { + return "{}"; + } + switch (aKeyboardEventOrInputEvent.type) { + case "keydown": + case "keypress": + case "keyup": + return `{ type: "${aKeyboardEventOrInputEvent.type}", key="${ + aKeyboardEventOrInputEvent.key + }", charCode=0x${ + aKeyboardEventOrInputEvent.charCode.toString(16).toUpperCase() + } }`; + default: + return `{ type: "${aKeyboardEventOrInputEvent.type}", inputType="${ + aKeyboardEventOrInputEvent.inputType + }", data="${aKeyboardEventOrInputEvent.data}" }`; + } + } + function getEventArrayData(aEvents) { + if (!aEvents.length) { + return "[]"; + } + let result = "[\n"; + for (const e of aEvents) { + result += ` ${getEventData(e)}\n`; + } + return result + "]"; + } + aElement.addEventListener("keydown", pushIntoEvents); + aElement.addEventListener("keypress", pushIntoEvents); + aElement.addEventListener("keyup", pushIntoEvents); + aElement.addEventListener("beforeinput", pushIntoEvents); + aElement.addEventListener("input", pushIntoEvents); + synthesizeKey("\uD842\uDFB7"); + aElement.removeEventListener("keydown", pushIntoEvents); + aElement.removeEventListener("keypress", pushIntoEvents); + aElement.removeEventListener("keyup", pushIntoEvents); + aElement.removeEventListener("beforeinput", pushIntoEvents); + aElement.removeEventListener("input", pushIntoEvents); + const settingDescription = + `aTestPerSurrogateKeyPress=${ + aTestPerSurrogateKeyPress + }, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`; + const allowIllFormedUTF16 = + aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue; + + check(`${aDescription}, ${settingDescription}a surrogate pair`, true, true, !aIsReadonly); + is( + aElement.value, + !aIsReadonly ? "\uD842\uDFB7" : "", + `${aDescription}, ${ + settingDescription + }, The typed surrogate pair should've been inserted` + ); + if (aIsReadonly) { + is( + getEventArrayData(events), + getEventArrayData( + // eslint-disable-next-line no-nested-ternary + aTestPerSurrogateKeyPress + ? ( + allowIllFormedUTF16 + ? [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842", charCode: 0xD842 }, + { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, + { type: "keypress", key: "", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ) + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ), + `${aDescription}, ${ + settingDescription + }, Typing a surrogate pair in readonly editor should not cause input events` + ); + } else { + is( + getEventArrayData(events), + getEventArrayData( + // eslint-disable-next-line no-nested-ternary + aTestPerSurrogateKeyPress + ? ( + allowIllFormedUTF16 + ? [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842", charCode: 0xD842 }, + { type: "beforeinput", data: "\uD842", inputType: "insertText" }, + { type: "input", data: "\uD842", inputType: "insertText" }, + { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, + { type: "beforeinput", data: "\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uDFB7", inputType: "insertText" }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, + { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "keypress", key: "", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ) + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, + { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ), + `${aDescription}, ${ + settingDescription + }, Typing a surrogate pair in editor should cause input events` + ); + } + } + await test_typing_surrogate_pair(true, true); + await test_typing_surrogate_pair(true, false); + await test_typing_surrogate_pair(false); } - doTest(inputField, "", true, false); + await doTest(inputField, "", true, false); inputField.setAttribute("readonly", "readonly"); - doTest(inputField, "", true, true); + await doTest(inputField, "", true, true); - doTest(passwordField, "", true, false); + await doTest(passwordField, "", true, false); passwordField.setAttribute("readonly", "readonly"); - doTest(passwordField, "", true, true); + await doTest(passwordField, "", true, true); - doTest(textarea, "