diff options
Diffstat (limited to 'widget/tests')
132 files changed, 38001 insertions, 0 deletions
diff --git a/widget/tests/TestChromeMargin.cpp b/widget/tests/TestChromeMargin.cpp new file mode 100644 index 0000000000..0eed86b208 --- /dev/null +++ b/widget/tests/TestChromeMargin.cpp @@ -0,0 +1,130 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* This tests the margin parsing functionality in nsAttrValue.cpp, which + * is accessible via nsContentUtils, and is used in setting chromemargins + * to widget windows. It's located here due to linking issues in the + * content directory. + */ + +/* This test no longer compiles now that we've removed nsIContentUtils (bug + * 647273). We need to be internal code in order to include nsContentUtils.h, + * but defining MOZILLA_INTERNAL_API is not enough to make us internal. + */ + +#include "TestHarness.h" + +#ifndef MOZILLA_INTERNAL_API +# error This test needs MOZILLA_INTERNAL_API (see bug 652123) +#endif + +#include "nscore.h" +#include "nsContentUtils.h" +#include "nsString.h" + +struct DATA { + bool shouldfail; + const char* margins; + int top; + int right; + int bottom; + int left; +}; + +const bool SHOULD_FAIL = true; +const int SHOULD_PASS = false; + +const DATA Data[] = { + {SHOULD_FAIL, "", 1, 2, 3, 4}, + {SHOULD_FAIL, "1,0,0,0", 1, 2, 3, 4}, + {SHOULD_FAIL, "1,2,0,0", 1, 2, 3, 4}, + {SHOULD_FAIL, "1,2,3,0", 1, 2, 3, 4}, + {SHOULD_FAIL, "4,3,2,1", 1, 2, 3, 4}, + {SHOULD_FAIL, "azsasdasd", 0, 0, 0, 0}, + {SHOULD_FAIL, ",azsasdasd", 0, 0, 0, 0}, + {SHOULD_FAIL, " ", 1, 2, 3, 4}, + {SHOULD_FAIL, + "azsdfsdfsdfsdfsdfsasdasd,asdasdasdasdasdasd,asdadasdasd,asdasdasdasd", 0, + 0, 0, 0}, + {SHOULD_FAIL, "as,as,as,as", 0, 0, 0, 0}, + {SHOULD_FAIL, "0,0,0", 0, 0, 0, 0}, + {SHOULD_FAIL, "0,0", 0, 0, 0, 0}, + {SHOULD_FAIL, "4.6,1,1,1", 0, 0, 0, 0}, + {SHOULD_FAIL, ",,,,", 0, 0, 0, 0}, + {SHOULD_FAIL, "1, , , ,", 0, 0, 0, 0}, + {SHOULD_FAIL, "1, , ,", 0, 0, 0, 0}, + {SHOULD_FAIL, "@!@%^&^*()", 1, 2, 3, 4}, + {SHOULD_PASS, "4,3,2,1", 4, 3, 2, 1}, + {SHOULD_PASS, "-4,-3,-2,-1", -4, -3, -2, -1}, + {SHOULD_PASS, "10000,3,2,1", 10000, 3, 2, 1}, + {SHOULD_PASS, "4 , 3 , 2 , 1", 4, 3, 2, 1}, + {SHOULD_PASS, "4, 3 ,2,1", 4, 3, 2, 1}, + {SHOULD_FAIL, "4,3,2,10000000000000 --", 4, 3, 2, 10000000000000}, + {SHOULD_PASS, "4,3,2,1000", 4, 3, 2, 1000}, + {SHOULD_PASS, "2147483647,3,2,1000", 2147483647, 3, 2, 1000}, + {SHOULD_PASS, "2147483647,2147483647,2147483647,2147483647", 2147483647, + 2147483647, 2147483647, 2147483647}, + {SHOULD_PASS, "-2147483647,3,2,1000", -2147483647, 3, 2, 1000}, + {SHOULD_FAIL, "2147483648,3,2,1000", 1, 3, 2, 1000}, + {0, nullptr, 0, 0, 0, 0}}; + +void DoAttrValueTest() { + int idx = -1; + bool didFail = false; + while (Data[++idx].margins) { + nsAutoString str; + str.AssignLiteral(Data[idx].margins); + nsIntMargin values(99, 99, 99, 99); + bool result = nsContentUtils::ParseIntMarginValue(str, values); + + // if the parse fails + if (!result) { + if (Data[idx].shouldfail) continue; + fail(Data[idx].margins); + didFail = true; + printf("*1\n"); + continue; + } + + if (Data[idx].shouldfail) { + if (Data[idx].top == values.top && Data[idx].right == values.right && + Data[idx].bottom == values.bottom && Data[idx].left == values.left) { + // not likely + fail(Data[idx].margins); + didFail = true; + printf("*2\n"); + continue; + } + // good failure, parse failed and that's what we expected. + continue; + } +#if 0 + printf("%d==%d %d==%d %d==%d %d==%d\n", + Data[idx].top, values.top, + Data[idx].right, values.right, + Data[idx].bottom, values.bottom, + Data[idx].left, values.left); +#endif + if (Data[idx].top == values.top && Data[idx].right == values.right && + Data[idx].bottom == values.bottom && Data[idx].left == values.left) { + // good parse results + continue; + } else { + fail(Data[idx].margins); + didFail = true; + printf("*3\n"); + continue; + } + } + + if (!didFail) passed("nsAttrValue margin parsing tests passed."); +} + +int main(int argc, char** argv) { + ScopedXPCOM xpcom(""); + if (xpcom.failed()) return 1; + DoAttrValueTest(); + return 0; +} diff --git a/widget/tests/browser/browser.toml b/widget/tests/browser/browser.toml new file mode 100644 index 0000000000..506fe1a998 --- /dev/null +++ b/widget/tests/browser/browser.toml @@ -0,0 +1,90 @@ +[DEFAULT] +skip-if = ["os == 'android'"] + +["browser_test_AZERTY_digit_shortcut.js"] +skip-if = ["os == 'linux'"] # Linux build has not implemented sendNativeKeyEvent yet + +["browser_test_ContentCache.js"] + +["browser_test_InputContextURI.js"] + +["browser_test_clipboard_contextmenu.js"] + +["browser_test_clipboardcache.js"] +skip-if = [ + "os == 'win' && bits == 32 && !debug", # Bug 1759422 + "os == 'linux'", # Bug 1792749 +] + +["browser_test_fullscreen_size.js"] + +["browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js"] +support-files = [ + "file_ime_state_tests.html", + "../file_ime_state_test_helper.js", + "../file_test_ime_state_on_focus_move.js", +] + +["browser_test_ime_state_in_contenteditable_on_readonly_change_in_remote_content.js"] +support-files = [ + "file_ime_state_tests.html", + "../file_ime_state_test_helper.js", + "../file_test_ime_state_in_contenteditable_on_readonly_change.js", +] + +["browser_test_ime_state_in_designMode_on_focus_move_in_remote_content.js"] +support-files = [ + "file_ime_state_tests.html", + "../file_ime_state_test_helper.js", + "../file_test_ime_state_on_focus_move.js", +] + +["browser_test_ime_state_in_plugin_in_remote_content.js"] +support-files = ["../file_ime_state_test_helper.js"] + +["browser_test_ime_state_in_text_control_on_reframe_in_remote_content.js"] +support-files = [ + "../file_ime_state_test_helper.js", + "../file_test_ime_state_in_text_control_on_reframe.js", +] + +["browser_test_ime_state_on_editable_state_change_in_remote_content.js"] +support-files = ["../file_ime_state_test_helper.js"] + +["browser_test_ime_state_on_focus_move_in_remote_content.js"] +support-files = [ + "file_ime_state_tests.html", + "../file_ime_state_test_helper.js", + "../file_test_ime_state_on_focus_move.js", +] + +["browser_test_ime_state_on_input_type_change_in_remote_content.js"] +skip-if = ["true"] # Bug 1817704 +support-files = [ + "file_ime_state_tests.html", + "../file_ime_state_test_helper.js", + "../file_test_ime_state_on_input_type_change.js", +] + +["browser_test_ime_state_on_readonly_change_in_remote_content.js"] +support-files = [ + "file_ime_state_tests.html", + "../file_ime_state_test_helper.js", + "../file_test_ime_state_on_readonly_change.js", +] + +["browser_test_scrollbar_colors.js"] +skip-if = ["os == 'linux'"] # bug 1460109 +support-files = ["helper_scrollbar_colors.html"] + +["browser_test_swipe_gesture.js"] +skip-if = [ + "win11_2009 && bits == 32 && !debug", # Bug 1759422 + "verify", # Bug 1800022 + "os == 'linux'", # Bug 1784772 +] +support-files = [ + "helper_swipe_gesture.html", + "!/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + "!/gfx/layers/apz/test/mochitest/apz_test_utils.js", +] diff --git a/widget/tests/browser/browser_test_AZERTY_digit_shortcut.js b/widget/tests/browser/browser_test_AZERTY_digit_shortcut.js new file mode 100644 index 0000000000..ef5112ac01 --- /dev/null +++ b/widget/tests/browser/browser_test_AZERTY_digit_shortcut.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + let tabs = []; + for (let i = 0; i < 10; i++) { + const tab = BrowserTestUtils.addTab(gBrowser); + tabs.push(tab); + } + const kIsMac = AppConstants.platform == "macosx"; + + await BrowserTestUtils.withNewTab( + "https://example.com/browser/toolkit/content/tests/browser/file_empty.html", + async function (browser) { + let NativeKeyConstants = {}; + Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js", + NativeKeyConstants + ); + + function promiseSynthesizeAccelHyphenMinusWithAZERTY() { + return new Promise(resolve => + EventUtils.synthesizeNativeKey( + EventUtils.KEYBOARD_LAYOUT_FRENCH_PC, + kIsMac + ? NativeKeyConstants.MAC_VK_ANSI_6 + : NativeKeyConstants.WIN_VK_6, + { accelKey: true }, + kIsMac ? "-" : "", + kIsMac ? "-" : "", + resolve + ) + ); + } + + async function waitForCondition(aFunc) { + for (let i = 0; i < 60; i++) { + await new Promise(resolve => + requestAnimationFrame(() => requestAnimationFrame(resolve)) + ); + if (aFunc(ZoomManager.getFullZoomForBrowser(browser))) { + return true; + } + } + return false; + } + + const minZoomLevel = ZoomManager.MIN; + while (true) { + const currentZoom = ZoomManager.getFullZoomForBrowser(browser); + if (minZoomLevel == currentZoom) { + break; + } + info(`Trying to zoom out: ${currentZoom}`); + await promiseSynthesizeAccelHyphenMinusWithAZERTY(); + if (!(await waitForCondition(aZoomLevel => aZoomLevel < currentZoom))) { + ok(false, `Failed to zoom out from ${currentZoom}`); + return; + } + } + + await promiseSynthesizeAccelHyphenMinusWithAZERTY(); + await waitForCondition(() => false); + is( + gBrowser.selectedBrowser, + browser, + "Tab shouldn't be changed by Ctrl+- of AZERTY keyboard layout" + ); + // Reset the zoom before going to the next test. + EventUtils.synthesizeKey("0", { accelKey: true }); + await waitForCondition(aZoomLevel => aZoomLevel == 1); + } + ); + + while (tabs.length) { + await new Promise(resolve => { + const tab = tabs.shift(); + BrowserTestUtils.waitForTabClosing(tab).then(resolve); + BrowserTestUtils.removeTab(tab); + }); + } +}); diff --git a/widget/tests/browser/browser_test_ContentCache.js b/widget/tests/browser/browser_test_ContentCache.js new file mode 100644 index 0000000000..7b64e00f13 --- /dev/null +++ b/widget/tests/browser/browser_test_ContentCache.js @@ -0,0 +1,296 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + const TIP = Cc["@mozilla.org/text-input-processor;1"].createInstance( + Ci.nsITextInputProcessor + ); + let notifications = []; + const observer = (aTIP, aNotification) => { + switch (aNotification.type) { + case "request-to-commit": + aTIP.commitComposition(); + break; + case "request-to-cancel": + aTIP.cancelComposition(); + break; + case "notify-end-input-transaction": + case "notify-focus": + case "notify-blur": + case "notify-text-change": + case "notify-selection-change": + notifications.push(aNotification); + break; + } + return true; + }; + + function checkNotifications(aExpectedNotifications, aDescription) { + for (const expectedNotification of aExpectedNotifications) { + const notification = notifications.find( + element => element.type == expectedNotification.type + ); + if (expectedNotification.expected) { + isnot( + notification, + undefined, + `"${expectedNotification.type}" should be notified ${aDescription}` + ); + } else { + is( + notification, + undefined, + `"${expectedNotification.type}" should not be notified ${aDescription}` + ); + } + } + } + + ok( + TIP.beginInputTransaction(window, observer), + "nsITextInputProcessor.beingInputTransaction should return true" + ); + ok( + TIP.beginInputTransactionForTests(window, observer), + "nsITextInputProcessor.beginInputTransactionForTests should return true" + ); + + await BrowserTestUtils.withNewTab( + "https://example.com/browser/toolkit/content/tests/browser/file_empty.html", + async function (browser) { + ok(browser.isRemoteBrowser, "This test passes only in e10s mode"); + + // IMEContentObserver flushes pending IME notifications at next vsync + // after something happens. Therefore, after doing something in content + // process, we need to guarantee that IMEContentObserver has a change to + // send IME notifications to the main process with calling this function. + function waitForSendingIMENotificationsInContent() { + return SpecialPowers.spawn(browser, [], async () => { + await new Promise(resolve => + content.requestAnimationFrame(() => + content.requestAnimationFrame(resolve) + ) + ); + }); + } + + /** + * Test when empty editor gets focus + */ + notifications = []; + await SpecialPowers.spawn(browser, [], () => { + content.document.body.innerHTML = "<div contenteditable><br></div>"; + const editor = content.document.querySelector("div[contenteditable]"); + editor.focus(); + }); + + await waitForSendingIMENotificationsInContent(); + + (function () { + checkNotifications( + [ + { type: "notify-focus", expected: true }, + { type: "notify-blur", expected: false }, + { type: "notify-end-input-transaction", expected: false }, + { type: "notify-text-change", expected: false }, + { type: "notify-selection-change", expected: false }, + ], + "after empty editor gets focus" + ); + const text = EventUtils.synthesizeQueryTextContent(0, 1000); + ok( + text?.succeeded, + "query text content should succeed after empty editor gets focus" + ); + if (text?.succeeded) { + is( + text.text.replace(/[\r\n]/g, ""), + "", + "text should be only line breaks after empty editor gets focus" + ); + } + const selection = EventUtils.synthesizeQuerySelectedText(); + ok( + selection?.succeeded, + "query selected text should succeed after empty editor gets focus" + ); + if (selection?.succeeded) { + ok( + !selection.notFound, + "query selected text should find a selection range after empty editor gets focus" + ); + if (!selection.notFound) { + is( + selection.text, + "", + "selection should be collapsed after empty editor gets focus" + ); + } + } + })(); + + /** + * Test when there is non-collapsed selection + */ + notifications = []; + await SpecialPowers.spawn(browser, [], () => { + const editor = content.document.querySelector("div[contenteditable]"); + editor.innerHTML = "<p>abc</p><p>def</p>"; + content + .getSelection() + .setBaseAndExtent( + editor.querySelector("p").firstChild, + 2, + editor.querySelector("p + p").firstChild, + 1 + ); + }); + + await waitForSendingIMENotificationsInContent(); + + (function () { + checkNotifications( + [ + { type: "notify-focus", expected: false }, + { type: "notify-blur", expected: false }, + { type: "notify-end-input-transaction", expected: false }, + { type: "notify-text-change", expected: true }, + { type: "notify-selection-change", expected: true }, + ], + "after modifying focused editor" + ); + const text = EventUtils.synthesizeQueryTextContent(0, 1000); + ok( + text?.succeeded, + "query text content should succeed after modifying focused editor" + ); + if (text?.succeeded) { + is( + text.text.trim().replace(/\r\n/g, "\n").replace(/\n\n+/g, "\n"), + "abc\ndef", + "text should include the both paragraph's text after modifying focused editor" + ); + } + const selection = EventUtils.synthesizeQuerySelectedText(); + ok( + selection?.succeeded, + "query selected text should succeed after modifying focused editor" + ); + if (selection?.succeeded) { + ok( + !selection.notFound, + "query selected text should find a selection range after modifying focused editor" + ); + if (!selection.notFound) { + is( + selection.text + .trim() + .replace(/\r\n/g, "\n") + .replace(/\n\n+/g, "\n"), + "c\nd", + "selection should have the selected characters in the both paragraphs after modifying focused editor" + ); + } + } + })(); + + /** + * Test when there is no selection ranges + */ + notifications = []; + await SpecialPowers.spawn(browser, [], () => { + content.getSelection().removeAllRanges(); + }); + + await waitForSendingIMENotificationsInContent(); + + (function () { + checkNotifications( + [ + { type: "notify-focus", expected: false }, + { type: "notify-blur", expected: false }, + { type: "notify-end-input-transaction", expected: false }, + { type: "notify-text-change", expected: false }, + { type: "notify-selection-change", expected: true }, + ], + "after removing all selection ranges from the focused editor" + ); + const text = EventUtils.synthesizeQueryTextContent(0, 1000); + ok( + text?.succeeded, + "query text content should succeed after removing all selection ranges from the focused editor" + ); + if (text?.succeeded) { + is( + text.text.trim().replace(/\r\n/g, "\n").replace(/\n\n+/g, "\n"), + "abc\ndef", + "text should include the both paragraph's text after removing all selection ranges from the focused editor" + ); + } + const selection = EventUtils.synthesizeQuerySelectedText(); + ok( + selection?.succeeded, + "query selected text should succeed after removing all selection ranges from the focused editor" + ); + if (selection?.succeeded) { + ok( + selection.notFound, + "query selected text should find no selection range after removing all selection ranges from the focused editor" + ); + } + })(); + + /** + * Test when no editable element has focus. + */ + notifications = []; + await SpecialPowers.spawn(browser, [], () => { + content.document.body.innerHTML = "abcdef"; + }); + + await waitForSendingIMENotificationsInContent(); + + (function () { + checkNotifications( + [ + { type: "notify-focus", expected: false }, + { type: "notify-blur", expected: true }, + ], + "removing editor should make ContentCacheInParent not have any data" + ); + const text = EventUtils.synthesizeQueryTextContent(0, 1000); + ok( + !text?.succeeded, + "query text content should fail because no editable element has focus" + ); + const selection = EventUtils.synthesizeQuerySelectedText(); + ok( + !selection?.succeeded, + "query selected text should fail because no editable element has focus" + ); + const caret = EventUtils.synthesizeQueryCaretRect(0); + ok( + !caret?.succeeded, + "query caret rect should fail because no editable element has focus" + ); + const textRect = EventUtils.synthesizeQueryTextRect(0, 5, false); + ok( + !textRect?.succeeded, + "query text rect should fail because no editable element has focus" + ); + const textRectArray = EventUtils.synthesizeQueryTextRectArray(0, 5); + ok( + !textRectArray?.succeeded, + "query text rect array should fail because no editable element has focus" + ); + const editorRect = EventUtils.synthesizeQueryEditorRect(); + todo( + !editorRect?.succeeded, + "query editor rect should fail because no editable element has focus" + ); + })(); + } + ); +}); diff --git a/widget/tests/browser/browser_test_InputContextURI.js b/widget/tests/browser/browser_test_InputContextURI.js new file mode 100644 index 0000000000..52f05d90f9 --- /dev/null +++ b/widget/tests/browser/browser_test_InputContextURI.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const gDOMWindowUtils = EventUtils._getDOMWindowUtils(window); + +function promiseURLBarFocus() { + const waitForFocusInURLBar = BrowserTestUtils.waitForEvent(gURLBar, "focus"); + gURLBar.blur(); + gURLBar.focus(); + return Promise.all([ + waitForFocusInURLBar, + TestUtils.waitForCondition( + () => + gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED && + gDOMWindowUtils.inputContextOrigin === + Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_MAIN + ), + ]); +} + +function promiseIMEStateEnabledByRemote() { + return TestUtils.waitForCondition( + () => + gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED && + gDOMWindowUtils.inputContextOrigin === + Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_CONTENT + ); +} + +async function test_url_bar_url(aDesc) { + await promiseURLBarFocus(); + + is( + gDOMWindowUtils.inputContextURI, + null, + `When the search bar has focus, input context URI should be null because of in chrome document (${aDesc})` + ); +} + +async function test_input_in_http_or_https(aIsHTTPS) { + await promiseURLBarFocus(); + + const scheme = aIsHTTPS ? "https" : "http"; + const url = `${scheme}://example.com/browser/toolkit/content/tests/browser/file_empty.html`; + await BrowserTestUtils.withNewTab(url, async browser => { + ok(browser.isRemoteBrowser, "This test passes only in e10s mode"); + + await SpecialPowers.spawn(browser, [], async () => { + content.document.body.innerHTML = "<input>"; + const input = content.document.querySelector("input"); + input.focus(); + + // Wait for a tick for flushing IMEContentObserver's pending notifications. + await new Promise(resolve => + content.requestAnimationFrame(() => + content.requestAnimationFrame(resolve) + ) + ); + }); + + await promiseIMEStateEnabledByRemote(); + if (!gDOMWindowUtils.inputContextURI) { + ok( + false, + `Input context should have valid URI when the scheme of focused tab's URL is ${scheme}` + ); + return; + } + is( + gDOMWindowUtils.inputContextURI.spec, + url, + `Input context should have the document URI when the scheme of focused tab's URL is ${scheme}` + ); + }); +} + +add_task(async () => { + await test_url_bar_url("first check"); +}); +add_task(async () => { + await test_input_in_http_or_https(true); +}); +add_task(async () => { + await test_url_bar_url("check after remote content sets the URI"); +}); +add_task(async () => { + await test_input_in_http_or_https(false); +}); + +add_task(async function test_input_in_data() { + await BrowserTestUtils.withNewTab("data:text/html,<input>", async browser => { + ok(browser.isRemoteBrowser, "This test passes only in e10s mode"); + + await SpecialPowers.spawn(browser, [], async () => { + const input = content.document.querySelector("input"); + input.focus(); + + // Wait for a tick for flushing IMEContentObserver's pending notifications. + await new Promise(resolve => + content.requestAnimationFrame(() => + content.requestAnimationFrame(resolve) + ) + ); + }); + + await promiseIMEStateEnabledByRemote(); + is( + gDOMWindowUtils.inputContextURI, + null, + "Input context should not have data URI" + ); + }); +}); + +add_task(async function test_omit_private_things_in_URL() { + await SpecialPowers.pushPrefEnv({ + set: [["network.auth.confirmAuth.enabled", false]], + }); + await promiseURLBarFocus(); + + await BrowserTestUtils.withNewTab( + "https://username:password@example.com/browser/toolkit/content/tests/browser/file_empty.html?query=some#ref", + async browser => { + ok(browser.isRemoteBrowser, "This test passes only in e10s mode"); + + await SpecialPowers.spawn(browser, [], async () => { + content.document.body.innerHTML = "<input>"; + const input = content.document.querySelector("input"); + input.focus(); + + // Wait for a tick for flushing IMEContentObserver's pending notifications. + await new Promise(resolve => + content.requestAnimationFrame(() => + content.requestAnimationFrame(resolve) + ) + ); + }); + + await promiseIMEStateEnabledByRemote(); + if (!gDOMWindowUtils.inputContextURI) { + ok( + false, + `Input context should have valid URI even when the URL contains some private things` + ); + return; + } + is( + gDOMWindowUtils.inputContextURI.spec, + "https://example.com/browser/toolkit/content/tests/browser/file_empty.html", + `Input context should have the document URI which omit some private things in the URL` + ); + } + ); +}); diff --git a/widget/tests/browser/browser_test_clipboard_contextmenu.js b/widget/tests/browser/browser_test_clipboard_contextmenu.js new file mode 100644 index 0000000000..4d268d5be4 --- /dev/null +++ b/widget/tests/browser/browser_test_clipboard_contextmenu.js @@ -0,0 +1,127 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const clipboard = SpecialPowers.Services.clipboard; +const clipboardTypes = [ + clipboard.kGlobalClipboard, + clipboard.kSelectionClipboard, + clipboard.kFindClipboard, + clipboard.kSelectionCache, +]; +const supportedClipboardTypes = clipboardTypes.filter(type => + clipboard.isClipboardTypeSupported(type) +); + +const kPasteMenuPopupId = "clipboardReadPasteMenuPopup"; +const kPasteMenuItemId = "clipboardReadPasteMenuItem"; + +function waitForPasteMenuPopupEvent(aEventSuffix) { + // The element with id `kPasteMenuPopupId` is inserted dynamically, hence + // calling `BrowserTestUtils.waitForEvent` instead of + // `BrowserTestUtils.waitForPopupEvent`. + return BrowserTestUtils.waitForEvent( + document, + "popup" + aEventSuffix, + false /* capture */, + e => { + return e.target.getAttribute("id") == kPasteMenuPopupId; + } + ); +} + +async function waitForPasteContextMenu() { + await waitForPasteMenuPopupEvent("shown"); + const pasteButton = document.getElementById(kPasteMenuItemId); + info("Wait for paste button enabled"); + await BrowserTestUtils.waitForMutationCondition( + pasteButton, + { attributeFilter: ["disabled"] }, + () => !pasteButton.disabled, + "Wait for paste button enabled" + ); +} + +function promiseClickPasteButton() { + info("Wait for clicking paste contextmenu"); + const pasteButton = document.getElementById(kPasteMenuItemId); + let promise = BrowserTestUtils.waitForEvent(pasteButton, "click"); + EventUtils.synthesizeMouseAtCenter(pasteButton, {}); + return promise; +} + +async function clipboardAsyncGetData(aBrowser, aClipboardType) { + await SpecialPowers.spawn(aBrowser, [aClipboardType], async type => { + return new Promise((resolve, reject) => { + SpecialPowers.Services.clipboard.asyncGetData( + ["text/plain"], + type, + content.browsingContext.currentWindowContext, + content.document.nodePrincipal, + { + QueryInterface: SpecialPowers.ChromeUtils.generateQI([ + "nsIAsyncClipboardGetCallback", + ]), + // nsIAsyncClipboardGetCallback + onSuccess: aAsyncGetClipboardData => { + resolve(); + }, + onError: aResult => { + reject(aResult); + }, + } + ); + }); + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Avoid paste button delay enabling making test too long. + ["security.dialog_enable_delay", 0], + ], + }); +}); + +supportedClipboardTypes.forEach(type => { + add_task(async function test_clipboard_contextmenu() { + await BrowserTestUtils.withNewTab( + "https://example.com", + async function (browser) { + info(`Test clipboard contextmenu for clipboard type ${type}`); + let pasteContextMenuPromise = waitForPasteContextMenu(); + const asyncRead = clipboardAsyncGetData(browser, type); + await pasteContextMenuPromise; + + // We don't allow requests for different clipboard type to be + // consolidated, so when the context menu is shown for a specific + // clipboard type and has not yet got user response, any new requests + // for a different type should be rejected. + info( + "Test other clipboard types before interact with paste contextmenu" + ); + for (let otherType of supportedClipboardTypes) { + if (type == otherType) { + continue; + } + + info(`Test other clipboard type ${otherType}`); + await Assert.rejects( + clipboardAsyncGetData(browser, otherType), + ex => ex == Cr.NS_ERROR_DOM_NOT_ALLOWED_ERR, + `Request for clipboard type ${otherType} should be rejected` + ); + } + + await promiseClickPasteButton(); + try { + await asyncRead; + ok(true, `nsIClipboard.asyncGetData() should success`); + } catch (e) { + ok(false, `nsIClipboard.asyncGetData() should not fail with ${e}`); + } + } + ); + }); +}); diff --git a/widget/tests/browser/browser_test_clipboardcache.js b/widget/tests/browser/browser_test_clipboardcache.js new file mode 100644 index 0000000000..8cb6adb8b5 --- /dev/null +++ b/widget/tests/browser/browser_test_clipboardcache.js @@ -0,0 +1,137 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +// Note: widget/tests/test_bug1123480.xhtml checks whether nsTransferable behaves +// as expected with regards to private browsing mode and the clipboard cache, +// i.e. that the clipboard is not cached to the disk when private browsing mode +// is enabled. +// +// This test tests that the clipboard is not cached to the disk by IPC, +// as a regression test for bug 1396224. +// It indirectly uses nsTransferable, via the async navigator.clipboard API. + +// Create over 1 MB of sample garbage text. JavaScript strings are represented +// by UTF16 strings, so the size is twice as much as the actual string length. +// This value is chosen such that the size of the memory for the string exceeds +// the kLargeDatasetSize threshold in nsTransferable.h. +// It is also not a round number to reduce the odds of having an accidental +// collisions with another file (since the test below looks at the file size +// to identify the file). +var Ipsum = "0123456789".repeat(1234321); +var IpsumByteLength = Ipsum.length * 2; +var SHORT_STRING_NO_CACHE = "short string that will not be cached to the disk"; + +// Get a list of open file descriptors that refer to a file with the same size +// as the expected data (and assume that any mutations in file descriptor counts +// are caused by our test). +// TODO: This logic only counts file descriptors that are still open (e.g. when +// data persists after a copy). It does not detect cache files that exist only +// temporarily (e.g. after a paste). +function getClipboardCacheFDCount() { + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + if (AppConstants.platform === "win") { + // On Windows, nsAnonymousTemporaryFile does not immediately delete a file. + // Instead, the Windows-specific FILE_FLAG_DELETE_ON_CLOSE flag is used, + // which means that the file is deleted when the last handle is closed. + // Apparently, this flag is unreliable (e.g. when the application crashes), + // so nsAnonymousTemporaryFile stores the temporary files in a subdirectory, + // which is cleaned up some time after start-up. + + // This is just a test, and during the test we deterministically close the + // handles, so if FILE_FLAG_DELETE_ON_CLOSE does the thing it promises, the + // file is actually removed when the handle is closed. + + // Path from nsAnonymousTemporaryFile.cpp, GetTempDir. + dir.initWithPath(PathUtils.join(PathUtils.tempDir, "mozilla-temp-files")); + } else { + dir.initWithPath("/dev/fd"); + } + let count = 0; + for (let fdFile of dir.directoryEntries) { + let fileSize; + try { + fileSize = fdFile.fileSize; + } catch (e) { + // This can happen on macOS. + continue; + } + if (fileSize === IpsumByteLength) { + // Assume that the file was created by us if the size matches. + ++count; + } + } + return count; +} + +async function testCopyPaste(isPrivate) { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: isPrivate }); + let tab = await BrowserTestUtils.openNewForegroundTab(win); + let browser = tab.linkedBrowser; + + // Sanitize environment + await ContentTask.spawn(browser, SHORT_STRING_NO_CACHE, async shortStr => { + await content.navigator.clipboard.writeText(shortStr); + }); + + let initialFdCount = getClipboardCacheFDCount(); + + await SpecialPowers.spawn(browser, [Ipsum], async largeString => { + await content.navigator.clipboard.writeText(largeString); + }); + + let fdCountAfterCopy = getClipboardCacheFDCount(); + if (isPrivate) { + is(fdCountAfterCopy, initialFdCount, "Private write"); + } else { + is(fdCountAfterCopy, initialFdCount + 1, "Cached write"); + } + + let readStr = await SpecialPowers.spawn(browser, [], async () => { + let { document } = content; + document.body.contentEditable = true; + document.body.focus(); + let pastePromise = new Promise(resolve => { + document.addEventListener( + "paste", + e => { + resolve(e.clipboardData.getData("text/plain")); + }, + { once: true } + ); + }); + document.execCommand("paste"); + return pastePromise; + }); + ok(readStr === Ipsum, "Read what we pasted"); + + if (isPrivate) { + is(getClipboardCacheFDCount(), fdCountAfterCopy, "Private read"); + } else { + // Upon reading from the clipboard, a temporary nsTransferable is used, for + // which the cache is disabled. The content process does not cache clipboard + // data either. So the file descriptor count should be identical. + is(getClipboardCacheFDCount(), fdCountAfterCopy, "Read not cached"); + } + + // Cleanup. + await SpecialPowers.spawn( + browser, + [SHORT_STRING_NO_CACHE], + async shortStr => { + await content.navigator.clipboard.writeText(shortStr); + } + ); + is(getClipboardCacheFDCount(), initialFdCount, "Drop clipboard cache if any"); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); +} + +add_task(async function test_private() { + await testCopyPaste(true); +}); + +add_task(async function test_non_private() { + await testCopyPaste(false); +}); diff --git a/widget/tests/browser/browser_test_fullscreen_size.js b/widget/tests/browser/browser_test_fullscreen_size.js new file mode 100644 index 0000000000..dace46603c --- /dev/null +++ b/widget/tests/browser/browser_test_fullscreen_size.js @@ -0,0 +1,66 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +function waitForReflow(aWindow) { + return new Promise(resolve => { + aWindow.requestAnimationFrame(() => { + aWindow.requestAnimationFrame(resolve); + }); + }); +} + +add_task(async function fullscreen_size() { + let win = await BrowserTestUtils.openNewBrowserWindow({}); + win.gBrowser.selectedBrowser.focus(); + + info("Enter browser fullscreen mode"); + let promise = Promise.all([ + BrowserTestUtils.waitForEvent(win, "fullscreen"), + BrowserTestUtils.waitForEvent(win, "resize"), + ]); + win.fullScreen = true; + await promise; + + info("Await reflow of the chrome window"); + await waitForReflow(win); + + is(win.innerHeight, win.outerHeight, "Check height"); + is(win.innerWidth, win.outerWidth, "Check width"); + + await BrowserTestUtils.closeWindow(win); +}); + +// https://bugzilla.mozilla.org/show_bug.cgi?id=1830721 +add_task(async function fullscreen_size_moz_appearance() { + const win = await BrowserTestUtils.openNewBrowserWindow({}); + win.gBrowser.selectedBrowser.focus(); + + info("Add -moz-appearance style to chrome document"); + const style = win.document.createElement("style"); + style.innerHTML = ` + #main-window { + -moz-appearance: button; + } + `; + win.document.head.appendChild(style); + + info("Await reflow of the chrome window"); + await waitForReflow(win); + + info("Enter browser fullscreen mode"); + let promise = Promise.all([ + BrowserTestUtils.waitForEvent(win, "fullscreen"), + BrowserTestUtils.waitForEvent(win, "resize"), + ]); + win.fullScreen = true; + await promise; + + info("Await reflow of the chrome window"); + await waitForReflow(win); + + is(win.innerHeight, win.outerHeight, "Check height"); + is(win.innerWidth, win.outerWidth, `Check width`); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js new file mode 100644 index 0000000000..50b19f0cc3 --- /dev/null +++ b/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ +/* import-globals-from ../file_test_ime_state_on_focus_move.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_focus_move.js", + this +); +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html", + async function (browser) { + const tipWrapper = new TIPWrapper(window); + ok( + tipWrapper.isAvailable(), + "TextInputProcessor should've been initialized" + ); + + // isnot is used in file_test_ime_state_on_focus_move.js, but it's not + // defined as the alias of Assert.notEqual in browser-chrome tests. + // Therefore, we need to define it here. + // eslint-disable-next-line no-unused-vars + const isnot = Assert.notEqual; + + async function runIMEStateOnFocusMoveTests(aDescription) { + await (async function test_IMEState_without_focused_element() { + const checker = new IMEStateWhenNoActiveElementTester(aDescription); + const expectedData = await SpecialPowers.spawn( + browser, + [aDescription], + description => { + const runner = + content.wrappedJSObject.createIMEStateWhenNoActiveElementTester( + description + ); + return runner.run(content.document, content.window); + } + ); + checker.check(expectedData); + })(); + for ( + let index = 0; + index < IMEStateOnFocusMoveTester.numberOfTests; + ++index + ) { + const checker = new IMEStateOnFocusMoveTester(aDescription, index); + const expectedData = await SpecialPowers.spawn( + browser, + [aDescription, index], + (description, aIndex) => { + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateOnFocusMoveTester( + description, + aIndex, + content.window + ); + return content.wrappedJSObject.runner.prepareToRun( + content.document.querySelector("div") + ); + } + ); + checker.prepareToCheck(expectedData, tipWrapper); + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.run(); + }); + checker.check(expectedData); + + if (checker.canTestOpenCloseState(expectedData)) { + for (const defaultOpenState of [false, true]) { + const expectedOpenStateData = await SpecialPowers.spawn( + browser, + [], + () => { + return content.wrappedJSObject.runner.prepareToRunOpenCloseTest( + content.document.querySelector("div") + ); + } + ); + checker.prepareToCheckOpenCloseTest( + defaultOpenState, + expectedOpenStateData + ); + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runOpenCloseTest(); + }); + checker.checkOpenCloseTest(expectedOpenStateData); + } + } + await SpecialPowers.spawn(browser, [], () => { + content.wrappedJSObject.runner.destroy(); + content.wrappedJSObject.runner = undefined; + }); + checker.destroy(); + } // for loop iterating test of IMEStateOnFocusMoveTester + } // definition of runIMEStateOnFocusMoveTests + + // test for contentEditable="true" + await SpecialPowers.spawn(browser, [], async () => { + content.document + .querySelector("div") + .setAttribute("contenteditable", "true"); + }); + await runIMEStateOnFocusMoveTests("in div[contenteditable]"); + + // test for contentEditable="false" + await SpecialPowers.spawn(browser, [], async () => { + content.document + .querySelector("div") + .setAttribute("contenteditable", "false"); + }); + await runIMEStateOnFocusMoveTests('in div[contenteditable="false"]'); + } + ); +}); diff --git a/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_readonly_change_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_readonly_change_in_remote_content.js new file mode 100644 index 0000000000..33217d1d2c --- /dev/null +++ b/widget/tests/browser/browser_test_ime_state_in_contenteditable_on_readonly_change_in_remote_content.js @@ -0,0 +1,261 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ +/* import-globals-from ../file_test_ime_state_in_contenteditable_on_readonly_change.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_in_contenteditable_on_readonly_change.js", + this +); +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html", + async function (browser) { + const tipWrapper = new TIPWrapper(window); + ok( + tipWrapper.isAvailable(), + "TextInputProcessor should've been initialized" + ); + + await (async function test_ime_state_in_contenteditable_on_readonly_change() { + const expectedDataOfInitialization = await SpecialPowers.spawn( + browser, + [], + () => { + content.document.body.innerHTML = "<div contenteditable><br></div>"; + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateInContentEditableOnReadonlyChangeTester(); + const editingHost = content.document.querySelector( + "div[contenteditable]" + ); + return content.wrappedJSObject.runner.prepareToRun( + editingHost, + editingHost, + content.window + ); + } + ); + const tester = new IMEStateInContentEditableOnReadonlyChangeTester(); + tester.checkResultOfPreparation( + expectedDataOfInitialization, + window, + tipWrapper + ); + const expectedDataOfReadonly = await SpecialPowers.spawn( + browser, + [], + () => { + return content.wrappedJSObject.runner.runToMakeHTMLEditorReadonly(); + } + ); + tester.checkResultOfMakingHTMLEditorReadonly(expectedDataOfReadonly); + const expectedDataOfEditable = await SpecialPowers.spawn( + browser, + [], + () => { + return content.wrappedJSObject.runner.runToMakeHTMLEditorEditable(); + } + ); + tester.checkResultOfMakingHTMLEditorEditable(expectedDataOfEditable); + const expectedDataOfFinalization = await SpecialPowers.spawn( + browser, + [], + () => { + return content.wrappedJSObject.runner.runToRemoveContentEditableAttribute(); + } + ); + tester.checkResultOfRemovingContentEditableAttribute( + expectedDataOfFinalization + ); + tester.clear(); + })(); + + await (async function test_ime_state_in_button_in_contenteditable_on_readonly_change() { + const expectedDataOfInitialization = await SpecialPowers.spawn( + browser, + [], + () => { + content.document.body.innerHTML = + "<div contenteditable><br><button>button</button></div>"; + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateInContentEditableOnReadonlyChangeTester(); + const editingHost = content.document.querySelector( + "div[contenteditable]" + ); + return content.wrappedJSObject.runner.prepareToRun( + editingHost, + editingHost.querySelector("button"), + content.window + ); + } + ); + const tester = new IMEStateInContentEditableOnReadonlyChangeTester(); + tester.checkResultOfPreparation( + expectedDataOfInitialization, + window, + tipWrapper + ); + const expectedDataOfReadonly = await SpecialPowers.spawn( + browser, + [], + () => { + return content.wrappedJSObject.runner.runToMakeHTMLEditorReadonly(); + } + ); + tester.checkResultOfMakingHTMLEditorReadonly(expectedDataOfReadonly); + const expectedDataOfEditable = await SpecialPowers.spawn( + browser, + [], + () => { + return content.wrappedJSObject.runner.runToMakeHTMLEditorEditable(); + } + ); + tester.checkResultOfMakingHTMLEditorEditable(expectedDataOfEditable); + const expectedDataOfFinalization = await SpecialPowers.spawn( + browser, + [], + () => { + return content.wrappedJSObject.runner.runToRemoveContentEditableAttribute(); + } + ); + tester.checkResultOfRemovingContentEditableAttribute( + expectedDataOfFinalization + ); + tester.clear(); + })(); + + await (async function test_ime_state_of_text_controls_in_contenteditable_on_readonly_change() { + const tester = + new IMEStateOfTextControlInContentEditableOnReadonlyChangeTester(); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.innerHTML = "<div contenteditable></div>"; + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateOfTextControlInContentEditableOnReadonlyChangeTester(); + }); + for ( + let index = 0; + index < + IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.numberOfTextControlTypes; + index++ + ) { + const expectedDataOfInitialization = await SpecialPowers.spawn( + browser, + [index], + aIndex => { + const editingHost = content.document.querySelector("div"); + return content.wrappedJSObject.runner.prepareToRun( + aIndex, + editingHost, + content.window + ); + } + ); + tester.checkResultOfPreparation( + expectedDataOfInitialization, + window, + tipWrapper + ); + const expectedDataOfMakingParentEditingHost = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeParentEditingHost(); + }); + tester.checkResultOfMakingParentEditingHost( + expectedDataOfMakingParentEditingHost + ); + const expectedDataOfMakingHTMLEditorReadonly = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeHTMLEditorReadonly(); + }); + tester.checkResultOfMakingHTMLEditorReadonly( + expectedDataOfMakingHTMLEditorReadonly + ); + const expectedDataOfMakingHTMLEditorEditable = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeHTMLEditorEditable(); + }); + tester.checkResultOfMakingHTMLEditorEditable( + expectedDataOfMakingHTMLEditorEditable + ); + const expectedDataOfMakingParentNonEditable = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeParentNonEditingHost(); + }); + tester.checkResultOfMakingParentNonEditable( + expectedDataOfMakingParentNonEditable + ); + tester.clear(); + } + })(); + + await (async function test_ime_state_outside_contenteditable_on_readonly_change() { + const tester = + new IMEStateOutsideContentEditableOnReadonlyChangeTester(); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.innerHTML = "<div contenteditable></div>"; + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateOutsideContentEditableOnReadonlyChangeTester(); + }); + for ( + let index = 0; + index < + IMEStateOutsideContentEditableOnReadonlyChangeTester.numberOfFocusTargets; + index++ + ) { + const expectedDataOfInitialization = await SpecialPowers.spawn( + browser, + [index], + aIndex => { + const editingHost = content.document.querySelector("div"); + return content.wrappedJSObject.runner.prepareToRun( + aIndex, + editingHost, + content.window + ); + } + ); + tester.checkResultOfPreparation( + expectedDataOfInitialization, + window, + tipWrapper + ); + const expectedDataOfMakingParentEditingHost = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeParentEditingHost(); + }); + tester.checkResultOfMakingParentEditingHost( + expectedDataOfMakingParentEditingHost + ); + const expectedDataOfMakingHTMLEditorReadonly = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeHTMLEditorReadonly(); + }); + tester.checkResultOfMakingHTMLEditorReadonly( + expectedDataOfMakingHTMLEditorReadonly + ); + const expectedDataOfMakingHTMLEditorEditable = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeHTMLEditorEditable(); + }); + tester.checkResultOfMakingHTMLEditorEditable( + expectedDataOfMakingHTMLEditorEditable + ); + const expectedDataOfMakingParentNonEditable = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeParentNonEditingHost(); + }); + tester.checkResultOfMakingParentNonEditable( + expectedDataOfMakingParentNonEditable + ); + tester.clear(); + } + })(); + } + ); +}); diff --git a/widget/tests/browser/browser_test_ime_state_in_designMode_on_focus_move_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_designMode_on_focus_move_in_remote_content.js new file mode 100644 index 0000000000..5ea5990e96 --- /dev/null +++ b/widget/tests/browser/browser_test_ime_state_in_designMode_on_focus_move_in_remote_content.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ +/* import-globals-from ../file_test_ime_state_on_focus_move.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_focus_move.js", + this +); +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html", + async function (browser) { + const tipWrapper = new TIPWrapper(window); + ok( + tipWrapper.isAvailable(), + "TextInputProcessor should've been initialized" + ); + + // isnot is used in file_test_ime_state_on_focus_move.js, but it's not + // defined as the alias of Assert.notEqual in browser-chrome tests. + // Therefore, we need to define it here. + // eslint-disable-next-line no-unused-vars + const isnot = Assert.notEqual; + + async function runIMEStateOnFocusMoveTests(aDescription) { + await (async function test_IMEState_without_focused_element() { + const checker = new IMEStateWhenNoActiveElementTester(aDescription); + const expectedData = await SpecialPowers.spawn( + browser, + [aDescription], + description => { + const runner = + content.wrappedJSObject.createIMEStateWhenNoActiveElementTester( + description + ); + return runner.run(content.document, content.window); + } + ); + checker.check(expectedData); + })(); + for ( + let index = 0; + index < IMEStateOnFocusMoveTester.numberOfTests; + ++index + ) { + const checker = new IMEStateOnFocusMoveTester(aDescription, index); + const expectedData = await SpecialPowers.spawn( + browser, + [aDescription, index], + (description, aIndex) => { + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateOnFocusMoveTester( + description, + aIndex, + content.window + ); + return content.wrappedJSObject.runner.prepareToRun( + content.document.querySelector("div") + ); + } + ); + checker.prepareToCheck(expectedData, tipWrapper); + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.run(); + }); + checker.check(expectedData); + + if (checker.canTestOpenCloseState(expectedData)) { + for (const defaultOpenState of [false, true]) { + const expectedOpenStateData = await SpecialPowers.spawn( + browser, + [], + () => { + return content.wrappedJSObject.runner.prepareToRunOpenCloseTest( + content.document.querySelector("div") + ); + } + ); + checker.prepareToCheckOpenCloseTest( + defaultOpenState, + expectedOpenStateData + ); + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runOpenCloseTest(); + }); + checker.checkOpenCloseTest(expectedOpenStateData); + } + } + await SpecialPowers.spawn(browser, [], () => { + content.wrappedJSObject.runner.destroy(); + content.wrappedJSObject.runner = undefined; + }); + checker.destroy(); + } // for loop iterating test of IMEStateOnFocusMoveTester + } // definition of runIMEStateOnFocusMoveTests + + // test designMode + await SpecialPowers.spawn(browser, [], async () => { + content.document.designMode = "on"; + }); + await runIMEStateOnFocusMoveTests('in designMode="on"'); + await SpecialPowers.spawn(browser, [], async () => { + content.document.designMode = "off"; + }); + await runIMEStateOnFocusMoveTests('in designMode="off"'); + } + ); +}); diff --git a/widget/tests/browser/browser_test_ime_state_in_plugin_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_plugin_in_remote_content.js new file mode 100644 index 0000000000..0862b51080 --- /dev/null +++ b/widget/tests/browser/browser_test_ime_state_in_plugin_in_remote_content.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js", + this +); +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/toolkit/content/tests/browser/file_empty.html", + async function (browser) { + const tipWrapper = new TIPWrapper(window); + ok( + tipWrapper.isAvailable(), + "TextInputProcessor should've been initialized" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.wrappedJSObject.waitForIMEContentObserverSendingNotifications = + () => { + return new content.window.Promise(resolve => + content.window.requestAnimationFrame(() => + content.window.requestAnimationFrame(resolve) + ) + ); + }; + content.document.body.innerHTML = + '<input><object type="application/x-test"></object>'; + }); + + await SpecialPowers.spawn(browser, [], () => { + content.document.activeElement?.blur(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when no element has focus" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME should not have focus when no element has focus" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("object").focus(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when an <object> for plugin has focus" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME enabled state should not have focus when an <object> for plugin has focus" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("object").blur(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when an <object> for plugin gets blurred" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME should not have focus when an <object> for plugin gets blurred" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("object").focus(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when an <object> for plugin gets focused again" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME should not have focus when an <object> for plugin gets focused again" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("object").remove(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when focused <object> for plugin is removed from the document" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME should not have focus when focused <object> for plugin is removed from the document" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("input").focus(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + "IME enabled state should be enabled after <input> gets focus" + ); + ok( + tipWrapper.IMEHasFocus, + "IME should have focus after <input> gets focus" + ); + } + ); +}); diff --git a/widget/tests/browser/browser_test_ime_state_in_text_control_on_reframe_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_in_text_control_on_reframe_in_remote_content.js new file mode 100644 index 0000000000..4d8acab1ff --- /dev/null +++ b/widget/tests/browser/browser_test_ime_state_in_text_control_on_reframe_in_remote_content.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ +/* import-globals-from ../file_test_ime_state_in_text_control_on_reframe.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_in_text_control_on_reframe.js", + this +); +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html", + async function (browser) { + const tipWrapper = new TIPWrapper(window); + ok( + tipWrapper.isAvailable(), + "TextInputProcessor should've been initialized" + ); + + await (async function test_ime_state_outside_contenteditable_on_readonly_change() { + const tester = new IMEStateInTextControlOnReframeTester(); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.innerHTML = "<div contenteditable></div>"; + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateInTextControlOnReframeTester(); + }); + for ( + let index = 0; + index < IMEStateInTextControlOnReframeTester.numberOfTextControlTypes; + index++ + ) { + tipWrapper.clearFocusBlurNotifications(); + const expectedData1 = await SpecialPowers.spawn( + browser, + [index], + aIndex => { + return content.wrappedJSObject.runner.prepareToRun( + aIndex, + content.document, + content.window + ); + } + ); + tipWrapper.typeA(); + await SpecialPowers.spawn(browser, [], () => { + return new Promise(resolve => + content.window.requestAnimationFrame(() => + content.window.requestAnimationFrame(resolve) + ) + ); + }); + tester.checkResultAfterTypingA(expectedData1, window, tipWrapper); + + const expectedData2 = await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.prepareToRun2(); + }); + tipWrapper.typeA(); + await SpecialPowers.spawn(browser, [], () => { + return new Promise(resolve => + content.window.requestAnimationFrame(() => + content.window.requestAnimationFrame(resolve) + ) + ); + }); + tester.checkResultAfterTypingA2(expectedData2); + tester.clear(); + } + })(); + } + ); +}); diff --git a/widget/tests/browser/browser_test_ime_state_on_editable_state_change_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_on_editable_state_change_in_remote_content.js new file mode 100644 index 0000000000..8c38f97b72 --- /dev/null +++ b/widget/tests/browser/browser_test_ime_state_on_editable_state_change_in_remote_content.js @@ -0,0 +1,297 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js", + this +); +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/toolkit/content/tests/browser/file_empty.html", + async function (browser) { + const tipWrapper = new TIPWrapper(window); + ok( + tipWrapper.isAvailable(), + "TextInputProcessor should've been initialized" + ); + + await SpecialPowers.spawn(browser, [], () => { + content.wrappedJSObject.waitForIMEContentObserverSendingNotifications = + () => { + return new content.window.Promise(resolve => + content.window.requestAnimationFrame(() => + content.window.requestAnimationFrame(resolve) + ) + ); + }; + content.wrappedJSObject.resetIMEStateWithFocusMove = () => { + const input = content.document.createElement("input"); + content.document.body.appendChild(input); + input.focus(); + input.remove(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }; + content.document.body.innerHTML = "<div></div>"; + }); + + function resetIMEStateWithFocusMove() { + return SpecialPowers.spawn(browser, [], () => { + const input = content.document.createElement("input"); + content.document.body.appendChild(input); + input.focus(); + input.remove(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + } + + await (async function test_setting_contenteditable_of_focused_div() { + await SpecialPowers.spawn(browser, [], () => { + const div = content.document.querySelector("div"); + div.setAttribute("tabindex", "0"); + div.focus(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "test_setting_contenteditable_of_focused_div: IME should be disabled when non-editable <div> has focus" + ); + await SpecialPowers.spawn(browser, [], () => { + content.document + .querySelector("div") + .setAttribute("contenteditable", ""); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + "test_setting_contenteditable_of_focused_div: IME should be enabled when contenteditable of focused <div> is set" + ); + ok( + tipWrapper.IMEHasFocus, + "test_setting_contenteditable_of_focused_div: IME should have focus when contenteditable of focused <div> is set" + ); + await SpecialPowers.spawn(browser, [], () => { + content.document + .querySelector("div") + .removeAttribute("contenteditable"); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "test_setting_contenteditable_of_focused_div: IME should be disabled when contenteditable of focused <div> is removed" + ); + ok( + !tipWrapper.IMEHasFocus, + "test_setting_contenteditable_of_focused_div: IME should not have focus when contenteditable of focused <div> is removed" + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("div").removeAttribute("tabindex"); + }); + })(); + + await resetIMEStateWithFocusMove(); + + await (async function test_removing_contenteditable_of_non_last_editable_div() { + await SpecialPowers.spawn(browser, [], async () => { + const div = content.document.querySelector("div"); + div.setAttribute("tabindex", "0"); + div.setAttribute("contenteditable", ""); + const anotherEditableDiv = content.document.createElement("div"); + anotherEditableDiv.setAttribute("contenteditable", ""); + div.parentElement.appendChild(anotherEditableDiv); + div.focus(); + await content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + div.removeAttribute("contenteditable"); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "test_removing_contenteditable_of_non_last_editable_div: IME should be disabled when contenteditable of focused <div> is removed" + ); + ok( + !tipWrapper.IMEHasFocus, + "test_removing_contenteditable_of_non_last_editable_div: IME should not have focus when contenteditable of focused <div> is removed" + ); + await SpecialPowers.spawn(browser, [], () => { + const divs = content.document.querySelectorAll("div"); + divs[1].remove(); + divs[0].removeAttribute("tabindex"); + }); + })(); + + await resetIMEStateWithFocusMove(); + + await (async function test_setting_designMode() { + await SpecialPowers.spawn(browser, [], () => { + content.window.focus(); + content.document.designMode = "on"; + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + 'test_setting_designMode: IME should be enabled when designMode is set to "on"' + ); + ok( + tipWrapper.IMEHasFocus, + 'test_setting_designMode: IME should have focus when designMode is set to "on"' + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.designMode = "off"; + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + 'test_setting_designMode: IME should be disabled when designMode is set to "off"' + ); + ok( + !tipWrapper.IMEHasFocus, + 'test_setting_designMode: IME should not have focus when designMode is set to "off"' + ); + })(); + + await resetIMEStateWithFocusMove(); + + async function test_setting_content_editable_of_body_when_shadow_DOM_has_focus( + aMode + ) { + await SpecialPowers.spawn(browser, [aMode], mode => { + const div = content.document.querySelector("div"); + const shadow = div.attachShadow({ mode }); + content.wrappedJSObject.divInShadow = + content.document.createElement("div"); + content.wrappedJSObject.divInShadow.setAttribute("tabindex", "0"); + shadow.appendChild(content.wrappedJSObject.divInShadow); + content.wrappedJSObject.divInShadow.focus(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${aMode}): IME should be disabled when non-editable <div> in a shadow DOM has focus` + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.setAttribute("contenteditable", ""); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + // todo_is because of bug 1807597. Gecko does not update focus when focused + // element becomes an editable child. Therefore, cannot initialize + // HTMLEditor with the new editing host. + todo_is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${aMode}): IME should be enabled when the <body> becomes editable` + ); + todo( + tipWrapper.IMEHasFocus, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${aMode}): IME should have focus when the <body> becomes editable` + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.removeAttribute("contenteditable"); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus)${aMode}): IME should be disabled when the <body> becomes not editable` + ); + ok( + !tipWrapper.IMEHasFocus, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus)${aMode}): IME should not have focus when the <body> becomes not editable` + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("div").remove(); + content.document.body.appendChild( + content.document.createElement("div") + ); + }); + } + + async function test_setting_designMode_when_shadow_DOM_has_focus(aMode) { + await SpecialPowers.spawn(browser, [aMode], mode => { + const div = content.document.querySelector("div"); + const shadow = div.attachShadow({ mode }); + content.wrappedJSObject.divInShadow = + content.document.createElement("div"); + content.wrappedJSObject.divInShadow.setAttribute("tabindex", "0"); + shadow.appendChild(content.wrappedJSObject.divInShadow); + content.wrappedJSObject.divInShadow.focus(); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should be disabled when non-editable <div> in a shadow DOM has focus` + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.designMode = "on"; + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should stay disabled when designMode is set` + ); + ok( + !tipWrapper.IMEHasFocus, + `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should not have focus when designMode is set` + ); + await SpecialPowers.spawn(browser, [], () => { + content.wrappedJSObject.divInShadow.setAttribute( + "contenteditable", + "" + ); + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + // todo_is because of bug 1807597. Gecko does not update focus when focused + // document is into the design mode. Therefore, cannot initialize + // HTMLEditor with the document node properly. + todo_is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should be enabled when focused <div> in a shadow DOM becomes editable` + ); + todo( + tipWrapper.IMEHasFocus, + `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should have focus when focused <div> in a shadow DOM becomes editable` + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.designMode = "off"; + return content.wrappedJSObject.waitForIMEContentObserverSendingNotifications(); + }); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should be disabled when designMode is unset` + ); + ok( + !tipWrapper.IMEHasFocus, + `test_setting_designMode_when_shadow_DOM_has_focus(${aMode}): IME should not have focus when designMode is unset` + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("div").remove(); + content.document.body.appendChild( + content.document.createElement("div") + ); + }); + } + + for (const mode of ["open", "closed"]) { + await test_setting_content_editable_of_body_when_shadow_DOM_has_focus( + mode + ); + await resetIMEStateWithFocusMove(); + await test_setting_designMode_when_shadow_DOM_has_focus(mode); + await resetIMEStateWithFocusMove(); + } + } + ); +}); diff --git a/widget/tests/browser/browser_test_ime_state_on_focus_move_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_on_focus_move_in_remote_content.js new file mode 100644 index 0000000000..3916d3d47c --- /dev/null +++ b/widget/tests/browser/browser_test_ime_state_on_focus_move_in_remote_content.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ +/* import-globals-from ../file_test_ime_state_on_focus_move.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_focus_move.js", + this +); +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html", + async function (browser) { + const tipWrapper = new TIPWrapper(window); + ok( + tipWrapper.isAvailable(), + "TextInputProcessor should've been initialized" + ); + + // isnot is used in file_test_ime_state_on_focus_move.js, but it's not + // defined as the alias of Assert.notEqual in browser-chrome tests. + // Therefore, we need to define it here. + // eslint-disable-next-line no-unused-vars + const isnot = Assert.notEqual; + + async function runIMEStateOnFocusMoveTests(aDescription) { + await (async function test_IMEState_without_focused_element() { + const checker = new IMEStateWhenNoActiveElementTester(aDescription); + const expectedData = await SpecialPowers.spawn( + browser, + [aDescription], + description => { + const runner = + content.wrappedJSObject.createIMEStateWhenNoActiveElementTester( + description + ); + return runner.run(content.document, content.window); + } + ); + checker.check(expectedData); + })(); + for ( + let index = 0; + index < IMEStateOnFocusMoveTester.numberOfTests; + ++index + ) { + const checker = new IMEStateOnFocusMoveTester(aDescription, index); + const expectedData = await SpecialPowers.spawn( + browser, + [aDescription, index], + (description, aIndex) => { + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateOnFocusMoveTester( + description, + aIndex, + content.window + ); + return content.wrappedJSObject.runner.prepareToRun( + content.document.querySelector("div") + ); + } + ); + checker.prepareToCheck(expectedData, tipWrapper); + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.run(); + }); + checker.check(expectedData); + + if (checker.canTestOpenCloseState(expectedData)) { + for (const defaultOpenState of [false, true]) { + const expectedOpenStateData = await SpecialPowers.spawn( + browser, + [], + () => { + return content.wrappedJSObject.runner.prepareToRunOpenCloseTest( + content.document.querySelector("div") + ); + } + ); + checker.prepareToCheckOpenCloseTest( + defaultOpenState, + expectedOpenStateData + ); + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runOpenCloseTest(); + }); + checker.checkOpenCloseTest(expectedOpenStateData); + } + } + await SpecialPowers.spawn(browser, [], () => { + content.wrappedJSObject.runner.destroy(); + content.wrappedJSObject.runner = undefined; + }); + checker.destroy(); + } // for loop iterating test of IMEStateOnFocusMoveTester + } // definition of runIMEStateOnFocusMoveTests + + // test for normal contents. + await runIMEStateOnFocusMoveTests("in non-editable container"); + + // test for removing contentEditable + await SpecialPowers.spawn(browser, [], async () => { + content.document + .querySelector("div") + .setAttribute("contenteditable", "true"); + content.document.querySelector("div").focus(); + await new Promise(resolve => + content.window.requestAnimationFrame(() => + content.window.requestAnimationFrame(resolve) + ) + ); + content.document + .querySelector("div") + .removeAttribute("contenteditable"); + }); + await runIMEStateOnFocusMoveTests( + "after removing contenteditable from the container" + ); + } + ); +}); diff --git a/widget/tests/browser/browser_test_ime_state_on_input_type_change_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_on_input_type_change_in_remote_content.js new file mode 100644 index 0000000000..2a4bc4c332 --- /dev/null +++ b/widget/tests/browser/browser_test_ime_state_on_input_type_change_in_remote_content.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ +/* import-globals-from ../file_test_ime_state_on_input_type_change.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_input_type_change.js", + this +); +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html", + async function (browser) { + const tipWrapper = new TIPWrapper(window); + ok( + tipWrapper.isAvailable(), + "TextInputProcessor should've been initialized" + ); + + for ( + let srcIndex = 0; + srcIndex < IMEStateOnInputTypeChangeTester.numberOfTests; + srcIndex++ + ) { + const tester = new IMEStateOnInputTypeChangeTester(srcIndex); + for ( + let destIndex = 0; + destIndex < IMEStateOnInputTypeChangeTester.numberOfTests; + destIndex++ + ) { + const expectedResultBefore = await SpecialPowers.spawn( + browser, + [srcIndex, destIndex], + (aSrcIndex, aDestIndex) => { + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateOnInputTypeChangeTester( + aSrcIndex + ); + return content.wrappedJSObject.runner.prepareToRun( + aDestIndex, + content.window, + content.document.body + ); + } + ); + if (expectedResultBefore === false) { + continue; + } + tester.checkBeforeRun(expectedResultBefore, tipWrapper); + const expectedResult = await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.run(); + }); + tester.checkResult(expectedResultBefore, expectedResult); + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.clear(); + }); + tipWrapper.clearFocusBlurNotifications(); + } + tester.clear(); + } + } + ); +}); diff --git a/widget/tests/browser/browser_test_ime_state_on_readonly_change_in_remote_content.js b/widget/tests/browser/browser_test_ime_state_on_readonly_change_in_remote_content.js new file mode 100644 index 0000000000..a0c0019328 --- /dev/null +++ b/widget/tests/browser/browser_test_ime_state_on_readonly_change_in_remote_content.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ +/* import-globals-from ../file_test_ime_state_on_readonly_change.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/widget/tests/browser/file_test_ime_state_on_readonly_change.js", + this +); +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/widget/tests/browser/file_ime_state_tests.html", + async function (browser) { + const tipWrapper = new TIPWrapper(window); + ok( + tipWrapper.isAvailable(), + "TextInputProcessor should've been initialized" + ); + + const tester = new IMEStateOnReadonlyChangeTester(); + for ( + let i = 0; + i < IMEStateOnReadonlyChangeTester.numberOfTextControlTypes; + i++ + ) { + const expectedResultBefore = await SpecialPowers.spawn( + browser, + [i], + aIndex => { + content.wrappedJSObject.runner = + content.wrappedJSObject.createIMEStateOnReadonlyChangeTester( + aIndex + ); + return content.wrappedJSObject.runner.prepareToRun( + aIndex, + content.window, + content.document.body + ); + } + ); + tester.checkBeforeRun(expectedResultBefore, tipWrapper); + const expectedResultOfMakingTextControlReadonly = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeTextControlReadonly(); + }); + tester.checkResultOfMakingTextControlReadonly( + expectedResultOfMakingTextControlReadonly + ); + const expectedResultOfMakingTextControlEditable = + await SpecialPowers.spawn(browser, [], () => { + return content.wrappedJSObject.runner.runToMakeTextControlEditable(); + }); + tester.checkResultOfMakingTextControlEditable( + expectedResultOfMakingTextControlEditable + ); + tipWrapper.clearFocusBlurNotifications(); + tester.clear(); + } + } + ); +}); diff --git a/widget/tests/browser/browser_test_scrollbar_colors.js b/widget/tests/browser/browser_test_scrollbar_colors.js new file mode 100644 index 0000000000..2152412071 --- /dev/null +++ b/widget/tests/browser/browser_test_scrollbar_colors.js @@ -0,0 +1,146 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +add_task(async () => { + const URL_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + URL_ROOT + "helper_scrollbar_colors.html" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + ChromeUtils.defineESModuleGetters(this, { + WindowsVersionInfo: + "resource://gre/modules/components-utils/WindowsVersionInfo.sys.mjs", + }); + + Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js", + this + ); + + // == Native theme == + + const WIN_REFERENCES = [ + // Yellow background + ["255,255,0", 6889], + // Blue scrollbar face + ["0,0,255", 540], + // Cyan scrollbar track + ["0,255,255", 2487], + ]; + + const MAC_REFERENCES = [ + // Yellow background + ["255,255,0", 7225], + // Blue scrollbar face + ["0,0,255", 416], + // Cyan scrollbar track + ["0,255,255", 1760], + ]; + + // Values have been updated from 8100, 720, 1180 for linux1804 + const LINUX_REFERENCES = [ + // Yellow background + ["255,255,0", 7744], + // Blue scrollbar face + ["0,0,255", 1104], + // Cyan scrollbar track + ["0,255,255", 1152], + ]; + + // == Non-native theme == + + const WIN10_NNT_REFERENCES = [ + // Yellow background + ["255,255,0", 6889], + // Blue scrollbar face + ["0,0,255", 612], + // Cyan scrollbar track + ["0,255,255", 2355], + ]; + + const WIN11_NNT_REFERENCES = [ + // Yellow background + ["255,255,0", 6889], + // Blue scrollbar face + ["0,0,255", 324], + // Cyan scrollbar track + ["0,255,255", 2787], + ]; + + const MAC_NNT_REFERENCES = MAC_REFERENCES; + + const LINUX_NNT_REFERENCES = [ + // Yellow background + ["255,255,0", 7744], + // Blue scrollbar face + ["0,0,255", 368], + // Cyan scrollbar track + ["0,255,255", 1852], + ]; + + function countPixels(canvas) { + let result = new Map(); + let ctx = canvas.getContext("2d"); + let image = ctx.getImageData(0, 0, canvas.width, canvas.height); + let data = image.data; + let size = image.width * image.height; + for (let i = 0; i < size; i++) { + let key = data.subarray(i * 4, i * 4 + 3).toString(); + let value = result.get(key); + value = value ? value : 0; + result.set(key, value + 1); + } + return result; + } + + let outer = content.document.querySelector(".outer"); + let outerRect = outer.getBoundingClientRect(); + if ( + outerRect.width == outer.clientWidth && + outerRect.height == outer.clientHeight + ) { + ok(true, "Using overlay scrollbar, skip this test"); + return; + } + content.document.querySelector("#style").textContent = ` + .outer { scrollbar-color: blue cyan; } + `; + + let canvas = snapshotRect(content.window, outerRect); + let stats = countPixels(canvas); + let isNNT = SpecialPowers.getBoolPref("widget.non-native-theme.enabled"); + + let references; + if (content.navigator.platform.startsWith("Win")) { + if (!isNNT) { + references = WIN_REFERENCES; + } else if (WindowsVersionInfo.get().buildNumber >= 22000) { + // Windows 11 NNT + references = WIN11_NNT_REFERENCES; + } else { + // Windows 10 NNT + references = WIN10_NNT_REFERENCES; + } + } else if (content.navigator.platform.startsWith("Mac")) { + references = isNNT ? MAC_NNT_REFERENCES : MAC_REFERENCES; + } else if (content.navigator.platform.startsWith("Linux")) { + references = isNNT ? LINUX_NNT_REFERENCES : LINUX_REFERENCES; + } else { + ok(false, "Unsupported platform"); + } + for (let [color, count] of references) { + let value = stats.get(color); + is(value, count, `Pixel count of color ${color}`); + } + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/widget/tests/browser/browser_test_swipe_gesture.js b/widget/tests/browser/browser_test_swipe_gesture.js new file mode 100644 index 0000000000..0ac85d80c8 --- /dev/null +++ b/widget/tests/browser/browser_test_swipe_gesture.js @@ -0,0 +1,1274 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +async function waitForWhile() { + await new Promise(resolve => { + requestIdleCallback(resolve, { timeout: 300 }); + }); + await new Promise(r => requestAnimationFrame(r)); +} + +requestLongerTimeout(2); + +add_task(async () => { + // Set the default values for an OS that supports swipe to nav, except for + // pixel-size which varies by OS, we vary it in differente tests in this file. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + // Set the velocity-contribution to 0 so we can exactly control the + // values in the swipe tracker via the delta in the events that we send. + ["widget.swipe.success-velocity-contribution", 0.0], + ["widget.swipe.pixel-size", 550.0], + ], + }); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + // and we cannot go forward to the next page. + ok(!gBrowser.webNavigation.canGoForward); + + let wheelEventCount = 0; + tab.linkedBrowser.addEventListener("wheel", () => { + wheelEventCount++; + }); + + // Send a pan that starts a navigate back but doesn't have enough delta to do + // anything. Don't send the pan end because we want to check the opacity + // before the MSD animation in SwipeTracker starts which can temporarily put + // us at 1 opacity. + await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 0.9); + await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 0.9); + + // Check both getComputedStyle instead of element.style.opacity because we use a transition on the opacity. + let computedOpacity = window + .getComputedStyle(gHistorySwipeAnimation._prevBox) + .getPropertyValue("opacity"); + is(computedOpacity, "1", "opacity of prevbox is 1"); + let opacity = gHistorySwipeAnimation._prevBox.style.opacity; + is(opacity, "", "opacity style isn't explicitly set"); + + const isTranslatingIcon = + Services.prefs.getIntPref( + "browser.swipe.navigation-icon-start-position", + 0 + ) != 0 || + Services.prefs.getIntPref( + "browser.swipe.navigation-icon-end-position", + 0 + ) != 0; + if (isTranslatingIcon != 0) { + isnot( + window + .getComputedStyle(gHistorySwipeAnimation._prevBox) + .getPropertyValue("translate"), + "none", + "translate of prevbox is not `none` during gestures" + ); + } + + await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 0.9); + + // NOTE: We only get a wheel event for the beginPhase, rest of events have + // been captured by the swipe gesture module. + is(wheelEventCount, 1, "Received a wheel event"); + + await waitForWhile(); + // Make sure any navigation didn't happen. + is(tab.linkedBrowser.currentURI.spec, secondPage); + + // Try to navigate backward. + wheelEventCount = 0; + let startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + firstPage + ); + let stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + firstPage + ); + await panLeftToRight(tab.linkedBrowser, 100, 100, 1); + // NOTE: We only get a wheel event for the beginPhase, rest of events have + // been captured by the swipe gesture module. + is(wheelEventCount, 1, "Received a wheel event"); + + // The element.style opacity will be 0 because we set it to 0 on successful navigation, however + // we have a tranisition on it so the computed style opacity will still be 1 because the transition hasn't started yet. + computedOpacity = window + .getComputedStyle(gHistorySwipeAnimation._prevBox) + .getPropertyValue("opacity"); + ok(computedOpacity == 1, "computed opacity of prevbox is 1"); + opacity = gHistorySwipeAnimation._prevBox.style.opacity; + ok(opacity == 0, "element.style opacity of prevbox 0"); + + if (isTranslatingIcon) { + // We don't have a transition for translate property so that we still have + // some amount of translate. + isnot( + window + .getComputedStyle(gHistorySwipeAnimation._prevBox) + .getPropertyValue("translate"), + "none", + "translate of prevbox is not `none` during the opacity transition" + ); + } + + // Make sure the gesture triggered going back to the previous page. + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoForward); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Same test as above but pixel-size is increased and the multipliers passed to panLeftToRight correspondingly increased. +add_task(async () => { + // Set the default values for an OS that supports swipe to nav, except for + // pixel-size which varies by OS, we vary it in differente tests + // in this file. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + // Set the velocity-contribution to 0 so we can exactly control the + // values in the swipe tracker via the delta in the events that we send. + ["widget.swipe.success-velocity-contribution", 0.0], + ["widget.swipe.pixel-size", 1100.0], + ], + }); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + // and we cannot go forward to the next page. + ok(!gBrowser.webNavigation.canGoForward); + + let wheelEventCount = 0; + tab.linkedBrowser.addEventListener("wheel", () => { + wheelEventCount++; + }); + + // Send a pan that starts a navigate back but doesn't have enough delta to do + // anything. Don't send the pan end because we want to check the opacity + // before the MSD animation in SwipeTracker starts which can temporarily put + // us at 1 opacity. + await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 1.8); + await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 1.8); + + // Check both getComputedStyle instead of element.style.opacity because we use a transition on the opacity. + let computedOpacity = window + .getComputedStyle(gHistorySwipeAnimation._prevBox) + .getPropertyValue("opacity"); + is(computedOpacity, "1", "opacity of prevbox is 1"); + let opacity = gHistorySwipeAnimation._prevBox.style.opacity; + is(opacity, "", "opacity style isn't explicitly set"); + + await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 1.8); + + // NOTE: We only get a wheel event for the beginPhase, rest of events have + // been captured by the swipe gesture module. + is(wheelEventCount, 1, "Received a wheel event"); + + await waitForWhile(); + // Make sure any navigation didn't happen. + is(tab.linkedBrowser.currentURI.spec, secondPage); + + // Try to navigate backward. + wheelEventCount = 0; + let startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + firstPage + ); + let stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + firstPage + ); + await panLeftToRight(tab.linkedBrowser, 100, 100, 2); + // NOTE: We only get a wheel event for the beginPhase, rest of events have + // been captured by the swipe gesture module. + is(wheelEventCount, 1, "Received a wheel event"); + + // The element.style opacity will be 0 because we set it to 0 on successful navigation, however + // we have a tranisition on it so the computed style opacity will still be 1 because the transition hasn't started yet. + computedOpacity = window + .getComputedStyle(gHistorySwipeAnimation._prevBox) + .getPropertyValue("opacity"); + ok(computedOpacity == 1, "computed opacity of prevbox is 1"); + opacity = gHistorySwipeAnimation._prevBox.style.opacity; + ok(opacity == 0, "element.style opacity of prevbox 0"); + + // Make sure the gesture triggered going back to the previous page. + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoForward); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async () => { + // Set the default values for an OS that supports swipe to nav, except for + // pixel-size which varies by OS, we vary it in different tests + // in this file. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + // Set the velocity-contribution to 1 (default 0.05f) so velocity is a + // large contribution to the success value in SwipeTracker.cpp so it + // pushes us into success territory without going into success territory + // purely from th deltas. + ["widget.swipe.success-velocity-contribution", 2.0], + ["widget.swipe.pixel-size", 550.0], + ], + }); + + async function runTest() { + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + // and we cannot go forward to the next page. + ok(!gBrowser.webNavigation.canGoForward); + + let wheelEventCount = 0; + tab.linkedBrowser.addEventListener("wheel", () => { + wheelEventCount++; + }); + + let startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + firstPage + ); + let stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + firstPage + ); + let startTime = performance.now(); + await panLeftToRight(tab.linkedBrowser, 100, 100, 0.2); + let endTime = performance.now(); + + // If sending the events took too long then we might not have been able + // to generate enough velocity. + // The value 230 was picked based on try runs, in particular test verify + // runs on mac were the long pole, and when we get times near this we can + // still achieve the required velocity. + if (endTime - startTime > 230) { + BrowserTestUtils.removeTab(tab); + return false; + } + + // NOTE: We only get a wheel event for the beginPhase, rest of events have + // been captured by the swipe gesture module. + is(wheelEventCount, 1, "Received a wheel event"); + + // The element.style opacity will be 0 because we set it to 0 on successful navigation, however + // we have a tranisition on it so the computed style opacity will still be 1 because the transition hasn't started yet. + let computedOpacity = window + .getComputedStyle(gHistorySwipeAnimation._prevBox) + .getPropertyValue("opacity"); + ok(computedOpacity == 1, "computed opacity of prevbox is 1"); + let opacity = gHistorySwipeAnimation._prevBox.style.opacity; + ok(opacity == 0, "element.style opacity of prevbox 0"); + + // Make sure the gesture triggered going back to the previous page. + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoForward); + + BrowserTestUtils.removeTab(tab); + + return true; + } + + let numTries = 15; + while (numTries > 0) { + await new Promise(r => requestAnimationFrame(r)); + await new Promise(resolve => requestIdleCallback(resolve)); + await new Promise(r => requestAnimationFrame(r)); + + // runTest return value indicates if test was able to run to the end. + if (await runTest()) { + break; + } + numTries--; + } + ok(numTries > 0, "never ran the test"); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async () => { + // Set the default values for an OS that supports swipe to nav, except for + // pixel-size which varies by OS, we vary it in differente tests + // in this file. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + // Set the velocity-contribution to 0 so we can exactly control the + // values in the swipe tracker via the delta in the events that we send. + ["widget.swipe.success-velocity-contribution", 0.0], + ["widget.swipe.pixel-size", 550.0], + ], + }); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + // and we cannot go forward to the next page. + ok(!gBrowser.webNavigation.canGoForward); + + let startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + firstPage + ); + let stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + firstPage + ); + await panLeftToRight(tab.linkedBrowser, 100, 100, 2); + + // Make sure the gesture triggered going back to the previous page. + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoForward); + + while ( + gHistorySwipeAnimation._prevBox != null || + gHistorySwipeAnimation._nextBox != null + ) { + await new Promise(r => requestAnimationFrame(r)); + } + + ok( + gHistorySwipeAnimation._prevBox == null && + gHistorySwipeAnimation._nextBox == null + ); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + // Set the velocity-contribution to 0 so we can exactly control the + // values in the swipe tracker via the delta in the events that we send. + ["widget.swipe.success-velocity-contribution", 0.0], + ["widget.swipe.pixel-size", 550.0], + ], + }); + + function swipeGestureEndPromise() { + return new Promise(resolve => { + let promiseObserver = { + handleEvent(aEvent) { + switch (aEvent.type) { + case "MozSwipeGestureEnd": + gBrowser.tabbox.removeEventListener( + "MozSwipeGestureEnd", + promiseObserver, + true + ); + resolve(); + break; + } + }, + }; + gBrowser.tabbox.addEventListener( + "MozSwipeGestureEnd", + promiseObserver, + true + ); + }); + } + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + // and we cannot go forward to the next page. + ok(!gBrowser.webNavigation.canGoForward); + + let numSwipeGestureEndEvents = 0; + var anObserver = { + handleEvent(aEvent) { + switch (aEvent.type) { + case "MozSwipeGestureEnd": + numSwipeGestureEndEvents++; + break; + } + }, + }; + + gBrowser.tabbox.addEventListener("MozSwipeGestureEnd", anObserver, true); + + let gestureEndPromise = swipeGestureEndPromise(); + + is( + numSwipeGestureEndEvents, + 0, + "expected no MozSwipeGestureEnd got " + numSwipeGestureEndEvents + ); + + // Send a pan that starts a navigate back but doesn't have enough delta to do + // anything. + await panLeftToRight(tab.linkedBrowser, 100, 100, 0.9); + + await waitForWhile(); + // Make sure any navigation didn't happen. + is(tab.linkedBrowser.currentURI.spec, secondPage); + // end event comes after a swipe that does not navigate + await gestureEndPromise; + is( + numSwipeGestureEndEvents, + 1, + "expected one MozSwipeGestureEnd got " + numSwipeGestureEndEvents + ); + + // Try to navigate backward. + let startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + firstPage + ); + let stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + firstPage + ); + + gestureEndPromise = swipeGestureEndPromise(); + + await panLeftToRight(tab.linkedBrowser, 100, 100, 1); + + // Make sure the gesture triggered going back to the previous page. + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoForward); + + await gestureEndPromise; + + is( + numSwipeGestureEndEvents, + 2, + "expected one MozSwipeGestureEnd got " + (numSwipeGestureEndEvents - 1) + ); + + gBrowser.tabbox.removeEventListener("MozSwipeGestureEnd", anObserver, true); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async () => { + // success-velocity-contribution is very high and pixel-size is + // very low so that one swipe goes over the threshold asap. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + ["widget.swipe.success-velocity-contribution", 999999.0], + ["widget.swipe.pixel-size", 1.0], + ], + }); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + // and we cannot go forward to the next page. + ok(!gBrowser.webNavigation.canGoForward); + + // Navigate backward. + let startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + firstPage + ); + let stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + firstPage + ); + + await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 100); + + ok(gHistorySwipeAnimation._prevBox != null, "should have prevbox"); + let transitionCancelPromise = new Promise(resolve => { + gHistorySwipeAnimation._prevBox.addEventListener( + "transitioncancel", + event => { + if ( + event.propertyName == "opacity" && + event.target == gHistorySwipeAnimation._prevBox + ) { + resolve(); + } + }, + { once: true } + ); + }); + let transitionStartPromise = new Promise(resolve => { + gHistorySwipeAnimation._prevBox.addEventListener( + "transitionstart", + event => { + if ( + event.propertyName == "opacity" && + event.target == gHistorySwipeAnimation._prevBox + ) { + resolve(); + } + }, + { once: true } + ); + }); + + await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 100); + await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 100); + + // Make sure the gesture triggered going back to the previous page. + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoForward); + + await Promise.any([transitionStartPromise, transitionCancelPromise]); + + await TestUtils.waitForCondition(() => { + return ( + gHistorySwipeAnimation._prevBox == null && + gHistorySwipeAnimation._nextBox == null + ); + }); + + // Navigate forward and check the forward navigation icon box state. + startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + secondPage + ); + stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + secondPage + ); + + await panRightToLeftBegin(tab.linkedBrowser, 100, 100, 100); + + ok(gHistorySwipeAnimation._nextBox != null, "should have nextbox"); + transitionCancelPromise = new Promise(resolve => { + gHistorySwipeAnimation._nextBox.addEventListener( + "transitioncancel", + event => { + if ( + event.propertyName == "opacity" && + event.target == gHistorySwipeAnimation._nextBox + ) { + resolve(); + } + } + ); + }); + transitionStartPromise = new Promise(resolve => { + gHistorySwipeAnimation._nextBox.addEventListener( + "transitionstart", + event => { + if ( + event.propertyName == "opacity" && + event.target == gHistorySwipeAnimation._nextBox + ) { + resolve(); + } + } + ); + }); + + await panRightToLeftUpdate(tab.linkedBrowser, 100, 100, 100); + await panRightToLeftEnd(tab.linkedBrowser, 100, 100, 100); + + // Make sure the gesture triggered going forward to the next page. + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoBack); + + await Promise.any([transitionStartPromise, transitionCancelPromise]); + + await TestUtils.waitForCondition(() => { + return ( + gHistorySwipeAnimation._nextBox == null && + gHistorySwipeAnimation._prevBox == null + ); + }); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// A simple test case on RTL. +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + ["widget.swipe.success-velocity-contribution", 0.5], + ["intl.l10n.pseudo", "bidi"], + ], + }); + + const newWin = await BrowserTestUtils.openNewBrowserWindow(); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + newWin.gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage); + + // Make sure we can go back to the previous page. + ok(newWin.gBrowser.webNavigation.canGoBack); + // and we cannot go forward to the next page. + ok(!newWin.gBrowser.webNavigation.canGoForward); + + // Make sure that our gesture support stuff has been initialized in the new + // browser window. + await TestUtils.waitForCondition(() => { + return newWin.gHistorySwipeAnimation.active; + }); + + // Try to navigate backward. + let startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + firstPage + ); + let stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + firstPage + ); + await panRightToLeft(tab.linkedBrowser, 100, 100, 1); + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(newWin.gBrowser.webNavigation.canGoForward); + + // Now try to navigate forward again. + startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + secondPage + ); + stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + secondPage + ); + await panLeftToRight(tab.linkedBrowser, 100, 100, 1); + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(newWin.gBrowser.webNavigation.canGoBack); + + await BrowserTestUtils.closeWindow(newWin); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + ["widget.swipe.success-velocity-contribution", 0.5], + ["apz.overscroll.enabled", true], + ["apz.test.logging_enabled", true], + ], + }); + + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:about", + true /* waitForLoad */ + ); + + const URL_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + URL_ROOT + "helper_swipe_gesture.html" + ); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false /* includeSubFrames */, + URL_ROOT + "helper_swipe_gesture.html" + ); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + // Set `overscroll-behavior-x: contain` and flush it. + content.document.documentElement.style.overscrollBehaviorX = "contain"; + content.document.documentElement.getBoundingClientRect(); + await content.wrappedJSObject.promiseApzFlushedRepaints(); + }); + + // Start a pan gesture but keep touching. + await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 2); + + // Flush APZ pending requests to make sure the pan gesture has been processed. + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.promiseApzFlushedRepaints(); + }); + + const isOverscrolled = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + const scrollId = SpecialPowers.DOMWindowUtils.getViewId( + content.document.scrollingElement + ); + const data = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData(); + return data.additionalData.some(entry => { + return ( + entry.key == scrollId && + entry.value.split(",").includes("overscrolled") + ); + }); + } + ); + + ok(isOverscrolled, "The root scroller should have overscrolled"); + + // Finish the pan gesture. + await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 2); + await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 2); + + // And wait a while to give a chance to navigate. + await waitForWhile(); + + // Make sure any navigation didn't happen. + is(tab.linkedBrowser.currentURI.spec, URL_ROOT + "helper_swipe_gesture.html"); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// A test case to make sure the short circuit path for swipe-to-navigations in +// APZ works, i.e. cases where we know for sure that the target APZC for a given +// pan-start event isn't scrollable in the pan-start event direction. +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["apz.overscroll.enabled", true], + ], + }); + + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:about", + true /* waitForLoad */ + ); + + const URL_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + URL_ROOT + "helper_swipe_gesture.html" + ); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false /* includeSubFrames */, + URL_ROOT + "helper_swipe_gesture.html" + ); + + // Make sure the content can allow both of overscrolling and + // swipe-to-navigations. + const overscrollBehaviorX = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + return content.window.getComputedStyle(content.document.documentElement) + .overscrollBehaviorX; + } + ); + is(overscrollBehaviorX, "auto"); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + + // Start a pan gesture but keep touching. + await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 2); + + // The above pan event should invoke a SwipeGestureStart event immediately so + // that the swipe-to-navigation icon box should be uncollapsed to show it. + ok(!gHistorySwipeAnimation._prevBox.collapsed); + + // Finish the pan gesture, i.e. sending a pan-end event, otherwise a new + // pan-start event in the next will also generate a pan-interrupt event which + // will break the test. + await panLeftToRightUpdate(tab.linkedBrowser, 100, 100, 2); + await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 2); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + ["widget.swipe.success-velocity-contribution", 0.5], + ["apz.overscroll.enabled", true], + ["apz.test.logging_enabled", true], + ], + }); + + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:about", + true /* waitForLoad */ + ); + + const URL_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + URL_ROOT + "helper_swipe_gesture.html" + ); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false /* includeSubFrames */, + URL_ROOT + "helper_swipe_gesture.html" + ); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + + // Start a pan gesture but keep touching. + await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 2); + + // Flush APZ pending requests to make sure the pan gesture has been processed. + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.promiseApzFlushedRepaints(); + }); + + const isOverscrolled = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + const scrollId = SpecialPowers.DOMWindowUtils.getViewId( + content.document.scrollingElement + ); + const data = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData(); + return data.additionalData.some(entry => { + return entry.key == scrollId && entry.value.includes("overscrolled"); + }); + } + ); + + ok(!isOverscrolled, "The root scroller should not have overscrolled"); + + await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 0); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + ["widget.swipe.success-velocity-contribution", 0.5], + ], + }); + + // Load three pages and go to the second page so that it can be navigated + // to both back and forward. + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:about", + true /* waitForLoad */ + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:mozilla"); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false /* includeSubFrames */, + "about:mozilla" + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:home"); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false /* includeSubFrames */, + "about:home" + ); + + gBrowser.goBack(); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false /* includeSubFrames */, + "about:mozilla" + ); + + // Make sure we can go back and go forward. + ok(gBrowser.webNavigation.canGoBack); + ok(gBrowser.webNavigation.canGoForward); + + // Start a history back pan gesture but keep touching. + await panLeftToRightBegin(tab.linkedBrowser, 100, 100, 1); + + ok( + !gHistorySwipeAnimation._prevBox.collapsed, + "The icon box for the previous navigation should NOT be collapsed" + ); + ok( + gHistorySwipeAnimation._nextBox.collapsed, + "The icon box for the next navigation should be collapsed" + ); + + // Pan back to the opposite direction so that the gesture should be cancelled. + // eslint-disable-next-line no-undef + await NativePanHandler.promiseNativePanEvent( + tab.linkedBrowser, + 100, + 100, + // eslint-disable-next-line no-undef + NativePanHandler.delta, + 0, + // eslint-disable-next-line no-undef + NativePanHandler.updatePhase + ); + + ok( + gHistorySwipeAnimation._prevBox.collapsed, + "The icon box for the previous navigation should be collapsed" + ); + ok( + gHistorySwipeAnimation._nextBox.collapsed, + "The icon box for the next navigation should be collapsed" + ); + + await panLeftToRightEnd(tab.linkedBrowser, 100, 100, 0); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + ["widget.swipe.success-velocity-contribution", 0.5], + ["apz.overscroll.enabled", true], + ["apz.overscroll.damping", 5.0], + ["apz.content_response_timeout", 0], + ], + }); + + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:about", + true /* waitForLoad */ + ); + + const URL_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + + // Load a horizontal scrollable content. + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + URL_ROOT + "helper_swipe_gesture.html" + ); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false /* includeSubFrames */, + URL_ROOT + "helper_swipe_gesture.html" + ); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + + // Shift the horizontal scroll position slightly to make the content + // overscrollable. + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + content.document.documentElement.scrollLeft = 1; + content.document.documentElement.getBoundingClientRect(); + await content.wrappedJSObject.promiseApzFlushedRepaints(); + }); + + // Swipe horizontally to overscroll. + await panLeftToRight(tab.linkedBrowser, 1, 100, 1); + + // Swipe again over the overscroll gutter. + await panLeftToRight(tab.linkedBrowser, 1, 100, 1); + + // Wait the overscroll gutter is restored. + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + // For some reasons using functions in apz_test_native_event_utils.js + // sometimes causes "TypeError content.wrappedJSObject.XXXX is not a + // function" error, so we observe "APZ:TransformEnd" instead of using + // promiseTransformEnd(). + await new Promise((resolve, reject) => { + SpecialPowers.Services.obs.addObserver(function observer( + subject, + topic, + data + ) { + try { + SpecialPowers.Services.obs.removeObserver(observer, topic); + resolve([subject, data]); + } catch (ex) { + SpecialPowers.Services.obs.removeObserver(observer, topic); + reject(ex); + } + }, + "APZ:TransformEnd"); + }); + }); + + // Set up an APZ aware event listener and... + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + content.document.documentElement.addEventListener("wheel", e => {}, { + passive: false, + }); + await content.wrappedJSObject.promiseApzFlushedRepaints(); + }); + + // Try to swipe back again without overscrolling to make sure swipe-navigation + // works with the APZ aware event listener. + await panLeftToRight(tab.linkedBrowser, 100, 100, 1); + + let startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + "about:about" + ); + let stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + "about:about" + ); + + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoForward); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// NOTE: This test listens wheel events so that it causes an overscroll issue +// (bug 1800022). To avoid the bug, we need to run this test case at the end +// of this file. +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"], + ["browser.gesture.swipe.right", "Browser:ForwardOrForwardDuplicate"], + ["widget.disable-swipe-tracker", false], + ["widget.swipe.velocity-twitch-tolerance", 0.0000001], + ["widget.swipe.success-velocity-contribution", 0.5], + ], + }); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, secondPage); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage); + + // Make sure we can go back to the previous page. + ok(gBrowser.webNavigation.canGoBack); + // and we cannot go forward to the next page. + ok(!gBrowser.webNavigation.canGoForward); + + let wheelEventCount = 0; + tab.linkedBrowser.addEventListener("wheel", () => { + wheelEventCount++; + }); + + // Try to navigate forward. + await panRightToLeft(tab.linkedBrowser, 100, 100, 1); + // NOTE: The last endPhase shouldn't fire a wheel event since + // its delta is zero. + is(wheelEventCount, 2, "Received 2 wheel events"); + + await waitForWhile(); + // Make sure any navigation didn't happen. + is(tab.linkedBrowser.currentURI.spec, secondPage); + + // Try to navigate backward. + wheelEventCount = 0; + let startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + firstPage + ); + let stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + firstPage + ); + await panLeftToRight(tab.linkedBrowser, 100, 100, 1); + // NOTE: We only get a wheel event for the beginPhase, rest of events have + // been captured by the swipe gesture module. + is(wheelEventCount, 1, "Received a wheel event"); + + // Make sure the gesture triggered going back to the previous page. + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoForward); + + // Now try to navigate forward again. + wheelEventCount = 0; + startLoadingPromise = BrowserTestUtils.browserStarted( + tab.linkedBrowser, + secondPage + ); + stoppedLoadingPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + secondPage + ); + await panRightToLeft(tab.linkedBrowser, 100, 100, 1); + is(wheelEventCount, 1, "Received a wheel event"); + + await Promise.all([startLoadingPromise, stoppedLoadingPromise]); + + ok(gBrowser.webNavigation.canGoBack); + + // Now try to navigate backward again but with preventDefault-ed event + // handler. + wheelEventCount = 0; + let wheelEventListener = event => { + event.preventDefault(); + }; + tab.linkedBrowser.addEventListener("wheel", wheelEventListener); + await panLeftToRight(tab.linkedBrowser, 100, 100, 1); + is(wheelEventCount, 3, "Received all wheel events"); + + await waitForWhile(); + // Make sure any navigation didn't happen. + is(tab.linkedBrowser.currentURI.spec, secondPage); + + // Now drop the event handler and disable the swipe tracker and try to swipe + // again. + wheelEventCount = 0; + tab.linkedBrowser.removeEventListener("wheel", wheelEventListener); + await SpecialPowers.pushPrefEnv({ + set: [["widget.disable-swipe-tracker", true]], + }); + + await panLeftToRight(tab.linkedBrowser, 100, 100, 1); + is(wheelEventCount, 3, "Received all wheel events"); + + await waitForWhile(); + // Make sure any navigation didn't happen. + is(tab.linkedBrowser.currentURI.spec, secondPage); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); diff --git a/widget/tests/browser/file_ime_state_tests.html b/widget/tests/browser/file_ime_state_tests.html new file mode 100644 index 0000000000..d6b63f1e52 --- /dev/null +++ b/widget/tests/browser/file_ime_state_tests.html @@ -0,0 +1,48 @@ +<!doctype html> +<html style="ime-mode: disabled;"> +<head> +<meta charset="utf-8"> +<script src="file_ime_state_test_helper.js"></script> +<script src="file_test_ime_state_in_contenteditable_on_readonly_change.js"></script> +<script src="file_test_ime_state_in_text_control_on_reframe.js"></script> +<script src="file_test_ime_state_on_focus_move.js"></script> +<script src="file_test_ime_state_on_input_type_change.js"></script> +<script src="file_test_ime_state_on_readonly_change.js"></script> +<script> +"use strict"; + +/* import-globals-from ../file_ime_state_test_helper.js */ +/* import-globals-from ../file_test_ime_state_in_contenteditable_on_readonly_change.js */ +/* import-globals-from ../file_test_ime_state_in_text_control_on_reframe.js */ +/* import-globals-from ../file_test_ime_state_on_focus_move.js */ +/* import-globals-from ../file_test_ime_state_on_input_type_change.js */ +/* import-globals-from ../file_test_ime_state_on_readonly_change.js */ + +function createIMEStateInContentEditableOnReadonlyChangeTester() { + return new IMEStateInContentEditableOnReadonlyChangeTester(); +} +function createIMEStateOfTextControlInContentEditableOnReadonlyChangeTester() { + return new IMEStateOfTextControlInContentEditableOnReadonlyChangeTester(); +} +function createIMEStateOutsideContentEditableOnReadonlyChangeTester() { + return new IMEStateOutsideContentEditableOnReadonlyChangeTester(); +} +function createIMEStateInTextControlOnReframeTester() { + return new IMEStateInTextControlOnReframeTester(); +} +function createIMEStateWhenNoActiveElementTester(aDescription) { + return new IMEStateWhenNoActiveElementTester(aDescription); +} +function createIMEStateOnFocusMoveTester(aDescription, aIndex, aWindow = window) { + return new IMEStateOnFocusMoveTester(aDescription, aIndex, aWindow); +} +function createIMEStateOnInputTypeChangeTester(aSrcIndex) { + return new IMEStateOnInputTypeChangeTester(aSrcIndex); +} +function createIMEStateOnReadonlyChangeTester() { + return new IMEStateOnReadonlyChangeTester(); +} +</script> +</head> +<body style="ime-mode: disabled;"><div style="ime-mode: disabled;"></div></body> +</html> diff --git a/widget/tests/browser/helper_scrollbar_colors.html b/widget/tests/browser/helper_scrollbar_colors.html new file mode 100644 index 0000000000..e6001906e2 --- /dev/null +++ b/widget/tests/browser/helper_scrollbar_colors.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<meta charset="UTF-8"> +<title>Test for scrollbar-*-color properties</title> +<style> + .outer { + width: 100px; + height: 100px; + background: yellow; + overflow: scroll; + } + .inner { + width: 200px; + height: 200px; + } +</style> +<style id="style"></style> +<div class="outer"> + <div class="inner"> + </div> +</div> +</html> diff --git a/widget/tests/browser/helper_swipe_gesture.html b/widget/tests/browser/helper_swipe_gesture.html new file mode 100644 index 0000000000..1fa79dbbf3 --- /dev/null +++ b/widget/tests/browser/helper_swipe_gesture.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script src="/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> +<style> +html { + overflow-x: scroll; +} +body { + margin: 0; +} +div { + height: 100vh; + width: 110vw; + background-color: blue; +} +</style> +<div></div> +</html> diff --git a/widget/tests/bug586713_window.xhtml b/widget/tests/bug586713_window.xhtml new file mode 100644 index 0000000000..c180c00235 --- /dev/null +++ b/widget/tests/bug586713_window.xhtml @@ -0,0 +1,50 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="bug586713_window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="300" + height="300" + onload="onLoad();" + title="Bug 586713 Test"> + + <menubar id="nativemenubar"> + <menu id="foo" label="Foo"> + <menupopup> + <menuitem label="FooItem0"/> + </menupopup> + </menu> + </menubar> + + <script type="application/javascript"><![CDATA[ + function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); + } + + function onTestsFinished() { + window.close(); + window.arguments[0].SimpleTest.finish(); + } + + var fooCallCount = 0; + function foo() { + fooCallCount++; + let instruction = document.createProcessingInstruction("xml-stylesheet", 'href="chrome://foo.css" type="text/css"'); + document.insertBefore(instruction, document.documentElement); + if (fooCallCount == 2) { + ok(true, "If we got here we didn't crash, excellent."); + onTestsFinished(); + } + } + + function onLoad() { + foo(); + setTimeout(() => foo(), 0); + } + ]]></script> +</window> diff --git a/widget/tests/chrome.toml b/widget/tests/chrome.toml new file mode 100644 index 0000000000..08d02d3c36 --- /dev/null +++ b/widget/tests/chrome.toml @@ -0,0 +1,200 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = [ + "empty_window.xhtml", + "clipboard_helper.js", +] + +["test_alwaysontop_focus.xhtml"] + +# Privacy relevant + +["test_bug1123480.xhtml"] +skip-if = ["win11_2009 && bits == 32"] + +["test_bug343416.xhtml"] +skip-if = ["debug"] + +["test_bug428405.xhtml"] +run-if = ["os == 'mac'"] # Cocoa widget test + +["test_bug429954.xhtml"] +support-files = ["window_bug429954.xhtml"] + +["test_bug444800.xhtml"] + +["test_bug466599.xhtml"] +run-if = ["os == 'mac'"] # Cocoa widget test + +["test_bug478536.xhtml"] +skip-if = ["true"] # Bug 561929 +support-files = ["window_bug478536.xhtml"] + +["test_bug485118.xhtml"] +run-if = ["os == 'mac'"] # Cocoa widget test + +["test_bug517396.xhtml"] +skip-if = ["verify && os == 'win'"] + +["test_bug522217.xhtml"] +tags = "fullscreen" +run-if = ["os == 'mac'"] # Cocoa widget test +support-files = ["window_bug522217.xhtml"] + +["test_bug538242.xhtml"] +support-files = ["window_bug538242.xhtml"] + +["test_bug565392.html"] +run-if = ["os == 'win'"] + +["test_bug586713.xhtml"] +run-if = ["os == 'mac'"] # Cocoa widget test +support-files = ["bug586713_window.xhtml"] + +["test_bug593307.xhtml"] +support-files = [ + "window_bug593307_offscreen.xhtml", + "window_bug593307_centerscreen.xhtml", +] + +["test_bug596600.xhtml"] +support-files = "file_bug596600.html" +run-if = ["os == 'mac'"] # Cocoa widget test + +["test_bug673301.xhtml"] +run-if = ["os == 'mac'"] # Cocoa widget test + +["test_bug760802.xhtml"] + +["test_clipboard_chrome.html"] +support-files = "file_test_clipboard.js" + +["test_clipboard_asyncGetData_chrome.html"] +support-files = "file_test_clipboard_asyncGetData.js" + +["test_clipboard_asyncSetData_chrome.html"] +support-files = "file_test_clipboard_asyncSetData.js" + +["test_clipboard_cache_chrome.html"] + +["test_clipboard_owner_chrome.html"] + +["test_composition_text_querycontent.xhtml"] +support-files = ["window_composition_text_querycontent.xhtml"] + +["test_ime_state_in_contenteditable_on_readonly_change_in_parent.html"] +support-files = [ + "file_ime_state_test_helper.js", + "file_test_ime_state_in_contenteditable_on_readonly_change.js", +] + +["test_ime_state_in_plugin_in_parent.html"] +support-files = ["file_ime_state_test_helper.js"] + +["test_ime_state_in_text_control_on_reframe_in_parent.html"] +support-files = [ + "file_ime_state_test_helper.js", + "file_test_ime_state_in_text_control_on_reframe.js", +] + +["test_ime_state_on_editable_state_change_in_parent.html"] +support-files = ["file_ime_state_test_helper.js"] + +["test_ime_state_on_focus_move_in_parent.html"] +support-files = [ + "file_ime_state_test_helper.js", + "file_test_ime_state_on_focus_move.js", +] + +["test_ime_state_on_input_type_change_in_parent.html"] +skip-if = ["true"] # Bug 1817704 +support-files = [ + "file_ime_state_test_helper.js", + "file_test_ime_state_on_input_type_change.js", +] + +["test_ime_state_on_readonly_change_in_parent.html"] +support-files = [ + "file_ime_state_test_helper.js", + "file_test_ime_state_on_readonly_change.js", +] + +["test_ime_state_others_in_parent.html"] +support-files = ["window_imestate_iframes.html"] + +["test_input_events_on_deactive_window.xhtml"] +support-files = ["file_input_events_on_deactive_window.html"] + +["test_key_event_counts.xhtml"] +run-if = ["os == 'mac'"] # Cocoa widget test + +["test_keycodes.xhtml"] + +["test_mouse_scroll.xhtml"] +run-if = ["os == 'win'"] # windows widget test +support-files = [ + "window_mouse_scroll_win.html", + "window_mouse_scroll_win_2.html", +] + +["test_native_key_bindings_mac.html"] +run-if = ["os == 'mac'"] # Cocoa widget test +skip-if = [ + "verify", +] + +["test_native_menus.xhtml"] +run-if = ["os == 'mac'"] # Cocoa widget test +support-files = ["native_menus_window.xhtml"] + +["test_panel_mouse_coords.xhtml"] +skip-if = ["os == 'win'"] # bug 1009955 + +["test_platform_colors.xhtml"] +skip-if = ["true"] # Bug 1207190 + +["test_position_on_resize.xhtml"] +skip-if = [ + "verify && os == 'win'", + "os == 'linux' && bits == 64", # Bug 1616760 +] + +["test_secure_input.html"] +run-if = ["os == 'mac'"] # Cocoa widget test +support-files = ["file_secure_input.html"] + +["test_sizemode_events.xhtml"] + +["test_standalone_native_menu.xhtml"] +run-if = ["os == 'mac'"] # Cocoa widget test +support-files = ["standalone_native_menu_window.xhtml"] + +["test_surrogate_pair_native_key_handling.xhtml"] +run-if = ["os == 'win'"] # Windows widget test + +["test_system_font_changes.xhtml"] +support-files = ["system_font_changes.xhtml"] +run-if = ["os == 'linux'"] # Currently the test works on only gtk3 + +["test_system_status_bar.xhtml"] +run-if = ["os == 'mac'"] # Cocoa widget test + +["test_taskbar_progress.xhtml"] +skip-if = [ + "os == 'linux'", + "os == 'android'", + "win10_2009 && !ccov", # Bug 1456811 +] + +["test_transferable_overflow.xhtml"] +skip-if = [ + "verify && apple_catalina", + "verify && os == 'linux'", +] + +["test_wheeltransaction.xhtml"] +support-files = ["window_wheeltransaction.xhtml"] + +# Windows +# taskbar_previews.xhtml +# window_state_windows.xhtml diff --git a/widget/tests/clipboard_helper.js b/widget/tests/clipboard_helper.js new file mode 100644 index 0000000000..96787468fb --- /dev/null +++ b/widget/tests/clipboard_helper.js @@ -0,0 +1,230 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; +const Cr = SpecialPowers.Cr; +const clipboard = SpecialPowers.Services.clipboard; +const clipboardTypes = [ + clipboard.kGlobalClipboard, + clipboard.kSelectionClipboard, + clipboard.kFindClipboard, + clipboard.kSelectionCache, +]; + +function emptyClipboardData(aType) { + // XXX gtk doesn't support emptying clipboard data which is stored from + // other application (bug 1853884). As a workaround, we set dummy data + // to the clipboard first to ensure the subsequent emptyClipboard call + // works. + if (navigator.platform.includes("Linux")) { + writeStringToClipboard("foo", "text/plain", aType); + } + + clipboard.emptyClipboard(aType); +} + +function cleanupAllClipboard() { + clipboardTypes.forEach(function (type) { + if (clipboard.isClipboardTypeSupported(type)) { + info(`cleanup clipboard ${type}`); + emptyClipboardData(type); + } + }); +} + +function generateRandomString() { + return "random number: " + Math.random(); +} + +function generateNewTransferable(aFlavor, aStr) { + let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + trans.addDataFlavor(aFlavor); + + let supportsStr = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + supportsStr.data = aStr; + trans.setTransferData(aFlavor, supportsStr); + + return trans; +} + +function addStringToTransferable(aFlavor, aStr, aTrans) { + aTrans.addDataFlavor(aFlavor); + + let supportsStr = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + supportsStr.data = aStr; + aTrans.setTransferData(aFlavor, supportsStr); +} + +function updateStringToTransferable(aFlavor, aStr, aTrans) { + let supportsStr = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + supportsStr.data = aStr; + aTrans.setTransferData(aFlavor, supportsStr); +} + +function writeStringToClipboard( + aStr, + aFlavor, + aClipboardType, + aClipboardOwner = null, + aAsync = false +) { + let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + trans.addDataFlavor(aFlavor); + + let supportsStr = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + supportsStr.data = aStr; + trans.setTransferData(aFlavor, supportsStr); + + if (aAsync) { + let request = clipboard.asyncSetData(aClipboardType); + request.setData(trans, aClipboardOwner); + return; + } + + clipboard.setData(trans, aClipboardOwner, aClipboardType); + // XXX gtk doesn't support get empty text data from clipboard, bug 1852983. + if (aStr == "" && navigator.platform.includes("Linux")) { + todo_is( + getClipboardData(aFlavor, aClipboardType), + "", + `Should get empty string on clipboard type ${aClipboardType}` + ); + } else { + is( + getClipboardData(aFlavor, aClipboardType), + // On Windows, widget adds extra data into HTML clipboard. + aFlavor == "text/html" && navigator.platform.includes("Win") + ? `<html><body>\n<!--StartFragment-->${aStr}<!--EndFragment-->\n</body>\n</html>` + : aStr, + "ensure clipboard data is set" + ); + } +} + +function writeRandomStringToClipboard( + aFlavor, + aClipboardType, + aClipboardOwner = null, + aAsync = false +) { + let randomString = generateRandomString(); + writeStringToClipboard( + randomString, + aFlavor, + aClipboardType, + aClipboardOwner, + aAsync + ); + return randomString; +} + +function getClipboardData(aFlavor, aClipboardType) { + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + trans.addDataFlavor(aFlavor); + clipboard.getData( + trans, + aClipboardType, + SpecialPowers.wrap(window).browsingContext.currentWindowContext + ); + + try { + var data = SpecialPowers.createBlankObject(); + trans.getTransferData(aFlavor, data); + return data.value.QueryInterface(SpecialPowers.Ci.nsISupportsString).data; + } catch (ex) { + // If the clipboard is empty getTransferData will throw. + return null; + } +} + +function asyncGetClipboardData(aClipboardType) { + return new Promise((resolve, reject) => { + try { + clipboard.asyncGetData( + ["text/plain", "text/html", "image/png"], + aClipboardType, + null, + SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(), + { + QueryInterface: SpecialPowers.ChromeUtils.generateQI([ + "nsIAsyncClipboardGetCallback", + ]), + // nsIAsyncClipboardGetCallback + onSuccess: SpecialPowers.wrapCallback(function ( + aAsyncGetClipboardData + ) { + resolve(aAsyncGetClipboardData); + }), + onError: SpecialPowers.wrapCallback(function (aResult) { + reject(aResult); + }), + } + ); + } catch (e) { + ok(false, `asyncGetData should not throw`); + reject(e); + } + }); +} + +function asyncClipboardRequestGetData(aRequest, aFlavor, aThrows = false) { + return new Promise((resolve, reject) => { + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + trans.addDataFlavor(aFlavor); + try { + aRequest.getData(trans, aResult => { + if (aResult != Cr.NS_OK) { + reject(aResult); + return; + } + + try { + var data = SpecialPowers.createBlankObject(); + trans.getTransferData(aFlavor, data); + resolve(data.value.QueryInterface(Ci.nsISupportsString).data); + } catch (ex) { + // XXX: should widget set empty string to transferable when there no + // data in system clipboard? + resolve(""); + } + }); + ok( + !aThrows, + `nsIAsyncGetClipboardData.getData should ${ + aThrows ? "throw" : "success" + }` + ); + } catch (e) { + ok( + aThrows, + `nsIAsyncGetClipboardData.getData should ${ + aThrows ? "throw" : "success" + }` + ); + reject(e); + } + }); +} diff --git a/widget/tests/empty_window.xhtml b/widget/tests/empty_window.xhtml new file mode 100644 index 0000000000..f0e01761d2 --- /dev/null +++ b/widget/tests/empty_window.xhtml @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Empty window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/> diff --git a/widget/tests/file_bug596600.html b/widget/tests/file_bug596600.html new file mode 100644 index 0000000000..1b178a6b68 --- /dev/null +++ b/widget/tests/file_bug596600.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<body> +Content page +</body> diff --git a/widget/tests/file_ime_state_test_helper.js b/widget/tests/file_ime_state_test_helper.js new file mode 100644 index 0000000000..0cee5c036f --- /dev/null +++ b/widget/tests/file_ime_state_test_helper.js @@ -0,0 +1,197 @@ +"use strict"; + +function IsIMEOpenStateSupported() { + // We support to control IME open state on Windows and Mac actually. However, + // we cannot test it on Mac if the current keyboard layout is not CJK. And also + // we cannot test it on Win32 if the system didn't be installed IME. So, + // currently we should not run the open state testing. + return false; +} + +/** + * @param {Node} aNode + */ +function nodeIsInShadowDOM(aNode) { + for (let node = aNode; node; node = node.parentNode) { + if (node instanceof ShadowRoot) { + return true; + } + if (node == node.parentNode) { + break; + } + } + return false; +} + +/** + * @param {Node} aNode + */ +function nodeIsInDesignMode(aNode) { + return ( + aNode.isConnected && + !nodeIsInShadowDOM(aNode) && + aNode.ownerDocument.designMode == "on" + ); +} + +/** + * param {Node} aNode + */ +function getEditingHost(aNode) { + if (nodeIsInDesignMode(aNode)) { + return aNode.ownerDocument.documentElement; + } + for ( + let element = + aNode.nodeType == Node.ELEMENT_NODE ? aNode : aNode.parentElement; + element; + element = element.parentElement + ) { + const contenteditable = element.getAttribute("contenteditable"); + if (contenteditable === "true" || contenteditable === "") { + return element; + } + if (contenteditable === "false") { + return null; + } + } + return null; +} + +/** + * @param {Node} aNode + */ +function nodeIsEditable(aNode) { + if (nodeIsInDesignMode(aNode)) { + return true; + } + if (!aNode.isConnected) { + return false; + } + return getEditingHost(aNode) != null; +} + +/** + * @param {Element} aElement + */ +function elementIsEditingHost(aElement) { + return ( + nodeIsEditable(aElement) && + (!aElement.parentElement || !getEditingHost(aElement) == aElement) + ); +} + +/** + * @returns {Element} Retrieve focused element. If focused element is a element + * in UA widget, this returns its host element. E.g., when + * a button in the controls of <audio> or <video> has focus, + * this returns the <video> or <audio>. + */ +function getFocusedElementOrUAWidgetHost() { + const focusedElement = SpecialPowers.focusManager.focusedElement; + if (SpecialPowers.wrap(focusedElement)?.containingShadowRoot?.isUAWidget()) { + return focusedElement.containingShadowRoot.host; + } + return focusedElement; +} + +class TIPWrapper { + #mTIP = null; + #mFocusBlurNotifications = []; + #mFocusBlurListener; + #mWindow; + + constructor(aWindow) { + this.#mWindow = aWindow; + this.#mTIP = Cc["@mozilla.org/text-input-processor;1"].createInstance( + Ci.nsITextInputProcessor + ); + if (!this.beginInputTransactionForTests()) { + this.#mTIP = null; + } + } + + beginInputTransactionForTests() { + return this.#mTIP.beginInputTransactionForTests( + this.#mWindow, + this.#observer.bind(this) + ); + } + + typeA() { + const AKey = new this.#mWindow.KeyboardEvent("", { + key: "a", + code: "KeyA", + keyCode: this.#mWindow.KeyboardEvent.DOM_VK_A, + }); + this.#mTIP.keydown(AKey); + this.#mTIP.keyup(AKey); + } + + isAvailable() { + return this.#mTIP != null; + } + + #observer(aTIP, aNotification) { + if (aTIP != this.#mTIP) { + return false; + } + switch (aNotification.type) { + case "request-to-commit": + this.#mTIP.commitComposition(); + break; + case "request-to-cancel": + this.#mTIP.cancelComposition(); + break; + case "notify-focus": + case "notify-blur": + this.#mFocusBlurNotifications.push(aNotification.type); + if (this.#mFocusBlurListener) { + this.#mFocusBlurListener(aNotification.type); + } + break; + } + return true; + } + + get TIP() { + return this.#mTIP; + } + + /** + * @param {Function} aListener + */ + set onIMEFocusBlur(aListener) { + this.#mFocusBlurListener = aListener; + } + + get focusBlurNotifications() { + return this.#mFocusBlurNotifications.concat(); + } + + get numberOfFocusNotifications() { + return this.#mFocusBlurNotifications.filter(t => t == "notify-focus") + .length; + } + get numberOfBlurNotifications() { + return this.#mFocusBlurNotifications.filter(t => t == "notify-blur").length; + } + + get IMEHasFocus() { + return ( + !!this.#mFocusBlurNotifications.length && + this.#mFocusBlurNotifications[this.#mFocusBlurNotifications.length - 1] == + "notify-focus" + ); + } + + clearFocusBlurNotifications() { + this.#mFocusBlurNotifications = []; + } + + destroy() { + this.#mTIP = null; + this.#mFocusBlurListener = null; + this.#mFocusBlurNotifications = []; + } +} diff --git a/widget/tests/file_input_events_on_deactive_window.html b/widget/tests/file_input_events_on_deactive_window.html new file mode 100644 index 0000000000..e733adbb9d --- /dev/null +++ b/widget/tests/file_input_events_on_deactive_window.html @@ -0,0 +1,5 @@ +<html> +<body> + this is an active window. +</body> +</html> diff --git a/widget/tests/file_secure_input.html b/widget/tests/file_secure_input.html new file mode 100644 index 0000000000..28fec7b44b --- /dev/null +++ b/widget/tests/file_secure_input.html @@ -0,0 +1 @@ +<input id="text" type"text"><input id="password" type"password"> diff --git a/widget/tests/file_test_clipboard.js b/widget/tests/file_test_clipboard.js new file mode 100644 index 0000000000..76bdbaa84d --- /dev/null +++ b/widget/tests/file_test_clipboard.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from clipboard_helper.js */ + +"use strict"; + +function getLoadContext() { + return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext); +} + +// Get clipboard data to paste. +function paste(clipboard) { + let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(getLoadContext()); + trans.addDataFlavor("text/plain"); + clipboard.getData( + trans, + Ci.nsIClipboard.kGlobalClipboard, + SpecialPowers.wrap(window).browsingContext.currentWindowContext + ); + let str = SpecialPowers.createBlankObject(); + try { + trans.getTransferData("text/plain", str); + } catch (e) { + str = ""; + } + if (str) { + str = str.value.QueryInterface(Ci.nsISupportsString); + if (str) { + str = str.data; + } + } + return str; +} + +add_setup(function init() { + cleanupAllClipboard(); +}); + +/* Test for bug 948065 */ +add_task(function test_copy() { + // Test copy. + const data = "random number: " + Math.random(); + let helper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + helper.copyString(data); + is(paste(clipboard), data, "Data was successfully copied."); + + clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard); + is(paste(clipboard), "", "Data was successfully cleared."); + + cleanupAllClipboard(); +}); + +/* Tests for bug 1834073 */ +clipboardTypes.forEach(function (clipboardType) { + if (clipboard.isClipboardTypeSupported(clipboardType)) { + add_task(function test_clipboard_apis() { + info(`Test clipboard apis for type ${clipboardType}`); + + // Set clipboard data + let str; + try { + str = writeRandomStringToClipboard("text/plain", clipboardType); + } catch (e) { + ok( + false, + `setData should not throw error for clipboard type ${clipboardType}` + ); + } + + // Test hasDataMatchingFlavors + try { + ok( + clipboard.hasDataMatchingFlavors(["text/plain"], clipboardType), + `Test hasDataMatchingFlavors for clipboard type ${clipboardType}` + ); + } catch (e) { + ok( + false, + `hasDataMatchingFlavors should not throw error for clipboard type ${clipboardType}` + ); + } + + // Test getData + try { + is( + getClipboardData("text/plain", clipboardType), + str, + `Test getData for clipboard type ${clipboardType}` + ); + } catch (e) { + ok( + false, + `getData should not throw error for clipboard type ${clipboardType}` + ); + } + }); + + add_task(function test_clipboard_set_empty_string() { + info(`Test setting empty string to type ${clipboardType}`); + + // Clear clipboard type. + clipboard.emptyClipboard(clipboardType); + is( + getClipboardData("text/plain", clipboardType), + null, + `Should get null data on clipboard type ${clipboardType}` + ); + ok( + !clipboard.hasDataMatchingFlavors(["text/plain"], clipboardType), + `Should not have text/plain flavor on clipboard ${clipboardType}` + ); + + // Set text/plain to empty string. + writeStringToClipboard("", "text/plain", clipboardType); + // XXX gtk doesn't support get empty text data from clipboard, bug 1852983. + if (navigator.platform.includes("Linux")) { + todo_is( + getClipboardData("text/plain", clipboardType), + "", + `Should get empty string on clipboard type ${clipboardType}` + ); + } else { + is( + getClipboardData("text/plain", clipboardType), + "", + `Should get empty string on clipboard type ${clipboardType}` + ); + } + // XXX android doesn't support setting empty text data to clipboard, bug 1841058. + if (navigator.userAgent.includes("Android")) { + todo_is( + clipboard.hasDataMatchingFlavors(["text/plain"], clipboardType), + true, + `Should have text/plain flavor on clipboard ${clipboardType}` + ); + } else { + ok( + clipboard.hasDataMatchingFlavors(["text/plain"], clipboardType), + `Should have text/plain flavor on clipboard ${clipboardType}` + ); + } + + // Clear all clipboard data. + cleanupAllClipboard(); + }); + } +}); diff --git a/widget/tests/file_test_clipboard_asyncGetData.js b/widget/tests/file_test_clipboard_asyncGetData.js new file mode 100644 index 0000000000..e7f2e5f0f1 --- /dev/null +++ b/widget/tests/file_test_clipboard_asyncGetData.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from clipboard_helper.js */ + +"use strict"; + +clipboardTypes.forEach(function (type) { + if (!clipboard.isClipboardTypeSupported(type)) { + add_task(async function test_clipboard_asyncGetData_not_support() { + info(`Test asyncGetData request throwing on ${type}`); + SimpleTest.doesThrow( + () => clipboard.asyncGetData(["text/plain"], type, {}), + "Passing unsupported clipboard type should throw" + ); + }); + return; + } + + add_task(async function test_clipboard_asyncGetData_throw() { + info(`Test asyncGetData request throwing on ${type}`); + SimpleTest.doesThrow( + () => clipboard.asyncGetData([], type, {}), + "Passing empty flavor list should throw" + ); + + SimpleTest.doesThrow( + () => clipboard.asyncGetData(["text/plain"], type, null), + "Passing no callback should throw" + ); + }); + + add_task(async function test_clipboard_asyncGetData_no_matched_flavor() { + info(`Test asyncGetData have no matched flavor on ${type}`); + cleanupAllClipboard(); + is(getClipboardData("text/plain", type), null, "ensure clipboard is empty"); + + writeRandomStringToClipboard("text/plain", type); + let request = await new Promise(resolve => { + clipboard.asyncGetData( + ["text/html"], + type, + null, + SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(), + { + QueryInterface: SpecialPowers.ChromeUtils.generateQI([ + "nsIAsyncClipboardGetCallback", + ]), + // nsIAsyncClipboardGetCallback + onSuccess: SpecialPowers.wrapCallback(function ( + aAsyncGetClipboardData + ) { + resolve(aAsyncGetClipboardData); + }), + } + ); + }); + isDeeply(request.flavorList, [], "Check flavorList"); + }); + + add_task(async function test_empty_data() { + info(`Test asyncGetData request with empty data on ${type}`); + cleanupAllClipboard(); + is(getClipboardData("text/plain", type), null, "ensure clipboard is empty"); + + let request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, [], "Check flavorList"); + await asyncClipboardRequestGetData(request, "text/plain", true).catch( + () => {} + ); + }); + + add_task(async function test_clipboard_asyncGetData_after_write() { + info(`Test asyncGetData request after write on ${type}`); + + let str = writeRandomStringToClipboard("text/plain", type); + let request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, ["text/plain"], "Check flavorList"); + is( + await asyncClipboardRequestGetData(request, "text/plain"), + str, + "Check data" + ); + ok(request.valid, "request should still be valid"); + // Requesting a flavor that is not in the list should throw error. + await asyncClipboardRequestGetData(request, "text/html", true).catch( + () => {} + ); + ok(request.valid, "request should still be valid"); + + // Writing a new data should invalid existing get request. + str = writeRandomStringToClipboard("text/plain", type); + await asyncClipboardRequestGetData(request, "text/plain").then( + () => { + ok(false, "asyncClipboardRequestGetData should not success"); + }, + e => { + ok(true, "asyncClipboardRequestGetData should reject"); + } + ); + ok(!request.valid, "request should no longer be valid"); + + info(`check clipboard data again`); + request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, ["text/plain"], "Check flavorList"); + is( + await asyncClipboardRequestGetData(request, "text/plain"), + str, + "Check data" + ); + + cleanupAllClipboard(); + }); + + add_task(async function test_clipboard_asyncGetData_after_empty() { + info(`Test asyncGetData request after empty on ${type}`); + + let str = writeRandomStringToClipboard("text/plain", type); + let request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, ["text/plain"], "Check flavorList"); + is( + await asyncClipboardRequestGetData(request, "text/plain"), + str, + "Check data" + ); + ok(request.valid, "request should still be valid"); + + // Empty clipboard data + emptyClipboardData(type); + is(getClipboardData("text/plain", type), null, "ensure clipboard is empty"); + + await asyncClipboardRequestGetData(request, "text/plain").then( + () => { + ok(false, "asyncClipboardRequestGetData should not success"); + }, + e => { + ok(true, "asyncClipboardRequestGetData should reject"); + } + ); + ok(!request.valid, "request should no longer be valid"); + + info(`check clipboard data again`); + request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, [], "Check flavorList"); + + cleanupAllClipboard(); + }); +}); + +add_task(async function test_html_data() { + info(`Test asyncGetData request with html data`); + + const html_str = `<img src="https://example.com/oops">`; + writeStringToClipboard(html_str, "text/html", clipboard.kGlobalClipboard); + + let request = await asyncGetClipboardData(clipboard.kGlobalClipboard); + isDeeply(request.flavorList, ["text/html"], "Check flavorList"); + is( + await asyncClipboardRequestGetData(request, "text/html"), + // On Windows, widget adds extra data into HTML clipboard. + navigator.platform.includes("Win") + ? `<html><body>\n<!--StartFragment-->${html_str}<!--EndFragment-->\n</body>\n</html>` + : html_str, + "Check data" + ); + // Requesting a flavor that is not in the list should throw error. + await asyncClipboardRequestGetData(request, "text/plain", true).catch( + () => {} + ); +}); diff --git a/widget/tests/file_test_clipboard_asyncSetData.js b/widget/tests/file_test_clipboard_asyncSetData.js new file mode 100644 index 0000000000..cceecd2c44 --- /dev/null +++ b/widget/tests/file_test_clipboard_asyncSetData.js @@ -0,0 +1,179 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from clipboard_helper.js */ + +"use strict"; + +clipboardTypes.forEach(function (type) { + if (clipboard.isClipboardTypeSupported(type)) { + clipboardTypes.forEach(function (otherType) { + if (clipboard.isClipboardTypeSupported(otherType)) { + [true, false].forEach(async function (async) { + add_task(async function test_clipboard_pending_asyncSetData() { + info( + `Test having a pending asyncSetData request on ${type} and then make a new ${ + async ? "asyncSetData" : "setData" + } request on ${otherType}` + ); + + // Create a pending asyncSetData request + let priorResult; + let priorRequest; + let priorPromise = new Promise(resolve => { + priorRequest = clipboard.asyncSetData(type, { + QueryInterface: SpecialPowers.ChromeUtils.generateQI([ + "nsIAsyncSetClipboardDataCallback", + ]), + onComplete(rv) { + priorResult = rv; + resolve(); + }, + }); + }); + + // Create a new request + let str = writeRandomStringToClipboard( + "text/plain", + otherType, + null, + async + ); + + if (type === otherType) { + info( + "The new request to the same clipboard type should cancel the prior pending request" + ); + await priorPromise; + + is( + priorResult, + Cr.NS_ERROR_ABORT, + "The pending asyncSetData request should be canceled" + ); + try { + priorRequest.setData( + generateNewTransferable("text/plain", generateRandomString()) + ); + ok( + false, + "An error should be thrown if setData is called on a canceled clipboard request" + ); + } catch (e) { + is( + e.result, + Cr.NS_ERROR_FAILURE, + "An error should be thrown if setData is called on a canceled clipboard request" + ); + } + } else { + info( + "The new request to the different clipboard type should not cancel the prior pending request" + ); + str = generateRandomString(); + priorRequest.setData( + generateNewTransferable("text/plain", str), + null + ); + await priorPromise; + + is( + priorResult, + Cr.NS_OK, + "The pending asyncSetData request should success" + ); + + try { + priorRequest.setData( + generateNewTransferable("text/plain", generateRandomString()) + ); + ok( + false, + "Calling setData multiple times should throw an error" + ); + } catch (e) { + is( + e.result, + Cr.NS_ERROR_FAILURE, + "Calling setData multiple times should throw an error" + ); + } + } + + // Test clipboard data. + is( + getClipboardData("text/plain", type), + str, + `Test clipboard data for type ${type}` + ); + + // Clean clipboard data. + cleanupAllClipboard(); + }); + }); + } + }); + + add_task(async function test_clipboard_asyncSetData_abort() { + info(`Test abort asyncSetData request on ${type}`); + + // Create a pending asyncSetData request + let result; + let request = clipboard.asyncSetData(type, rv => { + result = rv; + }); + + // Abort with NS_OK. + try { + request.abort(Cr.NS_OK); + ok(false, "Throw an error when attempting to abort with NS_OK"); + } catch (e) { + is( + e.result, + Cr.NS_ERROR_FAILURE, + "Should throw an error when attempting to abort with NS_OK" + ); + } + is(result, undefined, "The asyncSetData request should not be canceled"); + + // Abort with NS_ERROR_ABORT. + request.abort(Cr.NS_ERROR_ABORT); + is( + result, + Cr.NS_ERROR_ABORT, + "The asyncSetData request should be canceled" + ); + try { + request.abort(Cr.NS_ERROR_FAILURE); + ok(false, "Throw an error when attempting to abort again"); + } catch (e) { + is( + e.result, + Cr.NS_ERROR_FAILURE, + "Should throw an error when attempting to abort again" + ); + } + is( + result, + Cr.NS_ERROR_ABORT, + "The callback should not be notified again" + ); + + try { + request.setData( + generateNewTransferable("text/plain", generateRandomString()) + ); + ok( + false, + "An error should be thrown if setData is called on a canceled clipboard request" + ); + } catch (e) { + is( + e.result, + Cr.NS_ERROR_FAILURE, + "An error should be thrown if setData is called on a canceled clipboard request" + ); + } + }); + } +}); diff --git a/widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js b/widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js new file mode 100644 index 0000000000..9f1ab2d305 --- /dev/null +++ b/widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js @@ -0,0 +1,616 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ + +class IMEStateInContentEditableOnReadonlyChangeTester { + // Runner only fields. + #mEditingHost; + #mFocusElement; + #mWindow; + + // Tester only fields. + #mTIPWrapper; + #mWindowUtils; + + clear() { + this.#mTIPWrapper?.clearFocusBlurNotifications(); + this.#mTIPWrapper = null; + } + + #flushPendingIMENotifications() { + return new Promise(resolve => + this.#mWindow.requestAnimationFrame(() => + this.#mWindow.requestAnimationFrame(resolve) + ) + ); + } + + #getExpectedIMEState() { + // Although if this.#mFocusElement is a <button>, its `.focus()` call + // focus it, but caret is not set into it and following typing is handled + // outside the <button>. Therefore, anyway the enabled state should be + // "enabled". + return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + } + + /** + * @param {Element} aEditingHost The editing host. + * @param {Element} aFocusElement Element which should have focus. This must + * be an inclusive descendant of the editing host and editable element. + * @param {Window} aWindow [optional] The window. + * @returns {object} Expected result of initial state. + */ + async prepareToRun(aEditingHost, aFocusElement, aWindow = window) { + this.#mWindow = aWindow; + this.#mEditingHost = aEditingHost; + this.#mFocusElement = aFocusElement; + + if (this.#mEditingHost.ownerDocument.activeElement) { + this.#mEditingHost.ownerDocument.activeElement.blur(); + await this.#flushPendingIMENotifications(); + } + + this.#mWindow.focus(); + this.#mEditingHost.setAttribute("contenteditable", ""); + this.#mFocusElement.focus(); + + await this.#flushPendingIMENotifications(); + + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when initialized with setting focus to ${ + this.#mFocusElement == this.#mEditingHost + ? "the editing host" + : `<${this.#mFocusElement.tagName.toLowerCase()}>` + }`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + /** + * @param {object} aExpectedResult The expected result of the test. + */ + #checkResult(aExpectedResult) { + const description = `IMEStateInContentEditableOnReadonlyChangeTester`; + is( + this.#mWindowUtils.IMEStatus, + aExpectedResult.expectedIMEState, + `${description}: IME enabled state should be expected one ${aExpectedResult.description}` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedResult.expectedIMEFocus, + `${description}: IME should ${ + aExpectedResult.expectedIMEFocus ? "" : "not " + }have focus ${aExpectedResult.description}` + ); + } + + /** + * @param {object} aExpectedResult The expected result of prepareToRun(). + * @param {Window} aWindow The window to check IME state. + * @param {TIPWrapper} aTIPWrapper The TIPWrapper for aWindow. + */ + checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) { + this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils; + this.#mTIPWrapper = aTIPWrapper; + this.#checkResult(aExpectedResult); + } + + /** + * @returns {object} The expected result. + */ + async runToMakeHTMLEditorReadonly() { + const htmlEditor = SpecialPowers.wrap(this.#mWindow).docShell.editor; + htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + + await this.#flushPendingIMENotifications(); + + return { + description: + this.#mFocusElement == this.#mEditingHost + ? "when the editing host has focus" + : `when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + expectedIMEFocus: false, + }; + } + + /** + * @param {object} aExpectedResult The expected result of runToMakeHTMLEditorReadonly(). + */ + checkResultOfMakingHTMLEditorReadonly(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + /** + * @returns {object} The expected result. + */ + async runToMakeHTMLEditorEditable() { + const htmlEditor = SpecialPowers.wrap(this.#mWindow).docShell.editor; + htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + + await this.#flushPendingIMENotifications(); + + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: + this.#mFocusElement == this.#mEditingHost + ? "when the editing host has focus" + : `when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + /** + * @param {object} aExpectedResult The expected result of runToMakeHTMLEditorEditable(). + */ + checkResultOfMakingHTMLEditorEditable(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + async runToRemoveContentEditableAttribute() { + this.#mEditingHost.removeAttribute("contenteditable"); + + await this.#flushPendingIMENotifications(); + + return { + description: + this.#mFocusElement == this.#mEditingHost + ? "after removing contenteditable attribute when the editing host has focus" + : `after removing contenteditable attribute when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + expectedIMEFocus: false, + }; + } + + /** + * @param {object} aExpectedResult The expected result of runToRemoveContentEditableAttribute(). + */ + checkResultOfRemovingContentEditableAttribute(aExpectedResult) { + this.#checkResult(aExpectedResult); + } +} + +class IMEStateOfTextControlInContentEditableOnReadonlyChangeTester { + static #sTextControls = [ + { + tag: "input", + type: "text", + readonly: false, + }, + { + tag: "input", + type: "text", + readonly: true, + }, + { + tag: "textarea", + readonly: false, + }, + { + tag: "textarea", + readonly: true, + }, + ]; + + static get numberOfTextControlTypes() { + return IMEStateOfTextControlInContentEditableOnReadonlyChangeTester + .#sTextControls.length; + } + + static #createElement(aDocument, aTextControl) { + const textControl = aDocument.createElement(aTextControl.tag); + if (aTextControl.type !== undefined) { + textControl.setAttribute("type", aTextControl.type); + } + if (aTextControl.readonly) { + textControl.setAttribute("readonly", ""); + } + return textControl; + } + + #getDescription() { + return `<${this.#mTextControl.tag}${ + this.#mTextControl.type !== undefined + ? ` type=${this.#mTextControl.type}` + : "" + }${this.#mTextControl.readonly ? " readonly" : ""}>`; + } + + #getExpectedIMEState() { + return this.#mTextControl.readonly + ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED + : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + } + + #flushPendingIMENotifications() { + return new Promise(resolve => + this.#mWindow.requestAnimationFrame(() => + this.#mWindow.requestAnimationFrame(resolve) + ) + ); + } + + // Runner only fields. + #mEditingHost; + #mTextControl; + #mTextControlElement; + #mWindow; + + // Checker only fields. + #mWindowUtils; + #mTIPWrapper; + + clear() { + this.#mTIPWrapper?.clearFocusBlurNotifications(); + this.#mTIPWrapper = null; + } + + /** + * @param {number} aIndex Index of the test. + * @param {Element} aEditingHost The editing host which will have a text control. + * @param {Window} aWindow [optional] The DOM window containing aEditingHost. + * @returns {object} Expected result of initial state. + */ + async prepareToRun(aIndex, aEditingHost, aWindow = window) { + this.#mWindow = aWindow; + this.#mEditingHost = aEditingHost; + this.#mEditingHost.ownerDocument.activeElement?.blur(); + this.#mEditingHost.removeAttribute("contenteditable"); + this.#mTextControlElement?.remove(); + await this.#flushPendingIMENotifications(); + this.#mTextControl = + IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.#sTextControls[ + aIndex + ]; + this.#mTextControlElement = + IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.#createElement( + this.#mEditingHost.ownerDocument, + this.#mTextControl + ); + this.#mEditingHost.appendChild(this.#mTextControlElement); + this.#mTextControlElement.focus(); + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when ${this.#getDescription()} simply has focus`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + #checkResult(aExpectedResult) { + const description = + "IMEStateOfTextControlInContentEditableOnReadonlyChangeTester"; + is( + this.#mWindowUtils.IMEStatus, + aExpectedResult.expectedIMEState, + `${description}: IME state should be proper one for the text control ${aExpectedResult.description}` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedResult.expectedIMEFocus, + `${description}: IME should ${ + aExpectedResult.expectedIMEFocus ? "" : "not " + }have focus ${aExpectedResult.description}` + ); + } + + /** + * @param {object} aExpectedResult The expected result returned by prepareToRun(). + * @param {Window} aWindow The window whose IME state should be checked. + * @param {TIPWrapper} aTIPWrapper The TIP wrapper of aWindow. + */ + checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) { + this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils; + this.#mTIPWrapper = aTIPWrapper; + this.#checkResult(aExpectedResult); + } + + async runToMakeParentEditingHost() { + this.#mEditingHost.setAttribute("contenteditable", ""); + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when parent of ${this.#getDescription()} becomes contenteditable`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + checkResultOfMakingParentEditingHost(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + async runToMakeHTMLEditorReadonly() { + const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor; + editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when HTMLEditor for parent of ${this.#getDescription()} becomes readonly`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + checkResultOfMakingHTMLEditorReadonly(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + async runToMakeHTMLEditorEditable() { + const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor; + editor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when HTMLEditor for parent of ${this.#getDescription()} becomes editable`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + checkResultOfMakingHTMLEditorEditable(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + async runToMakeParentNonEditingHost() { + this.#mEditingHost.removeAttribute("contenteditable"); + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when parent of ${this.#getDescription()} becomes non-editable`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + checkResultOfMakingParentNonEditable(aExpectedResult) { + this.#checkResult(aExpectedResult); + } +} + +class IMEStateOutsideContentEditableOnReadonlyChangeTester { + static #sFocusTargets = [ + { + tag: "input", + type: "text", + readonly: false, + }, + { + tag: "input", + type: "text", + readonly: true, + }, + { + tag: "textarea", + readonly: false, + }, + { + tag: "textarea", + readonly: true, + }, + { + tag: "button", + }, + { + tag: "body", + }, + ]; + + static get numberOfFocusTargets() { + return IMEStateOutsideContentEditableOnReadonlyChangeTester.#sFocusTargets + .length; + } + + static #maybeCreateElement(aDocument, aFocusTarget) { + if (aFocusTarget.tag == "body") { + return null; + } + const element = aDocument.createElement(aFocusTarget.tag); + if (aFocusTarget.type !== undefined) { + element.setAttribute("type", aFocusTarget.type); + } + if (aFocusTarget.readonly) { + element.setAttribute("readonly", ""); + } + return element; + } + + #getDescription() { + return `<${this.#mFocusTarget.tag}${ + this.#mFocusTarget.type !== undefined + ? ` type=${this.#mFocusTarget.type}` + : "" + }${this.#mFocusTarget.readonly ? " readonly" : ""}>`; + } + + #getExpectedIMEState() { + return this.#mFocusTarget.readonly || + this.#mFocusTarget.tag == "button" || + this.#mFocusTarget.tag == "body" + ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED + : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + } + + #flushPendingIMENotifications() { + return new Promise(resolve => + this.#mWindow.requestAnimationFrame(() => + this.#mWindow.requestAnimationFrame(resolve) + ) + ); + } + + // Runner only fields. + #mBody; + #mEditingHost; + #mFocusTarget; + #mFocusTargetElement; + #mWindow; + + // Checker only fields. + #mWindowUtils; + #mTIPWrapper; + + clear() { + this.#mTIPWrapper?.clearFocusBlurNotifications(); + this.#mTIPWrapper = null; + } + + /** + * @param {number} aIndex Index of the test. + * @param {Element} aEditingHost The editing host. + * @param {Window} aWindow [optional] The DOM window containing aEditingHost. + * @returns {object} Expected result of initial state. + */ + async prepareToRun(aIndex, aEditingHost, aWindow = window) { + this.#mWindow = aWindow; + this.#mEditingHost = aEditingHost; + this.#mEditingHost.removeAttribute("contenteditable"); + this.#mBody = this.#mEditingHost.ownerDocument.body; + this.#mBody.ownerDocument.activeElement?.blur(); + if (this.#mFocusTargetElement != this.#mBody) { + this.#mFocusTargetElement?.remove(); + } + await this.#flushPendingIMENotifications(); + this.#mFocusTarget = + IMEStateOutsideContentEditableOnReadonlyChangeTester.#sFocusTargets[ + aIndex + ]; + this.#mFocusTargetElement = + IMEStateOutsideContentEditableOnReadonlyChangeTester.#maybeCreateElement( + this.#mBody.ownerDocument, + this.#mFocusTarget + ); + if (this.#mFocusTargetElement) { + this.#mBody.appendChild(this.#mFocusTargetElement); + this.#mFocusTargetElement.focus(); + } + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when ${this.#getDescription()} simply has focus`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + #checkResult(aExpectedResult) { + const description = "IMEStateOutsideContentEditableOnReadonlyChangeTester"; + is( + this.#mWindowUtils.IMEStatus, + aExpectedResult.expectedIMEState, + `${description}: IME state should be proper one for the focused element ${aExpectedResult.description}` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedResult.expectedIMEFocus, + `${description}: IME should ${ + aExpectedResult.expectedIMEFocus ? "" : "not " + }have focus ${aExpectedResult.description}` + ); + } + + /** + * @param {object} aExpectedResult The expected result returned by prepareToRun(). + * @param {Window} aWindow The window whose IME state should be checked. + * @param {TIPWrapper} aTIPWrapper The TIP wrapper of aWindow. + */ + checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) { + this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils; + this.#mTIPWrapper = aTIPWrapper; + this.#checkResult(aExpectedResult); + } + + async runToMakeParentEditingHost() { + this.#mEditingHost.setAttribute("contenteditable", ""); + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when parent of ${this.#getDescription()} becomes contenteditable`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + checkResultOfMakingParentEditingHost(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + async runToMakeHTMLEditorReadonly() { + const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor; + editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when HTMLEditor for parent of ${this.#getDescription()} becomes readonly`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + checkResultOfMakingHTMLEditorReadonly(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + async runToMakeHTMLEditorEditable() { + const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor; + editor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when HTMLEditor for parent of ${this.#getDescription()} becomes editable`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + checkResultOfMakingHTMLEditorEditable(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + async runToMakeParentNonEditingHost() { + this.#mEditingHost.removeAttribute("contenteditable"); + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when parent of ${this.#getDescription()} becomes non-editable`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + checkResultOfMakingParentNonEditable(aExpectedResult) { + this.#checkResult(aExpectedResult); + } +} diff --git a/widget/tests/file_test_ime_state_in_text_control_on_reframe.js b/widget/tests/file_test_ime_state_in_text_control_on_reframe.js new file mode 100644 index 0000000000..719022b889 --- /dev/null +++ b/widget/tests/file_test_ime_state_in_text_control_on_reframe.js @@ -0,0 +1,190 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ + +// Bug 580388 and bug 808287 +class IMEStateInTextControlOnReframeTester { + static #sTextControls = [ + { + tag: "input", + type: "text", + }, + { + tag: "input", + type: "password", + }, + { + tag: "textarea", + }, + ]; + + static get numberOfTextControlTypes() { + return IMEStateInTextControlOnReframeTester.#sTextControls.length; + } + + #createElement() { + const textControl = this.#mDocument.createElement(this.#mTextControl.tag); + if (this.#mTextControl.type !== undefined) { + textControl.setAttribute("type", this.#mTextControl.type); + } + return textControl; + } + + #getDescription() { + return `<${this.#mTextControl.tag}${ + this.#mTextControl.type !== undefined + ? ` type=${this.#mTextControl.type}` + : "" + }>`; + } + + #getExpectedIMEState() { + return this.#mTextControl.type == "password" + ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD + : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + } + + #flushPendingIMENotifications() { + return new Promise(resolve => + this.#mWindow.requestAnimationFrame(() => + this.#mWindow.requestAnimationFrame(resolve) + ) + ); + } + + // Runner only fields. + #mTextControl; + #mTextControlElement; + #mWindow; + #mDocument; + + // Checker only fields. + #mWindowUtils; + #mTIPWrapper; + + clear() { + this.#mTIPWrapper?.clearFocusBlurNotifications(); + this.#mTIPWrapper = null; + } + + /** + * @param {number} aIndex Index of the test. + * @param {Element} aDocument The document to run the test. + * @param {Window} aWindow [optional] The DOM window for aDocument. + * @returns {object} Expected result of initial state. + */ + async prepareToRun(aIndex, aDocument, aWindow = window) { + this.#mWindow = aWindow; + this.#mDocument = aDocument; + this.#mDocument.activeElement?.blur(); + this.#mTextControlElement?.remove(); + await this.#flushPendingIMENotifications(); + this.#mTextControl = + IMEStateInTextControlOnReframeTester.#sTextControls[aIndex]; + this.#mTextControlElement = this.#createElement(); + this.#mDocument.body.appendChild(this.#mTextControlElement); + this.#mTextControlElement.focus(); + this.#mTextControlElement.style.overflow = "visible"; + this.#mTextControlElement.addEventListener( + "input", + aEvent => { + aEvent.target.style.overflow = "hidden"; + }, + { + capture: true, + } + ); + await this.#flushPendingIMENotifications(); + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when ${this.#getDescription()} has focus`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + expectedNumberOfFocusNotifications: 1, + }; + } + + #checkResult(aExpectedResult) { + const description = "IMEStateInTextControlOnReframeTester"; + is( + this.#mWindowUtils.IMEStatus, + aExpectedResult.expectedIMEState, + `${description}: IME state should be proper one for the text control ${aExpectedResult.description}` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedResult.expectedIMEFocus, + `${description}: IME should ${ + aExpectedResult.expectedIMEFocus ? "" : "not " + }have focus ${aExpectedResult.description}` + ); + if (aExpectedResult.numberOfFocusNotifications !== undefined) { + is( + this.#mTIPWrapper.numberOfFocusNotifications, + aExpectedResult.numberOfFocusNotifications, + `${description}: focus notifications should've been received ${ + this.#mTIPWrapper.numberOfFocusNotifications + } times ${aExpectedResult.description}` + ); + } + if (aExpectedResult.numberOfBlurNotifications !== undefined) { + is( + this.#mTIPWrapper.numberOfBlurNotifications, + aExpectedResult.numberOfBlurNotifications, + `${description}: blur notifications should've been received ${ + this.#mTIPWrapper.numberOfBlurNotifications + } times ${aExpectedResult.description}` + ); + } + } + + /** + * @param {object} aExpectedResult The expected result returned by prepareToRun(). + * @param {Window} aWindow The window whose IME state should be checked. + * @param {TIPWrapper} aTIPWrapper The TIP wrapper of aWindow. + */ + checkResultAfterTypingA(aExpectedResult, aWindow, aTIPWrapper) { + this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils; + this.#mTIPWrapper = aTIPWrapper; + this.#checkResult(aExpectedResult); + + this.#mTIPWrapper.clearFocusBlurNotifications(); + } + + async prepareToRun2() { + this.#mTextControlElement.addEventListener("focus", aEvent => { + // Perform a style change and flush it to trigger reframing. + aEvent.target.style.overflow = "visible"; + aEvent.target.getBoundingClientRect(); + }); + this.#mTextControlElement.blur(); + this.#mTextControlElement.focus(); + + await this.#flushPendingIMENotifications(); + + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when ${this.#getDescription()} is reframed by focus event listener`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + expectedNumberOfFocusNotifications: 1, + expectedNumberOfBlurNotifications: 1, + }; + } + + /** + * @param {object} aExpectedResult The expected result returned by prepareToRun(). + */ + checkResultAfterTypingA2(aExpectedResult) { + this.#checkResult(aExpectedResult); + + this.#mTIPWrapper.clearFocusBlurNotifications(); + } +} diff --git a/widget/tests/file_test_ime_state_on_focus_move.js b/widget/tests/file_test_ime_state_on_focus_move.js new file mode 100644 index 0000000000..f7760d8a09 --- /dev/null +++ b/widget/tests/file_test_ime_state_on_focus_move.js @@ -0,0 +1,1588 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ + +class IMEStateWhenNoActiveElementTester { + #mDescription; + + constructor(aDescription) { + this.#mDescription = aDescription; + } + + async run(aDocument, aWindow = window) { + aWindow.focus(); + aDocument.activeElement?.blur(); + + await new Promise(resolve => + requestAnimationFrame(() => requestAnimationFrame(resolve)) + ); // wait for sending IME notifications + + return { designModeValue: aDocument.designMode }; + } + + check(aExpectedData, aWindow = window) { + const winUtils = SpecialPowers.wrap(aWindow).windowUtils; + if (aExpectedData.designModeValue == "on") { + is( + winUtils.IMEStatus, + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + `IMEStateWhenNoActiveElementTester(${ + this.#mDescription + }): When no element has focus, IME should stay enabled in design mode` + ); + } else { + is( + winUtils.IMEStatus, + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `IMEStateWhenNoActiveElementTester(${ + this.#mDescription + }): When no element has focus, IME should be disabled` + ); + } + } +} + +class IMEStateOnFocusMoveTester { + // Common fields + #mDescription; + #mTest; + #mWindow; + #mWindowUtils; + + // Only runner fields + #mCreatedElement; + #mCreatedElementForPreviousFocusedElement; + #mElementToSetFocus; + #mContainerIsEditable; + + // Only checker fields + #mTIPWrapper; + + constructor(aDescription, aIndex, aWindow = window) { + this.#mTest = IMEStateOnFocusMoveTester.#sTestList[aIndex]; + this.#mDescription = `IMEStateOnFocusMoveTester(${aDescription}): ${ + this.#mTest.description + }`; + this.#mWindow = aWindow; + this.#mWindowUtils = SpecialPowers.wrap(this.#mWindow).windowUtils; + } + + /** + * prepareToRun should be called before run only in the process which will run the test. + */ + async prepareToRun(aContainer) { + const doc = aContainer.ownerDocument; + this.#mTest = this.#resolveTest(this.#mTest, aContainer); + this.#mContainerIsEditable = nodeIsEditable(aContainer); + this.#mCreatedElement = this.#mTest.createElement(doc); + const waitForLoadIfIFrame = new Promise(resolve => { + if (this.#mCreatedElement.tagName == "IFRAME") { + this.#mCreatedElement.addEventListener("load", resolve, { + capture: true, + once: true, + }); + } else { + resolve(); + } + }); + aContainer.appendChild(this.#mCreatedElement); + await waitForLoadIfIFrame; + this.#mElementToSetFocus = this.#mCreatedElement.contentDocument + ? this.#mCreatedElement.contentDocument.documentElement + : this.#mCreatedElement; + if (doc.designMode == "on") { + doc.activeElement?.blur(); + } else if (this.#mContainerIsEditable) { + getEditingHost(aContainer).focus(); // FIXME: use editing host instead + } else { + this.#mCreatedElementForPreviousFocusedElement = + doc.createElement("input"); + this.#mCreatedElementForPreviousFocusedElement.setAttribute( + "type", + this.#mTest.expectedEnabledValue == + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED + ? "password" + : "text" + ); + aContainer.appendChild(this.#mCreatedElementForPreviousFocusedElement); + this.#mCreatedElementForPreviousFocusedElement.focus(); + } + + await new Promise(resolve => + requestAnimationFrame(() => requestAnimationFrame(resolve)) + ); // wait for sending IME notifications + + return { + designModeValue: doc.designMode, + containerIsEditable: this.#mContainerIsEditable, + isFocusable: this.#mTest.isFocusable, + focusEventFired: this.#mTest.focusEventIsExpected, + enabledValue: this.#mTest.expectedEnabledValue, + testedSubDocumentInDesignMode: + this.#mCreatedElement.contentDocument?.designMode == "on", + }; + } + + /** + * prepareToCheck should be called before calling run only in the process which will check the result. + */ + prepareToCheck(aExpectedData, aTIPWrapper) { + info(`Starting ${this.#mDescription} (enable state check)...`); + this.#mTIPWrapper = aTIPWrapper; + this.#mTIPWrapper.onIMEFocusBlur = aNotificationType => { + switch (aNotificationType) { + case "notify-focus": + info(aNotificationType); + is( + this.#mWindowUtils.IMEStatus, + aExpectedData.enabledValue, + `${ + this.#mDescription + }, IME should receive a focus notification after IME state is updated` + ); + break; + case "notify-blur": + info(aNotificationType); + const changingStatus = !( + aExpectedData.containerIsEditable && + aExpectedData.enabledValue == + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED + ); + if (aExpectedData.designModeValue == "on") { + is( + // FIXME: This is odd, but #mWindowUtils.IMEStatus sometimes IME_STATUS_PASSWORD + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + aExpectedData.enabledValue, + `${ + this.#mDescription + }, IME should receive a blur notification after IME state is updated` + ); + } else if (changingStatus) { + isnot( + this.#mWindowUtils.IMEStatus, + aExpectedData.enabledValue, + `${ + this.#mDescription + }, IME should receive a blur notification BEFORE IME state is updated` + ); + } else { + is( + this.#mWindowUtils.IMEStatus, + aExpectedData.enabledValue, + `${ + this.#mDescription + }, IME should receive a blur notification and its context has expected IME state if the state isn't being changed` + ); + } + break; + } + }; + + this.#mTIPWrapper.clearFocusBlurNotifications(); + } + + /** + * @returns {bool} whether expected element has focus or not after moving focus. + */ + async run() { + const previousFocusedElement = getFocusedElementOrUAWidgetHost(); + if (this.#mTest.setFocusIntoUAWidget) { + this.#mTest.setFocusIntoUAWidget(this.#mElementToSetFocus); + } else { + this.#mElementToSetFocus.focus(); + } + + await new Promise(resolve => + requestAnimationFrame(() => requestAnimationFrame(resolve)) + ); // wait for sending IME notifications + + const currentFocusedElement = getFocusedElementOrUAWidgetHost(); + this.#mCreatedElementForPreviousFocusedElement?.remove(); + if (this.#mTest.isFocusable) { + return this.#mElementToSetFocus == currentFocusedElement; + } + return previousFocusedElement == currentFocusedElement; + } + + check(aExpectedData) { + this.#mTIPWrapper.onIMEFocusBlur = null; + + if (aExpectedData.isFocusable) { + if (aExpectedData.focusEventFired) { + if ( + aExpectedData.enabledValue == + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED || + aExpectedData.enabledValue == + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD + ) { + ok( + this.#mTIPWrapper.numberOfFocusNotifications > 0, + `${this.#mDescription}, IME should receive a focus notification` + ); + if ( + aExpectedData.designModeValue == "on" && + !aExpectedData.testedSubDocumentInDesignMode + ) { + is( + this.#mTIPWrapper.numberOfBlurNotifications, + 0, + `${ + this.#mDescription + }, IME shouldn't receive a blur notification in designMode since focus isn't moved from another editor` + ); + } else { + ok( + this.#mTIPWrapper.numberOfBlurNotifications > 0, + `${ + this.#mDescription + }, IME should receive a blur notification for the previous focused editor` + ); + } + ok( + this.#mTIPWrapper.IMEHasFocus, + `${this.#mDescription}, IME should have focus right now` + ); + } else { + is( + this.#mTIPWrapper.numberOfFocusNotifications, + 0, + `${this.#mDescription}, IME shouldn't receive a focus notification` + ); + ok( + this.#mTIPWrapper.numberOfBlurNotifications > 0, + `${this.#mDescription}, IME should receive a blur notification` + ); + ok( + !this.#mTIPWrapper.IMEHasFocus, + `${this.#mDescription}, IME shouldn't have focus right now` + ); + } + } else { + ok(true, `${this.#mDescription}, focus event should be fired`); + } + } else { + is( + this.#mTIPWrapper.numberOfFocusNotifications, + 0, + `${ + this.#mDescription + }, IME shouldn't receive a focus notification at testing non-focusable element` + ); + is( + this.#mTIPWrapper.numberOfBlurNotifications, + 0, + `${ + this.#mDescription + }, IME shouldn't receive a blur notification at testing non-focusable element` + ); + } + + is( + this.#mWindowUtils.IMEStatus, + aExpectedData.enabledValue, + `${this.#mDescription}, wrong enabled state` + ); + if ( + this.#mTest.expectedInputElementType && + aExpectedData.designModeValue != "on" + ) { + is( + this.#mWindowUtils.focusedInputType, + this.#mTest.expectedInputElementType, + `${this.#mDescription}, wrong input type` + ); + } else if (aExpectedData.designModeValue == "on") { + is( + this.#mWindowUtils.focusedInputType, + "", + `${this.#mDescription}, wrong input type` + ); + } + } + + destroy() { + this.#mCreatedElement?.remove(); + this.#mCreatedElementForPreviousFocusedElement?.remove(); + this.#mTIPWrapper?.clearFocusBlurNotifications(); + this.#mTIPWrapper = null; + } + + /** + * Open/Close state test check + * Note that these tests are not run now. + * If these tests should run between `run` and `cleanUp` call of the above + * tests. + */ + canTestOpenCloseState(aExpectedData) { + return ( + IsIMEOpenStateSupported() && + this.#mWindowUtils.IMEStatus == + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED && + aExpectedData.enabledValue == + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED + ); + } + async prepareToRunOpenCloseTest(aContainer) { + const doc = aContainer.ownerDocument; + this.#mCreatedElementForPreviousFocusedElement?.remove(); + this.#mCreatedElementForPreviousFocusedElement = doc.createElement("input"); + this.#mCreatedElementForPreviousFocusedElement.setAttribute("type", "text"); + aContainer.appendChild(this.#mCreatedElementForPreviousFocusedElement); + + this.#mContainerIsEditable = nodeIsEditable(aContainer); + this.#mCreatedElement = this.#mTest.createElement(doc); + const waitForLoadIfIFrame = new Promise(resolve => { + if (this.#mCreatedElement.tagName == "IFRAME") { + this.#mCreatedElement.addEventListener("load", resolve, { + capture: true, + once: true, + }); + } else { + resolve(); + } + }); + aContainer.appendChild(this.#mCreatedElement); + await waitForLoadIfIFrame; + this.#mElementToSetFocus = this.#mCreatedElement.contentDocument + ? this.#mCreatedElement.contentDocument.documentElement + : this.#mCreatedElement; + + this.#mCreatedElementForPreviousFocusedElement.focus(); + + return {}; + } + prepareToCheckOpenCloseTest(aPreviousOpenState, aExpectedData) { + info(`Starting ${this.#mDescription} (open/close state check)...`); + this.#mWindowUtils.IMEIsOpen = aPreviousOpenState; + aExpectedData.defaultOpenState = this.#mWindowUtils.IMEIsOpen; + } + async runOpenCloseTest() { + return this.run(); + } + checkOpenCloseTest(aExpectedData) { + const expectedOpenState = + this.#mTest.expectedOpenState != undefined + ? this.#mTest.expectedOpenState + : aExpectedData.defaultOpenState; + is( + this.#mWindowUtils.IMEIsOpen, + expectedOpenState, + `${this.#mDescription}, IME should ${ + expectedOpenState != aExpectedData.defaultOpenState ? "become" : "keep" + } ${expectedOpenState ? "open" : "closed"}` + ); + } + + /** + * Utility methods for defining kIMEStateTestList. + */ + + /** + * @param {Element} aElement + */ + static #elementIsConnectedAndNotInDesignMode(aElement) { + return aElement.isConnected && !nodeIsInDesignMode(aElement); + } + + /** + * @param {Element} aElementContainer + * @param {bool} aElementIsEditingHost + */ + static #elementIsFocusableIfEditingHost( + aElementContainer, + aElementIsEditingHost + ) { + return !nodeIsEditable(aElementContainer) && aElementIsEditingHost; + } + + static #IMEStateEnabledAlways() { + return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + } + + /** + * @param {Element} aElement + * @param {bool} aElementIsEditingHost + */ + static #IMEStateEnabledIfEditable(aElement, aElementIsEditingHost) { + return nodeIsEditable(aElement) || aElementIsEditingHost + ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED + : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED; + } + + /** + * @param {Element} aElement + */ + static #IMEStateEnabledIfInDesignMode(aElement) { + return IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode( + aElement + ) + ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED + : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + } + + /** + * @param {Element} aElement + */ + static #IMEStatePasswordIfNotInDesignMode(aElement) { + return IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode( + aElement + ) + ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD + : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + } + + /** + * @param {Element} aElement + */ + static #elementIsConnectedAndNotEditable(aElement) { + return aElement.isConnected && !nodeIsEditable(aElement); + } + + /** + * @param {Element} aElementContainer + */ + static #focusEventIsExpectedUnlessEditableChild(aElementContainer) { + return !nodeIsEditable(aElementContainer); + } + + // Form controls except text editable elements are "disable" in normal + // condition, however, if they are editable, they are "enabled". + // XXX Probably there are some bugs: If the form controls editable, they + // shouldn't be focusable. + #resolveTest(aTest, aContainer) { + const isFocusable = aTest.isFocusable( + aContainer, + aTest.isNewElementEditingHost + ); + return { + // Description of the new element + description: aTest.description, + // Create element to check IME state + createElement: aTest.createElement, + // Whether the new element is an editing host if container is not editable + isNewElementEditingHost: aTest.isNewElementEditingHost, + // If the test wants to move focus into an element in UA widget, define + // this and set focus in it. + setFocusIntoUAWidget: aTest.setFocusIntoUAWidget, + // Whether the element is focusable or not + isFocusable, + // Whether focus events are fired on the element if it's focusable + focusEventIsExpected: + isFocusable && + aTest.focusEventIsExpected(aContainer, aTest.isNewElementEditingHost), + // Expected IME enabled state when the element has focus + expectedEnabledValue: aTest.expectedEnabledValue( + aContainer, + aTest.isNewElementEditingHost + ), + // Expected IME open state when the element gets focus + // "undefined" means that IME open state should not be changed + expectedOpenState: aTest.expectedOpenState, + // Expected type of input element if it's an <input> + expectedInputElementType: aTest.expectedInputElementType, + }; + } + static #sTestList = [ + { + description: "input[type=text]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "text"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedInputElementType: "text", + }, + { + description: "input[type=text][readonly]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "text"); + element.setAttribute("readonly", ""); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + { + description: "input[type=password]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "password"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + expectedInputElementType: "password", + }, + { + description: "input[type=password][readonly]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "password"); + element.setAttribute("readonly", ""); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + { + description: "input[type=checkbox]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "checkbox"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: + IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "input[type=radio]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "radio"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: + IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "input[type=submit]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "submit"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "input[type=reset]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "reset"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "input[type=file]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "file"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: + IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "input[type=button]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "button"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "input[type=image]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "image"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "input[type=url]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "url"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedInputElementType: "url", + }, + { + description: "input[type=email]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "email"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedInputElementType: "email", + }, + { + description: "input[type=search]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "search"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedInputElementType: "search", + }, + { + description: "input[type=tel]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "tel"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedInputElementType: "tel", + }, + { + description: "input[type=number]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "number"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedInputElementType: "number", + }, + { + description: "input[type=date]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "date"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + expectedInputElementType: "date", + }, + { + description: "input[type=datetime-local]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "datetime-local"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + expectedInputElementType: "datetime-local", + }, + { + description: "input[type=time]", + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "time"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + expectedInputElementType: "time", + }, + // TODO(bug 1283382, bug 1283382): month and week + + // form controls + { + description: "button", + createElement: aDocument => aDocument.createElement("button"), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "textarea", + createElement: aDocument => aDocument.createElement("textarea"), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: "textarea[readonly]", + createElement: aDocument => { + const element = aDocument.createElement("textarea"); + element.setAttribute("readonly", ""); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + { + description: "select (dropdown list)", + createElement: aDocument => { + const select = aDocument.createElement("select"); + const option1 = aDocument.createElement("option"); + option1.textContent = "abc"; + const option2 = aDocument.createElement("option"); + option2.textContent = "def"; + const option3 = aDocument.createElement("option"); + option3.textContent = "ghi"; + select.appendChild(option1); + select.appendChild(option2); + select.appendChild(option3); + return select; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: + IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "select (list box)", + createElement: aDocument => { + const select = aDocument.createElement("select"); + select.setAttribute("multiple", "multiple"); + const option1 = aDocument.createElement("option"); + option1.textContent = "abc"; + const option2 = aDocument.createElement("option"); + option2.textContent = "def"; + const option3 = aDocument.createElement("option"); + option3.textContent = "ghi"; + select.appendChild(option1); + select.appendChild(option2); + select.appendChild(option3); + return select; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: + IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + + // a element + { + id: "a_href", + description: "a[href]", + createElement: aDocument => { + const element = aDocument.createElement("a"); + element.setAttribute("href", "about:blank"); + return element; + }, + isFocusable: IMEStateOnFocusMoveTester.#elementIsConnectedAndNotEditable, + focusEventIsExpected: + IMEStateOnFocusMoveTester.#focusEventIsExpectedUnlessEditableChild, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + + // audio element + { + description: "audio[controls]", + createElement: aDocument => { + const element = aDocument.createElement("audio"); + element.setAttribute("controls", ""); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "playButton in audio", + createElement: aDocument => { + const element = aDocument.createElement("audio"); + element.setAttribute("controls", ""); + return element; + }, + setFocusIntoUAWidget: aElement => + SpecialPowers.wrap(aElement) + .openOrClosedShadowRoot.getElementById("playButton") + .focus(), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + { + description: "scrubber in audio", + createElement: aDocument => { + const element = aDocument.createElement("audio"); + element.setAttribute("controls", ""); + return element; + }, + setFocusIntoUAWidget: aElement => + SpecialPowers.wrap(aElement) + .openOrClosedShadowRoot.getElementById("scrubber") + .focus(), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + { + description: "muteButton in audio", + createElement: aDocument => { + const element = aDocument.createElement("audio"); + element.setAttribute("controls", ""); + return element; + }, + setFocusIntoUAWidget: aElement => + SpecialPowers.wrap(aElement) + .openOrClosedShadowRoot.getElementById("muteButton") + .focus(), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + { + description: "volumeControl in audio", + createElement: aDocument => { + const element = aDocument.createElement("audio"); + element.setAttribute("controls", ""); + return element; + }, + setFocusIntoUAWidget: aElement => + SpecialPowers.wrap(aElement) + .openOrClosedShadowRoot.getElementById("volumeControl") + .focus(), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + + // video element + { + description: "video", + createElement: aDocument => { + const element = aDocument.createElement("video"); + element.setAttribute("controls", ""); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfEditable, + }, + { + description: "playButton in video", + createElement: aDocument => { + const element = aDocument.createElement("video"); + element.setAttribute("controls", ""); + return element; + }, + setFocusIntoUAWidget: aElement => + SpecialPowers.wrap(aElement) + .openOrClosedShadowRoot.getElementById("playButton") + .focus(), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + { + description: "scrubber in video", + createElement: aDocument => { + const element = aDocument.createElement("video"); + element.setAttribute("controls", ""); + return element; + }, + setFocusIntoUAWidget: aElement => + SpecialPowers.wrap(aElement) + .openOrClosedShadowRoot.getElementById("scrubber") + .focus(), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + { + description: "muteButton in video", + createElement: aDocument => { + const element = aDocument.createElement("video"); + element.setAttribute("controls", ""); + return element; + }, + setFocusIntoUAWidget: aElement => + SpecialPowers.wrap(aElement) + .openOrClosedShadowRoot.getElementById("muteButton") + .focus(), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + { + description: "volumeControl in video", + createElement: aDocument => { + const element = aDocument.createElement("video"); + element.setAttribute("controls", ""); + return element; + }, + setFocusIntoUAWidget: aElement => + SpecialPowers.wrap(aElement) + .openOrClosedShadowRoot.getElementById("volumeControl") + .focus(), + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStateEnabledIfInDesignMode, + }, + + // ime-mode + { + description: 'input[type=text][style="ime-mode: auto;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "text"); + element.setAttribute("style", "ime-mode: auto;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=text][style="ime-mode: normal;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "text"); + element.setAttribute("style", "ime-mode: normal;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=text][style="ime-mode: active;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "text"); + element.setAttribute("style", "ime-mode: active;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: true, + }, + { + description: 'input[type=text][style="ime-mode: inactive;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "text"); + element.setAttribute("style", "ime-mode: inactive;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: false, + }, + { + description: 'input[type=text][style="ime-mode: disabled;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "text"); + element.setAttribute("style", "ime-mode: disabled;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + }, + + { + description: 'input[type=url][style="ime-mode: auto;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "url"); + element.setAttribute("style", "ime-mode: auto;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=url][style="ime-mode: normal;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "url"); + element.setAttribute("style", "ime-mode: normal;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=url][style="ime-mode: active;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "url"); + element.setAttribute("style", "ime-mode: active;"); + return element; + }, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedOpenState: true, + }, + { + description: 'input[type=url][style="ime-mode: inactive;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "url"); + element.setAttribute("style", "ime-mode: inactive;"); + return element; + }, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedOpenState: false, + }, + { + description: 'input[type=url][style="ime-mode: disabled;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "url"); + element.setAttribute("style", "ime-mode: disabled;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + }, + + { + description: 'input[type=email][style="ime-mode: auto;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "email"); + element.setAttribute("style", "ime-mode: auto;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=email][style="ime-mode: normal;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "email"); + element.setAttribute("style", "ime-mode: normal;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=email][style="ime-mode: active;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "email"); + element.setAttribute("style", "ime-mode: active;"); + return element; + }, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedOpenState: true, + }, + { + description: 'input[type=email][style="ime-mode: inactive;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "email"); + element.setAttribute("style", "ime-mode: inactive;"); + return element; + }, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedOpenState: false, + }, + { + description: 'input[type=email][style="ime-mode: disabled;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "email"); + element.setAttribute("style", "ime-mode: disabled;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + }, + + { + description: 'input[type=search][style="ime-mode: auto;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "search"); + element.setAttribute("style", "ime-mode: auto;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=search][style="ime-mode: normal;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "search"); + element.setAttribute("style", "ime-mode: normal;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=search][style="ime-mode: active;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "search"); + element.setAttribute("style", "ime-mode: active;"); + return element; + }, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedOpenState: true, + }, + { + description: 'input[type=search][style="ime-mode: inactive;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "search"); + element.setAttribute("style", "ime-mode: inactive;"); + return element; + }, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedOpenState: false, + }, + { + description: 'input[type=search][style="ime-mode: disabled;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "search"); + element.setAttribute("style", "ime-mode: disabled;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + }, + + { + description: 'input[type=tel][style="ime-mode: auto;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "tel"); + element.setAttribute("style", "ime-mode: auto;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=tel][style="ime-mode: normal;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "tel"); + element.setAttribute("style", "ime-mode: normal;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=tel][style="ime-mode: active;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "tel"); + element.setAttribute("style", "ime-mode: active;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: true, + }, + { + description: 'input[type=tel][style="ime-mode: inactive;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "tel"); + element.setAttribute("style", "ime-mode: inactive;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: false, + }, + { + description: 'input[type=tel][style="ime-mode: disabled;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "tel"); + element.setAttribute("style", "ime-mode: disabled;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + }, + + { + description: 'input[type=number][style="ime-mode: auto;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "number"); + element.setAttribute("style", "ime-mode: auto;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=number][style="ime-mode: normal;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "number"); + element.setAttribute("style", "ime-mode: normal;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=number][style="ime-mode: active;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "number"); + element.setAttribute("style", "ime-mode: active;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: true, + }, + { + description: 'input[type=number][style="ime-mode: inactive;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "number"); + element.setAttribute("style", "ime-mode: inactive;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: false, + }, + { + description: 'input[type=number][style="ime-mode: disabled;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "number"); + element.setAttribute("style", "ime-mode: disabled;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + }, + + { + description: 'input[type=password][style="ime-mode: auto;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "password"); + element.setAttribute("style", "ime-mode: auto;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + }, + { + description: 'input[type=password][style="ime-mode: normal;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "password"); + element.setAttribute("style", "ime-mode: normal;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'input[type=password][style="ime-mode: active;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "password"); + element.setAttribute("style", "ime-mode: active;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: true, + }, + { + description: 'input[type=password][style="ime-mode: inactive;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "password"); + element.setAttribute("style", "ime-mode: inactive;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: false, + }, + { + description: 'input[type=password][style="ime-mode: disabled;"]', + createElement: aDocument => { + const element = aDocument.createElement("input"); + element.setAttribute("type", "password"); + element.setAttribute("style", "ime-mode: disabled;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + }, + { + description: 'textarea[style="ime-mode: auto;"]', + createElement: aDocument => { + const element = aDocument.createElement("textarea"); + element.setAttribute("style", "ime-mode: auto;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'textarea[style="ime-mode: normal;"]', + createElement: aDocument => { + const element = aDocument.createElement("textarea"); + element.setAttribute("style", "ime-mode: normal;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: 'textarea[style="ime-mode: active;"]', + createElement: aDocument => { + const element = aDocument.createElement("textarea"); + element.setAttribute("style", "ime-mode: active;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: true, + }, + { + description: 'textarea[style="ime-mode: inactive;"]', + createElement: aDocument => { + const element = aDocument.createElement("textarea"); + element.setAttribute("style", "ime-mode: inactive;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + expectedOpenState: false, + }, + { + description: 'textarea[style="ime-mode: disabled;"]', + createElement: aDocument => { + const element = aDocument.createElement("textarea"); + element.setAttribute("style", "ime-mode: disabled;"); + return element; + }, + isFocusable: + IMEStateOnFocusMoveTester.#elementIsConnectedAndNotInDesignMode, + focusEventIsExpected: () => true, + expectedEnabledValue: + IMEStateOnFocusMoveTester.#IMEStatePasswordIfNotInDesignMode, + }, + + // HTML editors + { + description: 'div[contenteditable="true"]', + createElement: aDocument => { + const div = aDocument.createElement("div"); + div.setAttribute("contenteditable", ""); + return div; + }, + isNewElementEditingHost: true, + isFocusable: IMEStateOnFocusMoveTester.#elementIsFocusableIfEditingHost, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + { + description: "designMode editor", + createElement: aDocument => { + const iframe = aDocument.createElement("iframe"); + iframe.srcdoc = "<!doctype html><html><body></body></html>"; + iframe.addEventListener( + "load", + () => (iframe.contentDocument.designMode = "on"), + { capture: true, once: true } + ); + return iframe; + }, + isFocusable: () => true, + focusEventIsExpected: () => true, + expectedEnabledValue: IMEStateOnFocusMoveTester.#IMEStateEnabledAlways, + }, + ]; + + static get numberOfTests() { + return IMEStateOnFocusMoveTester.#sTestList.length; + } +} diff --git a/widget/tests/file_test_ime_state_on_input_type_change.js b/widget/tests/file_test_ime_state_on_input_type_change.js new file mode 100644 index 0000000000..38c139d6a2 --- /dev/null +++ b/widget/tests/file_test_ime_state_on_input_type_change.js @@ -0,0 +1,315 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ + +class IMEStateOnInputTypeChangeTester { + static #sInputElementList = [ + { + type: "", + textControl: true, + }, + { + type: "text", + textControl: true, + }, + { + type: "text", + readonly: true, + textControl: true, + }, + { + type: "password", + textControl: true, + }, + { + type: "password", + readonly: true, + textControl: true, + }, + { + type: "checkbox", + buttonControl: true, + }, + { + type: "radio", + buttonControl: true, + }, + { + type: "submit", + buttonControl: true, + }, + { + type: "reset", + buttonControl: true, + }, + { + type: "file", + buttonControl: true, + }, + { + type: "button", + buttonControl: true, + }, + { + type: "image", + alt: "image", + buttonControl: true, + }, + { + type: "url", + textControl: true, + }, + { + type: "email", + textControl: true, + }, + { + type: "search", + textControl: true, + }, + { + type: "tel", + textControl: true, + }, + { + type: "number", + textControl: true, + }, + { + type: "date", + dateTimeControl: true, + }, + { + type: "datetime-local", + dateTimeControl: true, + }, + { + type: "time", + dateTimeControl: true, + }, + { + type: "range", + buttonControl: true, + }, + { + type: "color", + buttonControl: true, + }, + // TODO(bug 1283382, bug 1283382): month and week + ]; + + static get numberOfTests() { + return IMEStateOnInputTypeChangeTester.#sInputElementList.length; + } + + /** + * @param {HTMLInputElement} aInputElement The input element. + * @returns {number} Expected IME state when aInputElement has focus. + */ + static #getExpectedIMEEnabledState(aInputElement) { + if (aInputElement.readonly) { + return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED; + } + switch (aInputElement.type) { + case "text": + case "url": + case "email": + case "search": + case "tel": + case "number": + return aInputElement.style.imeMode == "disabled" + ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD + : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + + case "password": + return aInputElement.style.imeMode == "" || + aInputElement.style.imeMode == "auto" || + aInputElement.style.imeMode == "disabled" + ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD + : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + + default: + return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED; + } + } + + static #getDescription(aInputElement) { + let desc = "<input"; + if (aInputElement.getAttribute("type")) { + desc += ` type="${aInputElement.getAttribute("type")}"`; + } + if (aInputElement.readonly) { + desc += " readonly"; + } + if (aInputElement.getAttribute("style")) { + desc += ` style="ime-mode: ${aInputElement.style.imeMode}"`; + } + return desc + ">"; + } + + // Only runner fields + #mSrcTypeIndex; + #mType; + #mInputElement; + #mNewType; + #mWindow; + + // Only checker fields + #mWindowUtils; + #mTIPWrapper; + + /** + * @param {number} aSrcTypeIndex `type` attribute value index in #sInputElementList + */ + constructor(aSrcTypeIndex) { + this.#mSrcTypeIndex = aSrcTypeIndex; + this.#mType = + IMEStateOnInputTypeChangeTester.#sInputElementList[this.#mSrcTypeIndex]; + } + + clear() { + this.#mInputElement?.remove(); + this.#mTIPWrapper?.clearFocusBlurNotifications(); + this.#mTIPWrapper = null; + } + + #flushPendingIMENotifications() { + return new Promise(resolve => + this.#mWindow.requestAnimationFrame(() => + this.#mWindow.requestAnimationFrame(resolve) + ) + ); + } + + /** + * @param {number} aDestTypeIndex New type index. + * @param {Window} aWindow The window running tests. + * @param {Element} aContainer The element which should have new <input>. + * @param {undefined|string} aIMEModeValue [optional] ime-mode value if you want to specify. + * @returns {object|bool} Expected data before running tests. If the test should not run, returns false. + */ + async prepareToRun(aDestTypeIndex, aWindow, aContainer, aIMEModeValue) { + this.#mWindow = aWindow; + + if (aContainer.ownerDocument.activeElement) { + aContainer.ownerDocument.activeElement.blur(); + await this.#flushPendingIMENotifications(); + } + if (this.#mInputElement?.isConnected) { + this.#mInputElement.remove(); + } + this.#mInputElement = null; + + this.#mNewType = + IMEStateOnInputTypeChangeTester.#sInputElementList[aDestTypeIndex]; + if ( + aDestTypeIndex == this.#mSrcTypeIndex || + this.#mNewType.readonly || + (!this.#mType.textControl && !this.#mNewType.textControl) + ) { + return false; + } + this.#mInputElement = aContainer.ownerDocument.createElement("input"); + if (this.#mType.type != "") { + this.#mInputElement.setAttribute("type", this.#mType.type); + } + if (this.#mType.readonly) { + this.#mInputElement.setAttribute("readonly", ""); + } + if (aIMEModeValue && aIMEModeValue !== "") { + this.#mInputElement.setAttribute("style", `ime-mode: ${aIMEModeValue}`); + } + if (this.#mType.alt) { + this.#mInputElement.setAttribute("alt", this.#mType.alt); + } + const waitForFocus = new Promise(resolve => + this.#mInputElement.addEventListener("focus", resolve, { once: true }) + ); + aContainer.appendChild(this.#mInputElement); + this.#mInputElement.focus(); + await waitForFocus; + + await this.#flushPendingIMENotifications(); + + const expectedIMEState = + IMEStateOnInputTypeChangeTester.#getExpectedIMEEnabledState( + this.#mInputElement + ); + return { + description: IMEStateOnInputTypeChangeTester.#getDescription( + this.#mInputElement + ), + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + /** + * @param {object} aExpectedData The expected data which was returned by prepareToRun(). + * @param {TIPWrapper} aTIPWrapper TIP wrapper in aWindow. + * @param {Window} aWindow [optional] The window object of which you want to check IME state. + */ + checkBeforeRun(aExpectedData, aTIPWrapper, aWindow = window) { + this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils; + this.#mTIPWrapper = aTIPWrapper; + const description = `IMEStateOnInputTypeChangeTester(${aExpectedData.description})`; + is( + this.#mWindowUtils.IMEStatus, + aExpectedData.expectedIMEState, + `${description}: IME state should be set to expected value before running test` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedData.expectedIMEFocus, + `${description}: IME state should ${ + aExpectedData.expectedIMEFocus ? "" : "not" + }have focus before running test` + ); + } + + /** + * @returns {object} The expected results. + */ + async run() { + if (this.#mNewType.type == "") { + this.#mInputElement.removeAttribute("type"); + } else { + this.#mInputElement.setAttribute("type", this.#mNewType.type); + } + + await this.#flushPendingIMENotifications(); + + const expectedIMEState = + IMEStateOnInputTypeChangeTester.#getExpectedIMEEnabledState( + this.#mInputElement + ); + return { + newType: this.#mNewType.type, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + checkResult(aExpectedDataBeforeRun, aExpectedData) { + const description = `IMEStateOnInputTypeChangeTester(${ + aExpectedDataBeforeRun.description + } -> type="${aExpectedData.newType || ""}")`; + is( + this.#mWindowUtils.IMEStatus, + aExpectedData.expectedIMEState, + `${description}: IME state should be set to expected value` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedData.expectedIMEFocus, + `${description}: IME state should have focus` + ); + } +} diff --git a/widget/tests/file_test_ime_state_on_readonly_change.js b/widget/tests/file_test_ime_state_on_readonly_change.js new file mode 100644 index 0000000000..af21f538ba --- /dev/null +++ b/widget/tests/file_test_ime_state_on_readonly_change.js @@ -0,0 +1,242 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ + +class IMEStateOnReadonlyChangeTester { + static #sTextControlTypes = [ + { + description: "<input>", + createElement: aDoc => aDoc.createElement("input"), + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + }, + { + description: `<input type="text">`, + createElement: aDoc => { + const input = aDoc.createElement("input"); + input.setAttribute("type", "text"); + return input; + }, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + }, + { + description: `<input type="password">`, + createElement: aDoc => { + const input = aDoc.createElement("input"); + input.setAttribute("type", "password"); + return input; + }, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_PASSWORD, + }, + { + description: `<input type="url">`, + createElement: aDoc => { + const input = aDoc.createElement("input"); + input.setAttribute("type", "url"); + return input; + }, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + }, + { + description: `<input type="email">`, + createElement: aDoc => { + const input = aDoc.createElement("input"); + input.setAttribute("type", "email"); + return input; + }, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + }, + { + description: `<input type="search">`, + createElement: aDoc => { + const input = aDoc.createElement("input"); + input.setAttribute("type", "search"); + return input; + }, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + }, + { + description: `<input type="tel">`, + createElement: aDoc => { + const input = aDoc.createElement("input"); + input.setAttribute("type", "tel"); + return input; + }, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + }, + { + description: `<input type="number">`, + createElement: aDoc => { + const input = aDoc.createElement("input"); + input.setAttribute("type", "number"); + return input; + }, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + }, + { + description: `<textarea></textarea>`, + createElement: aDoc => aDoc.createElement("textarea"), + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + }, + ]; + + static get numberOfTextControlTypes() { + return IMEStateOnReadonlyChangeTester.#sTextControlTypes.length; + } + + // Only runner fields + #mTextControl; + #mTextControlElement; + #mWindow; + + // Only checker fields + #mWindowUtils; + #mTIPWrapper; + + clear() { + this.#mTextControlElement?.remove(); + this.#mTIPWrapper?.clearFocusBlurNotifications(); + this.#mTIPWrapper = null; + } + + #flushPendingIMENotifications() { + return new Promise(resolve => + this.#mWindow.requestAnimationFrame(() => + this.#mWindow.requestAnimationFrame(resolve) + ) + ); + } + + /** + * @param {number} aIndex The index of text control types. + * @param {Window} aWindow The window running tests. + * @param {Element} aContainer The element which should have new <input> or <textarea>. + * @returns {object} Expected data before running tests. + */ + async prepareToRun(aIndex, aWindow, aContainer) { + this.#mWindow = aWindow; + this.#mTextControl = + IMEStateOnReadonlyChangeTester.#sTextControlTypes[aIndex]; + + if (aContainer.ownerDocument.activeElement) { + aContainer.ownerDocument.activeElement.blur(); + await this.#flushPendingIMENotifications(); + } + if (this.#mTextControlElement?.isConnected) { + this.#mTextControlElement.remove(); + } + this.#mTextControlElement = this.#mTextControl.createElement( + aContainer.ownerDocument + ); + + const waitForFocus = new Promise(resolve => + this.#mTextControlElement.addEventListener("focus", resolve, { + once: true, + }) + ); + aContainer.appendChild(this.#mTextControlElement); + this.#mTextControlElement.focus(); + await waitForFocus; + + await this.#flushPendingIMENotifications(); + + return { + description: this.#mTextControl.description, + expectedIMEState: this.#mTextControl.expectedIMEState, + expectedIMEFocus: true, + }; + } + + /** + * @param {object} aExpectedData The expected data which was returned by prepareToRun(). + * @param {TIPWrapper} aTIPWrapper TIP wrapper in aWindow. + * @param {Window} aWindow [optional] The window object of which you want to check IME state. + */ + checkBeforeRun(aExpectedData, aTIPWrapper, aWindow = window) { + this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils; + this.#mTIPWrapper = aTIPWrapper; + const description = `IMEStateOnReadonlyChangeTester(${aExpectedData.description})`; + is( + this.#mWindowUtils.IMEStatus, + aExpectedData.expectedIMEState, + `${description}: IME state should be set to expected value before setting readonly attribute` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedData.expectedIMEFocus, + `${description}: IME state should ${ + aExpectedData.expectedIMEFocus ? "" : "not" + } have focus before setting readonly attribute` + ); + } + + /** + * @returns {object} Expected result. + */ + async runToMakeTextControlReadonly() { + this.#mTextControlElement.setAttribute("readonly", ""); + + await this.#flushPendingIMENotifications(); + + return { + description: this.#mTextControl.description, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + expectedIMEFocus: false, + }; + } + + /** + * @param {object} aExpectedData Expected result which is returned by runToMakeTextControlReadonly(). + */ + checkResultOfMakingTextControlReadonly(aExpectedData) { + const description = `IMEStateOnReadonlyChangeTester(${aExpectedData.description})`; + is( + this.#mWindowUtils.IMEStatus, + aExpectedData.expectedIMEState, + `${description}: IME state should be set to expected value after setting readonly attribute` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedData.expectedIMEFocus, + `${description}: IME state should ${ + aExpectedData.expectedIMEFocus ? "" : "not" + } have focus after setting readonly attribute` + ); + } + + /** + * @returns {object} Expected result. + */ + async runToMakeTextControlEditable() { + this.#mTextControlElement.removeAttribute("readonly"); + + await this.#flushPendingIMENotifications(); + + return { + description: this.#mTextControl.description, + expectedIMEState: this.#mTextControl.expectedIMEState, + expectedIMEFocus: true, + }; + } + + /** + * @param {object} aExpectedData Expected result which is returned by runToMakeTextControlEditable(). + */ + checkResultOfMakingTextControlEditable(aExpectedData) { + const description = `IMEStateOnReadonlyChangeTester(${aExpectedData.description})`; + is( + this.#mWindowUtils.IMEStatus, + aExpectedData.expectedIMEState, + `${description}: IME state should be set to expected value after removing readonly attribute` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedData.expectedIMEFocus, + `${description}: IME state should ${ + aExpectedData.expectedIMEFocus ? "" : "not" + } have focus after removing readonly attribute` + ); + } +} diff --git a/widget/tests/gtest/MockWinWidget.cpp b/widget/tests/gtest/MockWinWidget.cpp new file mode 100644 index 0000000000..ed7850076f --- /dev/null +++ b/widget/tests/gtest/MockWinWidget.cpp @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +#include "MockWinWidget.h" + +#include "mozilla/gfx/Logging.h" + +NS_IMPL_ISUPPORTS_INHERITED0(MockWinWidget, nsBaseWidget) + +// static +RefPtr<MockWinWidget> MockWinWidget::Create(DWORD aStyle, DWORD aExStyle, + const LayoutDeviceIntRect& aRect) { + RefPtr<MockWinWidget> window = new MockWinWidget; + if (!window->Initialize(aStyle, aExStyle, aRect)) { + return nullptr; + } + + return window; +} + +MockWinWidget::MockWinWidget() {} + +MockWinWidget::~MockWinWidget() { + if (mWnd) { + ::DestroyWindow(mWnd); + mWnd = 0; + } +} + +bool MockWinWidget::Initialize(DWORD aStyle, DWORD aExStyle, + const LayoutDeviceIntRect& aRect) { + WNDCLASSW wc; + const wchar_t className[] = L"MozillaMockWinWidget"; + HMODULE hSelf = ::GetModuleHandle(nullptr); + + if (!GetClassInfoW(hSelf, className, &wc)) { + ZeroMemory(&wc, sizeof(WNDCLASSW)); + wc.hInstance = hSelf; + wc.lpfnWndProc = ::DefWindowProc; + wc.lpszClassName = className; + RegisterClassW(&wc); + } + + mWnd = ::CreateWindowExW(aExStyle, className, className, aStyle, aRect.X(), + aRect.Y(), aRect.Width(), aRect.Height(), nullptr, + nullptr, hSelf, nullptr); + if (!mWnd) { + gfxCriticalNoteOnce << "GetClientRect failed " << ::GetLastError(); + return false; + } + + // First nccalcszie (during CreateWindow) for captioned windows is + // deliberately ignored so force a second one here to get the right + // non-client set up. + if (mWnd && (aStyle & WS_CAPTION)) { + ::SetWindowPos(mWnd, NULL, 0, 0, 0, 0, + SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | + SWP_NOACTIVATE | SWP_NOREDRAW); + } + + mBounds = aRect; + return true; +} + +void MockWinWidget::NotifyOcclusionState( + mozilla::widget::OcclusionState aState) { + mCurrentState = aState; +} + +nsSizeMode MockWinWidget::SizeMode() { + if (::IsIconic(mWnd)) { + return nsSizeMode_Minimized; + } + return nsSizeMode_Normal; +} diff --git a/widget/tests/gtest/MockWinWidget.h b/widget/tests/gtest/MockWinWidget.h new file mode 100644 index 0000000000..891e1c942d --- /dev/null +++ b/widget/tests/gtest/MockWinWidget.h @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef GTEST_MockWinWidget_H +#define GTEST_MockWinWidget_H + +#include <windows.h> + +#include "nsBaseWidget.h" +#include "Units.h" + +class MockWinWidget : public nsBaseWidget { + public: + static RefPtr<MockWinWidget> Create(DWORD aStyle, DWORD aExStyle, + const LayoutDeviceIntRect& aRect); + + NS_DECL_ISUPPORTS_INHERITED + + void NotifyOcclusionState(mozilla::widget::OcclusionState aState) override; + + void SetExpectation(mozilla::widget::OcclusionState aExpectation) { + mExpectation = aExpectation; + } + + bool IsExpectingCall() const { return mExpectation != mCurrentState; } + + HWND GetWnd() { return mWnd; } + + nsSizeMode SizeMode() override; + void SetSizeMode(nsSizeMode aMode) override {} + + void* GetNativeData(uint32_t aDataType) override { return nullptr; } + + virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + InitData* aInitData = nullptr) override { + return NS_OK; + } + virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent, + const DesktopIntRect& aRect, + InitData* aInitData = nullptr) override { + return NS_OK; + } + virtual void Show(bool aState) override {} + virtual bool IsVisible() const override { return true; } + virtual void Move(double aX, double aY) override {} + virtual void Resize(double aWidth, double aHeight, bool aRepaint) override {} + virtual void Resize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint) override {} + + virtual void Enable(bool aState) override {} + virtual bool IsEnabled() const override { return true; } + virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override {} + virtual void Invalidate(const LayoutDeviceIntRect& aRect) override {} + virtual nsresult SetTitle(const nsAString& title) override { return NS_OK; } + virtual LayoutDeviceIntPoint WidgetToScreenOffset() override { + return LayoutDeviceIntPoint(0, 0); + } + virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) override { + return NS_OK; + } + virtual void SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) override {} + virtual InputContext GetInputContext() override { abort(); } + + private: + MockWinWidget(); + ~MockWinWidget(); + + bool Initialize(DWORD aStyle, DWORD aExStyle, + const LayoutDeviceIntRect& aRect); + + HWND mWnd = 0; + + mozilla::widget::OcclusionState mExpectation = + mozilla::widget::OcclusionState::UNKNOWN; + mozilla::widget::OcclusionState mCurrentState = + mozilla::widget::OcclusionState::UNKNOWN; +}; + +#endif diff --git a/widget/tests/gtest/TestTimeConverter.cpp b/widget/tests/gtest/TestTimeConverter.cpp new file mode 100644 index 0000000000..22cbc3f9e6 --- /dev/null +++ b/widget/tests/gtest/TestTimeConverter.cpp @@ -0,0 +1,265 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "gtest/gtest.h" +#include "mozilla/TimeStamp.h" +#include "SystemTimeConverter.h" + +using mozilla::SystemTimeConverter; +using mozilla::TimeDuration; +using mozilla::TimeStamp; + +namespace { + +// This class provides a mock implementation of the CurrentTimeGetter template +// type used in SystemTimeConverter. It can be constructed with a particular +// Time and always returns that Time. +template <typename Time> +class MockCurrentTimeGetter { + public: + MockCurrentTimeGetter() : mTime(0) {} + explicit MockCurrentTimeGetter(Time aTime) : mTime(aTime) {} + + // Methods needed for CurrentTimeGetter compatibility + Time GetCurrentTime() const { return mTime; } + void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) {} + + private: + Time mTime; +}; + +// This is another mock implementation of the CurrentTimeGetter template +// type used in SystemTimeConverter, except this asserts that it will not be +// used. i.e. it should only be used in calls to SystemTimeConverter that we +// know will not invoke it. +template <typename Time> +class UnusedCurrentTimeGetter { + public: + Time GetCurrentTime() const { + EXPECT_TRUE(false); + return 0; + } + + void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) { + EXPECT_TRUE(false); + } +}; + +// This class provides a mock implementation of the TimeStampNowProvider +// template type used in SystemTimeConverter. It also has other things in it +// that allow the test to better control time for testing purposes. +class MockTimeStamp { + public: + // This should generally be called at the start of every test function, as + // it will initialize this class's static fields to sane values. In particular + // it will initialize the baseline TimeStamp against which all other + // TimeStamps are compared. + static void Init() { + sBaseline = TimeStamp::Now(); + sTimeStamp = sBaseline; + } + + // Advance the timestamp returned by `MockTimeStamp::Now()` + static void Advance(double ms) { + sTimeStamp += TimeDuration::FromMilliseconds(ms); + } + + // Returns the baseline TimeStamp, that is used as a fixed reference point + // in time against which other TimeStamps can be compared. This is needed + // because mozilla::TimeStamp itself doesn't provide any conversion to + // human-readable strings, and we need to convert it to a TimeDuration in + // order to get that. This baseline TimeStamp can be used to turn an + // arbitrary TimeStamp into a TimeDuration. + static TimeStamp Baseline() { return sBaseline; } + + // This is the method needed for TimeStampNowProvider compatibility, and + // simulates `TimeStamp::Now()` + static TimeStamp Now() { return sTimeStamp; } + + private: + static TimeStamp sTimeStamp; + static TimeStamp sBaseline; +}; + +TimeStamp MockTimeStamp::sTimeStamp; +TimeStamp MockTimeStamp::sBaseline; + +// Could have platform-specific implementations of this using DWORD, guint32, +// etc behind ifdefs. But this is sufficient for now. +using GTestTime = uint32_t; +using TimeConverter = SystemTimeConverter<GTestTime, MockTimeStamp>; + +} // namespace + +// Checks the expectation that the TimeStamp `ts` is exactly `ms` milliseconds +// after the baseline timestamp. This is a macro so gtest still gives us useful +// line numbers for failures. +#define EXPECT_TS(ts, ms) \ + EXPECT_EQ((ts)-MockTimeStamp::Baseline(), TimeDuration::FromMilliseconds(ms)) + +#define EXPECT_TS_FUZZY(ts, ms) \ + EXPECT_DOUBLE_EQ(((ts)-MockTimeStamp::Baseline()).ToMilliseconds(), ms) + +TEST(TimeConverter, SanityCheck) +{ + MockTimeStamp::Init(); + + MockCurrentTimeGetter timeGetter(10); + UnusedCurrentTimeGetter<GTestTime> unused; + TimeConverter converter; + + // This call sets the reference time and timestamp + TimeStamp ts = converter.GetTimeStampFromSystemTime(10, timeGetter); + EXPECT_TS(ts, 0); + + // Advance "TimeStamp::Now" by 10ms, use the same event time and OS time. + // Since the event time is the same as before, we expect to get back the + // same TimeStamp as before too, despite Now() changing. + MockTimeStamp::Advance(10); + ts = converter.GetTimeStampFromSystemTime(10, unused); + EXPECT_TS(ts, 0); + + // Now let's use an event time 20ms after the old event. This will trigger + // forward skew detection and resync the TimeStamp for the new event to Now(). + ts = converter.GetTimeStampFromSystemTime(30, unused); + EXPECT_TS(ts, 10); +} + +TEST(TimeConverter, Overflow) +{ + // This tests wrapping time around the max value supported in the GTestTime + // type and ensuring it is handled properly. + + MockTimeStamp::Init(); + + const GTestTime max = std::numeric_limits<GTestTime>::max(); + const GTestTime min = std::numeric_limits<GTestTime>::min(); + double fullRange = (double)max - (double)min; + double wrapPeriod = fullRange + 1.0; + + GTestTime almostOverflowed = max - 100; + GTestTime overflowed = max + 100; + MockCurrentTimeGetter timeGetter(almostOverflowed); + UnusedCurrentTimeGetter<GTestTime> unused; + TimeConverter converter; + + // Set reference time to 100ms before the overflow point + TimeStamp ts = + converter.GetTimeStampFromSystemTime(almostOverflowed, timeGetter); + EXPECT_TS(ts, 0); + + // Advance everything by 200ms and verify we get back a TimeStamp 200ms from + // the baseline despite wrapping an overflow. + MockTimeStamp::Advance(200); + ts = converter.GetTimeStampFromSystemTime(overflowed, unused); + EXPECT_TS(ts, 200); + + // Advance by another full wraparound of the time. This loses some precision + // so we have to do the FUZZY match + MockTimeStamp::Advance(wrapPeriod); + ts = converter.GetTimeStampFromSystemTime(overflowed, unused); + EXPECT_TS_FUZZY(ts, 200.0 + wrapPeriod); +} + +TEST(TimeConverter, InvertedOverflow) +{ + // This tests time going from near the min value of GTestTime to the max + // value of GTestTime + + MockTimeStamp::Init(); + + const GTestTime max = std::numeric_limits<GTestTime>::max(); + const GTestTime min = std::numeric_limits<GTestTime>::min(); + double fullRange = (double)max - (double)min; + double wrapPeriod = fullRange + 1.0; + + GTestTime nearRangeMin = min + 100; + GTestTime nearRangeMax = max - 100; + double gap = (double)nearRangeMax - (double)nearRangeMin; + + MockCurrentTimeGetter timeGetter(nearRangeMin); + UnusedCurrentTimeGetter<GTestTime> unused; + TimeConverter converter; + + // Set reference time to value near min numeric limit + TimeStamp ts = converter.GetTimeStampFromSystemTime(nearRangeMin, timeGetter); + EXPECT_TS(ts, 0); + + // Advance to value near max numeric limit + MockTimeStamp::Advance(gap); + ts = converter.GetTimeStampFromSystemTime(nearRangeMax, unused); + EXPECT_TS(ts, gap); + + // Advance by another full wraparound of the time. This loses some precision + // so we have to do the FUZZY match + MockTimeStamp::Advance(wrapPeriod); + ts = converter.GetTimeStampFromSystemTime(nearRangeMax, unused); + EXPECT_TS_FUZZY(ts, gap + wrapPeriod); +} + +TEST(TimeConverter, HalfRangeBoundary) +{ + MockTimeStamp::Init(); + + GTestTime max = std::numeric_limits<GTestTime>::max(); + GTestTime min = std::numeric_limits<GTestTime>::min(); + double fullRange = (double)max - (double)min; + double wrapPeriod = fullRange + 1.0; + GTestTime halfRange = (GTestTime)(fullRange / 2.0); + GTestTime halfWrapPeriod = (GTestTime)(wrapPeriod / 2.0); + + TimeConverter converter; + + GTestTime firstEvent = 10; + MockCurrentTimeGetter timeGetter(firstEvent); + UnusedCurrentTimeGetter<GTestTime> unused; + + // Set reference time + TimeStamp ts = converter.GetTimeStampFromSystemTime(firstEvent, timeGetter); + EXPECT_TS(ts, 0); + + // Advance event time by just under the half-period, to trigger about as big + // a forwards skew as we possibly can. + GTestTime secondEvent = firstEvent + (halfWrapPeriod - 1); + ts = converter.GetTimeStampFromSystemTime(secondEvent, unused); + EXPECT_TS(ts, 0); + + // The above forwards skew will have reset the reference timestamp. Now + // advance Now time by just under the half-range, to trigger about as big + // a backwards skew as we possibly can. + MockTimeStamp::Advance(halfRange - 1); + ts = converter.GetTimeStampFromSystemTime(secondEvent, unused); + EXPECT_TS(ts, 0); +} + +TEST(TimeConverter, FractionalMillisBug1626734) +{ + MockTimeStamp::Init(); + + TimeConverter converter; + + GTestTime eventTime = 10; + MockCurrentTimeGetter timeGetter(eventTime); + UnusedCurrentTimeGetter<GTestTime> unused; + + TimeStamp ts = converter.GetTimeStampFromSystemTime(eventTime, timeGetter); + EXPECT_TS(ts, 0); + + MockTimeStamp::Advance(0.2); + ts = converter.GetTimeStampFromSystemTime(eventTime, unused); + EXPECT_TS(ts, 0); + + MockTimeStamp::Advance(0.9); + TimeStamp ts2 = converter.GetTimeStampFromSystemTime(eventTime, unused); + EXPECT_TS(ts2, 0); + + // Since ts2 came from a "future" call relative to ts, we expect ts2 to not + // be "before" ts. (i.e. time shouldn't go backwards, even by fractional + // milliseconds). This assertion is technically already implied by the + // EXPECT_TS checks above, but fixing this assertion is the end result that + // we wanted in bug 1626734 so it feels appropriate to recheck it explicitly. + EXPECT_TRUE(ts <= ts2); +} diff --git a/widget/tests/gtest/TestTouchResampler.cpp b/widget/tests/gtest/TestTouchResampler.cpp new file mode 100644 index 0000000000..1a5b8e2430 --- /dev/null +++ b/widget/tests/gtest/TestTouchResampler.cpp @@ -0,0 +1,941 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include <initializer_list> +#include "InputData.h" +#include "Units.h" +#include "gtest/gtest.h" +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" +#include "TouchResampler.h" + +using namespace mozilla; +using widget::TouchResampler; + +class TouchResamplerTest : public ::testing::Test { + protected: + virtual void SetUp() { baseTimeStamp = TimeStamp::Now(); } + + TimeStamp Time(double aMilliseconds) { + return baseTimeStamp + TimeDuration::FromMilliseconds(aMilliseconds); + } + + uint64_t ProcessEvent( + MultiTouchInput::MultiTouchType aType, + std::initializer_list<std::pair<TimeStamp, ScreenIntPoint>> + aHistoricalData, + const TimeStamp& aTimeStamp, const ScreenIntPoint& aPosition) { + MultiTouchInput input(aType, 0, aTimeStamp, 0); + input.mTouches.AppendElement(SingleTouchData(1, aPosition, {}, 0.0f, 0.0f)); + for (const auto& histData : aHistoricalData) { + input.mTouches[0].mHistoricalData.AppendElement( + SingleTouchData::HistoricalTouchData{ + histData.first, histData.second, {}, {}, 0.0f, 0.0f}); + } + return resampler.ProcessEvent(std::move(input)); + } + + void CheckTime(const TimeStamp& aTimeStamp, + const TimeStamp& aExpectedTimeStamp) { + EXPECT_EQ((aTimeStamp - baseTimeStamp).ToMilliseconds(), + (aExpectedTimeStamp - baseTimeStamp).ToMilliseconds()); + } + + void CheckEvent(const MultiTouchInput& aEvent, + MultiTouchInput::MultiTouchType aExpectedType, + std::initializer_list<std::pair<TimeStamp, ScreenIntPoint>> + aExpectedHistoricalData, + const TimeStamp& aExpectedTimeStamp, + const ScreenIntPoint& aExpectedPosition) { + EXPECT_EQ(aEvent.mType, aExpectedType); + EXPECT_EQ(aEvent.mTouches.Length(), size_t(1)); + EXPECT_EQ(aEvent.mTouches[0].mHistoricalData.Length(), + aExpectedHistoricalData.size()); + for (size_t i = 0; i < aExpectedHistoricalData.size(); i++) { + CheckTime(aEvent.mTouches[0].mHistoricalData[i].mTimeStamp, + aExpectedHistoricalData.begin()[i].first); + EXPECT_EQ(aEvent.mTouches[0].mHistoricalData[i].mScreenPoint, + aExpectedHistoricalData.begin()[i].second); + } + CheckTime(aEvent.mTimeStamp, aExpectedTimeStamp); + EXPECT_EQ(aEvent.mTouches[0].mScreenPoint, aExpectedPosition); + } + + struct ExpectedOutgoingEvent { + Maybe<uint64_t> mEventId; + MultiTouchInput::MultiTouchType mType = MultiTouchInput::MULTITOUCH_START; + std::initializer_list<std::pair<TimeStamp, ScreenIntPoint>> mHistoricalData; + TimeStamp mTimeStamp; + ScreenIntPoint mPosition; + }; + + void CheckOutgoingEvents( + std::initializer_list<ExpectedOutgoingEvent> aExpectedEvents) { + auto outgoing = resampler.ConsumeOutgoingEvents(); + EXPECT_EQ(outgoing.size(), aExpectedEvents.size()); + for (const auto& expectedEvent : aExpectedEvents) { + auto outgoingEvent = std::move(outgoing.front()); + outgoing.pop(); + + EXPECT_EQ(outgoingEvent.mEventId, expectedEvent.mEventId); + CheckEvent(outgoingEvent.mEvent, expectedEvent.mType, + expectedEvent.mHistoricalData, expectedEvent.mTimeStamp, + expectedEvent.mPosition); + } + } + + TimeStamp baseTimeStamp; + TouchResampler resampler; +}; + +TEST_F(TouchResamplerTest, BasicExtrapolation) { + // Execute the following sequence: + // + // 0----------10-------16-----20---------------32------------ + // * touchstart at (10, 10) + // * touchmove at (20, 20) + // * frame + // * touchend at (20, 20) + // * frame + // + // And expect the following output: + // + // 0----------10-------16-----20---------------32------------ + // * touchstart at (10, 10) + // * touchmove at (26, 26) + // * touchmove at (20, 20) + // * touchend at (20, 20) + // + // The first frame should emit an extrapolated touchmove from the position + // data in the touchstart and touchmove events. + // The touchend should force a synthesized touchmove that returns back to a + // non-resampled position. + + EXPECT_FALSE(resampler.InTouchingState()); + + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(10, 10)); + EXPECT_TRUE(resampler.InTouchingState()); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(20, 20)); + + resampler.NotifyFrame(Time(16.0)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(10, 10)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 20)}}, + Time(16.0), + ScreenIntPoint(26, 26)}, + }); + + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(20.0), + ScreenIntPoint(20, 20)); + + EXPECT_FALSE(resampler.InTouchingState()); + + CheckOutgoingEvents({ + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(16.0), + ScreenIntPoint(20, 20)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(20.0), + ScreenIntPoint(20, 20)}, + }); + + // No more events should be produced from here on out. + resampler.NotifyFrame(Time(32.0)); + auto outgoing = resampler.ConsumeOutgoingEvents(); + EXPECT_TRUE(outgoing.empty()); +} + +TEST_F(TouchResamplerTest, BasicInterpolation) { + // Same test as BasicExtrapolation, but with a frame time that's 10ms earlier. + // + // Execute the following sequence: + // + // 0------6---10-----------20--22------------30------------- + // * touchstart at (10, 10) + // * touchmove at (20, 20) + // * frame + // * touchend at (20, 20) + // * frame + // + // And expect the following output: + // + // 0------6---10-----------20--22------------30------------- + // * touchstart at (10, 10) + // * touchmove (16, 16) + // * touchmove (20, 20) + // * touchend at (20, 20) + // + // The first frame should emit an interpolated touchmove from the position + // data in the touchstart and touchmove events. + // The touchend should create a touchmove that returns back to a non-resampled + // position. + + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(10, 10)); + EXPECT_TRUE(resampler.InTouchingState()); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(20, 20)); + + resampler.NotifyFrame(Time(6.0)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(10, 10)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(6.0), + ScreenIntPoint(16, 16)}, + }); + + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(20.0), + ScreenIntPoint(20, 20)); + EXPECT_FALSE(resampler.InTouchingState()); + + CheckOutgoingEvents({ + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(10.0), + ScreenIntPoint(20, 20)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(20.0), + ScreenIntPoint(20, 20)}, + }); + + // No more events should be produced from here on out. + resampler.NotifyFrame(Time(22.0)); + auto outgoing = resampler.ConsumeOutgoingEvents(); + EXPECT_TRUE(outgoing.empty()); +} + +TEST_F(TouchResamplerTest, InterpolationFromHistoricalData) { + // Interpolate from the historical data in a touch move event. + // + // Execute the following sequence: + // + // 0----------10-------16-----20-----------30--32------------ + // * touchstart at (10, 10) + // * [hist] at (20, 25) for + // `---------------* touchmove at (30, 30) + // * frame + // * touchend at (30, 30) + // * frame + // + // And expect the following output: + // + // 0----------10-------16-----20-----------30--32------------ + // * touchstart at (10, 10) + // * [hist] at (20, 25) for + // `--------* touchmove at (26, 28) + // * touchmove at (30, 30) + // * touchend at (30, 30) + // + // The first frame should emit an interpolated touchmove from the position + // data in the touchmove event, and integrate the historical data point into + // the resampled event. + // The touchend should force a synthesized touchmove that returns back to a + // non-resampled position. + + // This also tests that interpolation works for both x and y, by giving the + // historical datapoint different values for x and y. + // (26, 28) is 60% of the way from (20, 25) to (30, 30). + + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(10, 10)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 25)}}, + Time(20.0), ScreenIntPoint(30, 30)); + resampler.NotifyFrame(Time(16.0)); + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(30.0), + ScreenIntPoint(30, 30)); + resampler.NotifyFrame(Time(32.0)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(10, 10)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 25)}}, + Time(16.0), + ScreenIntPoint(26, 28)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(20.0), + ScreenIntPoint(30, 30)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(30.0), + ScreenIntPoint(30, 30)}, + }); +} + +TEST_F(TouchResamplerTest, MultipleTouches) { + EXPECT_FALSE(resampler.InTouchingState()); + + // Touch start + MultiTouchInput inputStart0(MultiTouchInput::MULTITOUCH_START, 0, Time(0.0), + 0); + inputStart0.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(10, 10), {}, 0.0f, 0.0f)); + auto idStart0 = resampler.ProcessEvent(std::move(inputStart0)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Touch move + MultiTouchInput inputMove1(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(20.0), + 0); + inputMove1.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 30), {}, 0.0f, 0.0f)); + inputMove1.mTouches[0].mHistoricalData.AppendElement( + SingleTouchData::HistoricalTouchData{ + Time(10.0), ScreenIntPoint(20, 25), {}, {}, 0.0f, 0.0f}); + auto idMove1 = resampler.ProcessEvent(std::move(inputMove1)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Frame + resampler.NotifyFrame(Time(16.0)); + + // Touch move + MultiTouchInput inputMove2(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(30.0), + 0); + inputMove2.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 40), {}, 0.0f, 0.0f)); + auto idMove2 = resampler.ProcessEvent(std::move(inputMove2)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Touch start + MultiTouchInput inputStart3(MultiTouchInput::MULTITOUCH_START, 0, Time(30.0), + 0); + inputStart3.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 10), {}, 0.0f, 0.0f)); + inputStart3.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 40), {}, 0.0f, 0.0f)); + auto idStart3 = resampler.ProcessEvent(std::move(inputStart3)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Touch move + MultiTouchInput inputMove4(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(40.0), + 0); + inputMove4.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 50), {}, 0.0f, 0.0f)); + inputMove4.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 30), {}, 0.0f, 0.0f)); + auto idMove4 = resampler.ProcessEvent(std::move(inputMove4)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Frame + resampler.NotifyFrame(Time(32.0)); + + // Touch move + MultiTouchInput inputMove5(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(50.0), + 0); + inputMove5.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 60), {}, 0.0f, 0.0f)); + inputMove5.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 40), {}, 0.0f, 0.0f)); + auto idMove5 = resampler.ProcessEvent(std::move(inputMove5)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Touch end + MultiTouchInput inputEnd6(MultiTouchInput::MULTITOUCH_END, 0, Time(50.0), 0); + // Touch point with identifier 1 is lifted + inputEnd6.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 60), {}, 0.0f, 0.0f)); + auto idEnd6 = resampler.ProcessEvent(std::move(inputEnd6)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Frame + resampler.NotifyFrame(Time(48.0)); + + // Touch move + MultiTouchInput inputMove7(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(60.0), + 0); + inputMove7.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 60), {}, 0.0f, 0.0f)); + auto idMove7 = resampler.ProcessEvent(std::move(inputMove7)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Frame + resampler.NotifyFrame(Time(64.0)); + + // Touch end + MultiTouchInput inputEnd8(MultiTouchInput::MULTITOUCH_END, 0, Time(70.0), 0); + // Touch point with identifier 2 is lifted + inputEnd8.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 60), {}, 0.0f, 0.0f)); + auto idEnd8 = resampler.ProcessEvent(std::move(inputEnd8)); + EXPECT_FALSE(resampler.InTouchingState()); + + // Check outgoing events + auto outgoing = resampler.ConsumeOutgoingEvents(); + EXPECT_EQ(outgoing.size(), size_t(9)); + + auto outgoingStart0 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingStart0.mEventId, Some(idStart0)); + CheckEvent(outgoingStart0.mEvent, MultiTouchInput::MULTITOUCH_START, {}, + Time(0.0), ScreenIntPoint(10, 10)); + + auto outgoingMove1 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove1.mEventId, Some(idMove1)); + // (26, 28) is 60% of the way from (20, 25) to (30, 30). + CheckEvent(outgoingMove1.mEvent, MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 25)}}, Time(16.0), + ScreenIntPoint(26, 28)); + + auto outgoingMove2 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove2.mEventId, Some(idMove2)); + CheckEvent(outgoingMove2.mEvent, MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(30, 30)}}, Time(30.0), + ScreenIntPoint(30, 40)); + + auto outgoingStart3 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingStart3.mEventId, Some(idStart3)); + EXPECT_EQ(outgoingStart3.mEvent.mType, MultiTouchInput::MULTITOUCH_START); + CheckTime(outgoingStart3.mEvent.mTimeStamp, Time(30.0)); + EXPECT_EQ(outgoingStart3.mEvent.mTouches.Length(), size_t(2)); + // touch order should be taken from the original touch start event + EXPECT_EQ(outgoingStart3.mEvent.mTouches[0].mIdentifier, 2); + EXPECT_EQ(outgoingStart3.mEvent.mTouches[0].mScreenPoint, + ScreenIntPoint(100, 10)); + EXPECT_EQ(outgoingStart3.mEvent.mTouches[1].mIdentifier, 1); + EXPECT_EQ(outgoingStart3.mEvent.mTouches[1].mScreenPoint, + ScreenIntPoint(30, 40)); + + auto outgoingMove4 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove4.mEventId, Some(idMove4)); + EXPECT_EQ(outgoingMove4.mEvent.mType, MultiTouchInput::MULTITOUCH_MOVE); + CheckTime(outgoingMove4.mEvent.mTimeStamp, Time(32.0)); + EXPECT_EQ(outgoingMove4.mEvent.mTouches.Length(), size_t(2)); + // Touch order should be taken from the original touch move event. + // Both touches should be resampled. + EXPECT_EQ(outgoingMove4.mEvent.mTouches[0].mIdentifier, 1); + EXPECT_EQ(outgoingMove4.mEvent.mTouches[0].mScreenPoint, + ScreenIntPoint(30, 42)); + EXPECT_EQ(outgoingMove4.mEvent.mTouches[1].mIdentifier, 2); + EXPECT_EQ(outgoingMove4.mEvent.mTouches[1].mScreenPoint, + ScreenIntPoint(100, 14)); + + auto outgoingMove5 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove5.mEventId, Some(idMove5)); + EXPECT_EQ(outgoingMove5.mEvent.mType, MultiTouchInput::MULTITOUCH_MOVE); + CheckTime(outgoingMove5.mEvent.mTimeStamp, Time(50.0)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches.Length(), size_t(2)); + // touch order should be taken from the original touch move event + EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mIdentifier, 1); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mScreenPoint, + ScreenIntPoint(30, 60)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mHistoricalData.Length(), + size_t(1)); + CheckTime(outgoingMove5.mEvent.mTouches[0].mHistoricalData[0].mTimeStamp, + Time(40.0)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mHistoricalData[0].mScreenPoint, + ScreenIntPoint(30, 50)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mIdentifier, 2); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mScreenPoint, + ScreenIntPoint(100, 40)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mHistoricalData.Length(), + size_t(1)); + CheckTime(outgoingMove5.mEvent.mTouches[1].mHistoricalData[0].mTimeStamp, + Time(40.0)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mHistoricalData[0].mScreenPoint, + ScreenIntPoint(100, 30)); + + auto outgoingEnd6 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingEnd6.mEventId, Some(idEnd6)); + CheckEvent(outgoingEnd6.mEvent, MultiTouchInput::MULTITOUCH_END, {}, + Time(50.0), ScreenIntPoint(30, 60)); + + auto outgoingMove7 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove7.mEventId, Some(idMove7)); + // No extrapolation because the frame at 64.0 cleared the data points because + // there was no pending touch move event at that point + CheckEvent(outgoingMove7.mEvent, MultiTouchInput::MULTITOUCH_MOVE, {}, + Time(60.0), ScreenIntPoint(100, 60)); + EXPECT_EQ(outgoingMove7.mEvent.mTouches[0].mIdentifier, 2); + + auto outgoingEnd8 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingEnd8.mEventId, Some(idEnd8)); + CheckEvent(outgoingEnd8.mEvent, MultiTouchInput::MULTITOUCH_END, {}, + Time(70.0), ScreenIntPoint(100, 60)); +} + +TEST_F(TouchResamplerTest, MovingPauses) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(10, 10)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(20, 20)); + resampler.NotifyFrame(Time(16.0)); + auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(30.0), + ScreenIntPoint(40, 40)); + resampler.NotifyFrame(Time(32.0)); + auto idMove3 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(40.0), + ScreenIntPoint(50, 40)); + resampler.NotifyFrame(Time(48.0)); + resampler.NotifyFrame(Time(64.0)); + auto idEnd4 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(50, 40)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(10, 10)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 20)}}, + Time(16.0), + ScreenIntPoint(26, 26)}, + + {Some(idMove2), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(30.0), ScreenIntPoint(40, 40)}}, + Time(32.0), + ScreenIntPoint(42, 42)}, + + {Some(idMove3), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(40.0), ScreenIntPoint(50, 40)}}, + Time(48.0), + ScreenIntPoint(58, 40)}, + + // There was no event between two frames here, so we expect a reset event, + // so that we pause at a non-resampled position. + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(48.0), + ScreenIntPoint(50, 40)}, + + {Some(idEnd4), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(50, 40)}, + }); +} + +TEST_F(TouchResamplerTest, MixedInterAndExtrapolation) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(0, 10)); + resampler.NotifyFrame(Time(11.0)); // 16 - 5 + auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, Time(30.0), + ScreenIntPoint(0, 30)); + resampler.NotifyFrame(Time(27.0)); // 32 - 5 + auto idMove3 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(40.0), + ScreenIntPoint(0, 40)); + resampler.NotifyFrame(Time(43.0)); // 48 - 5 + auto idMove4 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(50.0), ScreenIntPoint(0, 50)}}, Time(60.0), + ScreenIntPoint(0, 60)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd5 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 60)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, + Time(11.0), + ScreenIntPoint(0, 11)}, + + {Some(idMove2), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, + Time(27.0), + ScreenIntPoint(0, 27)}, + + {Some(idMove3), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(30.0), ScreenIntPoint(0, 30)}, + {Time(40.0), ScreenIntPoint(0, 40)}}, + Time(43.0), + ScreenIntPoint(0, 43)}, + + {Some(idMove4), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(50.0), ScreenIntPoint(0, 50)}}, + Time(59.0), + ScreenIntPoint(0, 59)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(60.0), + ScreenIntPoint(0, 60)}, + + {Some(idEnd5), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 60)}, + }); +} + +TEST_F(TouchResamplerTest, MultipleMoveEvents) { + // Test what happens if multiple touch move events appear between two frames. + // This scenario shouldn't occur on Android but we should be able to deal with + // it anyway. Check that we don't discard any event IDs. + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(0, 10)); + resampler.NotifyFrame(Time(11.0)); // 16 - 5 + auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, Time(30.0), + ScreenIntPoint(0, 30)); + auto idMove3 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(40.0), + ScreenIntPoint(0, 40)); + auto idMove4 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(45.0), ScreenIntPoint(0, 45)}}, Time(50.0), + ScreenIntPoint(0, 50)); + auto idMove5 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(55.0), + ScreenIntPoint(0, 55)); + resampler.NotifyFrame(Time(43.0)); // 48 - 5 + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd5 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 60)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, + Time(11.0), + ScreenIntPoint(0, 11)}, + + {Some(idMove2), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, + Time(30.0), + ScreenIntPoint(0, 30)}, + + {Some(idMove3), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(40.0), + ScreenIntPoint(0, 40)}, + + {Some(idMove4), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(43.0), + ScreenIntPoint(0, 43)}, + + {Some(idMove5), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(45.0), ScreenIntPoint(0, 45)}, + {Time(50.0), ScreenIntPoint(0, 50)}, + {Time(55.0), ScreenIntPoint(0, 55)}}, + Time(59.0), + ScreenIntPoint(0, 59)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(59.0), + ScreenIntPoint(0, 55)}, + + {Some(idEnd5), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 60)}, + }); +} + +TEST_F(TouchResamplerTest, LimitFuturePrediction) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 44, then pause. UI thread is occupied until 64. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(32.0), ScreenIntPoint(0, 32)}}, + Time(44.0), ScreenIntPoint(0, 44)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 44)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleMaxPredictMs == 8 + // Refuse to predict more than 8ms into the future, the fingers might have + // paused. Make an event for time 52 (= 44 + 8) instead of 59. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(32.0), ScreenIntPoint(0, 32)}, + {Time(44.0), ScreenIntPoint(0, 44)}}, + Time(52.0), + ScreenIntPoint(0, 52)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(52.0), + ScreenIntPoint(0, 44)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 44)}, + }); +} + +TEST_F(TouchResamplerTest, LimitBacksampling) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 44, then pause. UI thread is occupied until 64. + // Then we get a frame callback with a very outdated frametime. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(32.0), ScreenIntPoint(0, 32)}}, + Time(44.0), ScreenIntPoint(0, 44)); + resampler.NotifyFrame(Time(11.0)); // 16 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 44)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleMaxBacksampleMs == 20 + // Refuse to sample further back than 20ms before the last data point. + // Make an event for time 24 (= 44 - 20) instead of time 11. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, + Time(24.0), + ScreenIntPoint(0, 24)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(32.0), ScreenIntPoint(0, 32)}}, + Time(44.0), + ScreenIntPoint(0, 44)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 44)}, + }); +} + +TEST_F(TouchResamplerTest, DontExtrapolateFromOldTouch) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 40, then pause. UI thread is occupied until 64. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(30.0), ScreenIntPoint(0, 30)}}, + Time(40.0), ScreenIntPoint(0, 40)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 44)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleOldTouchThresholdMs == 17 + // Refuse to extrapolate from a data point that's more than 17ms older + // than the frame time. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(30.0), ScreenIntPoint(0, 30)}}, + Time(40.0), + ScreenIntPoint(0, 40)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 44)}, + }); +} + +TEST_F(TouchResamplerTest, DontExtrapolateIfTooOld) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 10, pause, and move again at 55. + // UI thread is occupied until 64. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, Time(55.0), + ScreenIntPoint(0, 55)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 60)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleWindowSize == 40 + // Refuse to resample between two data points that are more than 40ms + // apart. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, + Time(55.0), + ScreenIntPoint(0, 55)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 60)}, + }); +} + +TEST_F(TouchResamplerTest, DontInterpolateIfTooOld) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 10, pause, and move again at 60. + // UI thread is occupied until 64. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, Time(60.0), + ScreenIntPoint(0, 60)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 60)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleWindowSize == 40 + // Refuse to resample between two data points that are more than 40ms + // apart. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, + Time(60.0), + ScreenIntPoint(0, 60)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 60)}, + }); +} + +TEST_F(TouchResamplerTest, DiscardOutdatedHistoricalData) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, Time(16.0), + ScreenIntPoint(0, 16)); + resampler.NotifyFrame(Time(20.0)); + auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(18.0), ScreenIntPoint(0, 18)}}, Time(25.0), + ScreenIntPoint(0, 25)); + auto idEnd3 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(35.0), + ScreenIntPoint(0, 25)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}, + {Time(16.0), ScreenIntPoint(0, 16)}}, + Time(20.0), + ScreenIntPoint(0, 20)}, + + // Discard the historical data point from time 18, because we've already + // sent out an event with time 20 and don't want to go back before that. + {Some(idMove2), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(25.0), + ScreenIntPoint(0, 25)}, + + {Some(idEnd3), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(35.0), + ScreenIntPoint(0, 25)}, + }); +} diff --git a/widget/tests/gtest/TestWinHeaderOnlyUtils.cpp b/widget/tests/gtest/TestWinHeaderOnlyUtils.cpp new file mode 100644 index 0000000000..5ac361fea0 --- /dev/null +++ b/widget/tests/gtest/TestWinHeaderOnlyUtils.cpp @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "gtest/gtest.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +#include <shlwapi.h> + +TEST(WinHeaderOnlyUtils, MozPathGetDriveNumber) +{ + constexpr auto TestPathGetDriveNumber = [](const wchar_t* aPath) { + return mozilla::MozPathGetDriveNumber(aPath) == + ::PathGetDriveNumberW(aPath); + }; + EXPECT_TRUE(TestPathGetDriveNumber(nullptr)); + EXPECT_TRUE(TestPathGetDriveNumber(L"")); + EXPECT_TRUE(TestPathGetDriveNumber(L" :")); + EXPECT_TRUE(TestPathGetDriveNumber(L"a:")); + EXPECT_TRUE(TestPathGetDriveNumber(L"C:\\file")); + EXPECT_TRUE(TestPathGetDriveNumber(L"x:/file")); + EXPECT_TRUE(TestPathGetDriveNumber(L"@:\\\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"B")); + EXPECT_TRUE(TestPathGetDriveNumber(L"abc:\\file")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\:A")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\c:\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\A")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\ \\")); +} diff --git a/widget/tests/gtest/TestWinMessageLoggingUtils.cpp b/widget/tests/gtest/TestWinMessageLoggingUtils.cpp new file mode 100644 index 0000000000..b0ab09f866 --- /dev/null +++ b/widget/tests/gtest/TestWinMessageLoggingUtils.cpp @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "windows/nsWindowDbg.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_CombinationFlagsHandledFirst)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x3, "COMBO"}, {0x1, "ONE"}, {0x2, "TWO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x3, flags, nullptr));
+ EXPECT_STREQ("COMBO", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_SingleFlag)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x4, flags, nullptr));
+ EXPECT_STREQ("FOUR", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_SingleFlagWithName)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x4, flags, "paramName"));
+ EXPECT_STREQ("paramName=FOUR", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_MultipleFlags)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x5, flags, nullptr));
+ EXPECT_STREQ("ONE|FOUR", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_NoFlags)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x8, flags, nullptr));
+ EXPECT_STREQ("Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_OnlySomeFlagsMatch)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x9, flags, nullptr));
+ EXPECT_STREQ("ONE|Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_FlagsMatch_NoZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x2, flags, nullptr));
+ EXPECT_STREQ("TWO", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_NoFlagsMatch_NoZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x8, flags, nullptr));
+ EXPECT_STREQ("Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_NameAndNoFlagsMatch_NoZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x8, flags, "paramName"));
+ EXPECT_STREQ("paramName=Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_ValueIsZero_ZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x0, flags, nullptr));
+ EXPECT_STREQ("ZERO", result.get());
+}
diff --git a/widget/tests/gtest/TestWinWindowOcclusionTracker.cpp b/widget/tests/gtest/TestWinWindowOcclusionTracker.cpp new file mode 100644 index 0000000000..5ef7e3f81c --- /dev/null +++ b/widget/tests/gtest/TestWinWindowOcclusionTracker.cpp @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "gtest/gtest.h" + +#include <dwmapi.h> +#include <windows.h> + +#include "MockWinWidget.h" +#include "mozilla/widget/WinWindowOcclusionTracker.h" + +using namespace mozilla; +using namespace mozilla::widget; + +class WinWindowOcclusionTrackerTest : public ::testing::Test { + protected: + HWND CreateNativeWindow(DWORD aStyle, DWORD aExStyle) { + mMockWinWidget = + MockWinWidget::Create(WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | aStyle, + aExStyle, LayoutDeviceIntRect(0, 0, 100, 100)); + EXPECT_NE(nullptr, mMockWinWidget.get()); + HWND hwnd = mMockWinWidget->GetWnd(); + HRGN region = ::CreateRectRgn(0, 0, 0, 0); + EXPECT_NE(nullptr, region); + if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) { + // On Windows 7, the newly created window has a complex region, which + // means it will be ignored during the occlusion calculation. So, force + // it to have a simple region so that we get test coverage on win 7. + RECT boundingRect; + EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect)); + HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect); + EXPECT_NE(nullptr, rectangularRegion); + ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE); + ::DeleteObject(rectangularRegion); + } + ::DeleteObject(region); + + ::ShowWindow(hwnd, SW_SHOWNORMAL); + EXPECT_TRUE(UpdateWindow(hwnd)); + return hwnd; + } + + // Wrapper around IsWindowVisibleAndFullyOpaque so only the test class + // needs to be a friend of NativeWindowOcclusionTrackerWin. + bool CheckWindowVisibleAndFullyOpaque(HWND aHWnd, + LayoutDeviceIntRect* aWinRect) { + bool ret = WinWindowOcclusionTracker::IsWindowVisibleAndFullyOpaque( + aHWnd, aWinRect); + // In general, if IsWindowVisibleAndFullyOpaque returns false, the + // returned rect should not be altered. + if (!ret) { + EXPECT_EQ(*aWinRect, LayoutDeviceIntRect(0, 0, 0, 0)); + } + return ret; + } + + RefPtr<MockWinWidget> mMockWinWidget; +}; + +TEST_F(WinWindowOcclusionTrackerTest, VisibleOpaqueWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0); + LayoutDeviceIntRect returnedRect; + // Normal windows should be visible. + EXPECT_TRUE(CheckWindowVisibleAndFullyOpaque(hwnd, &returnedRect)); + + // Check that the returned rect == the actual window rect of the hwnd. + RECT winRect; + ASSERT_TRUE(::GetWindowRect(hwnd, &winRect)); + EXPECT_EQ(returnedRect, LayoutDeviceIntRect(winRect.left, winRect.top, + winRect.right - winRect.left, + winRect.bottom - winRect.top)); +} + +TEST_F(WinWindowOcclusionTrackerTest, MinimizedWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0); + LayoutDeviceIntRect winRect; + ::ShowWindow(hwnd, SW_MINIMIZE); + // Minimized windows are not considered visible. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, TransparentWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_TRANSPARENT); + LayoutDeviceIntRect winRect; + // Transparent windows are not considered visible and opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, ToolWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_TOOLWINDOW); + LayoutDeviceIntRect winRect; + // Tool windows are not considered visible and opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, LayeredAlphaWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_LAYERED); + LayoutDeviceIntRect winRect; + BYTE alpha = 1; + DWORD flags = LWA_ALPHA; + COLORREF colorRef = RGB(1, 1, 1); + SetLayeredWindowAttributes(hwnd, colorRef, alpha, flags); + // Layered windows with alpha < 255 are not considered visible and opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, UpdatedLayeredAlphaWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_LAYERED); + LayoutDeviceIntRect winRect; + HDC hdc = ::CreateCompatibleDC(nullptr); + EXPECT_NE(nullptr, hdc); + BLENDFUNCTION blend = {AC_SRC_OVER, 0x00, 0xFF, AC_SRC_ALPHA}; + + ::UpdateLayeredWindow(hwnd, hdc, nullptr, nullptr, nullptr, nullptr, + RGB(0xFF, 0xFF, 0xFF), &blend, ULW_OPAQUE); + // Layered windows set up with UpdateLayeredWindow instead of + // SetLayeredWindowAttributes should not be considered visible and opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); + ::DeleteDC(hdc); +} + +TEST_F(WinWindowOcclusionTrackerTest, LayeredNonAlphaWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_LAYERED); + LayoutDeviceIntRect winRect; + BYTE alpha = 1; + DWORD flags = 0; + COLORREF colorRef = RGB(1, 1, 1); + ::SetLayeredWindowAttributes(hwnd, colorRef, alpha, flags); + // Layered non alpha windows are considered visible and opaque. + EXPECT_TRUE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, ComplexRegionWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0); + LayoutDeviceIntRect winRect; + // Create a region with rounded corners, which should be a complex region. + HRGN region = CreateRoundRectRgn(1, 1, 100, 100, 5, 5); + EXPECT_NE(nullptr, region); + ::SetWindowRgn(hwnd, region, /* bRedraw = */ TRUE); + // Windows with complex regions are not considered visible and fully opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); + DeleteObject(region); +} + +TEST_F(WinWindowOcclusionTrackerTest, PopupWindow) { + HWND hwnd = CreateNativeWindow(WS_POPUP, /* aExStyle = */ 0); + LayoutDeviceIntRect winRect; + // Normal Popup Windows are not considered visible. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, CloakedWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0); + LayoutDeviceIntRect winRect; + BOOL cloak = TRUE; + ::DwmSetWindowAttribute(hwnd, DWMWA_CLOAK, &cloak, sizeof(cloak)); + // Cloaked Windows are not considered visible. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} diff --git a/widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp b/widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp new file mode 100644 index 0000000000..13cd95651c --- /dev/null +++ b/widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp @@ -0,0 +1,402 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "gtest/gtest.h" + +#include "MockWinWidget.h" +#include "mozilla/Preferences.h" +#include "mozilla/widget/WinEventObserver.h" +#include "mozilla/widget/WinWindowOcclusionTracker.h" +#include "nsThreadUtils.h" +#include "Units.h" +#include "WinUtils.h" + +using namespace mozilla; +using namespace mozilla::widget; + +#define PREF_DISPLAY_STATE \ + "widget.windows.window_occlusion_tracking_display_state.enabled" +#define PREF_SESSION_LOCK \ + "widget.windows.window_occlusion_tracking_session_lock.enabled" + +class WinWindowOcclusionTrackerInteractiveTest : public ::testing::Test { + protected: + void SetUp() override { + // Shut down WinWindowOcclusionTracker if it exists. + // This could happen when WinWindowOcclusionTracker was initialized by other + // gtest + if (WinWindowOcclusionTracker::Get()) { + WinWindowOcclusionTracker::ShutDown(); + } + EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get()); + + WinWindowOcclusionTracker::Ensure(); + EXPECT_NE(nullptr, WinWindowOcclusionTracker::Get()); + + WinWindowOcclusionTracker::Get()->EnsureDisplayStatusObserver(); + WinWindowOcclusionTracker::Get()->EnsureSessionChangeObserver(); + } + + void TearDown() override { + WinWindowOcclusionTracker::ShutDown(); + EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get()); + } + + void SetNativeWindowBounds(HWND aHWnd, const LayoutDeviceIntRect aBounds) { + RECT wr; + wr.left = aBounds.X(); + wr.top = aBounds.Y(); + wr.right = aBounds.XMost(); + wr.bottom = aBounds.YMost(); + + ::AdjustWindowRectEx(&wr, ::GetWindowLong(aHWnd, GWL_STYLE), FALSE, + ::GetWindowLong(aHWnd, GWL_EXSTYLE)); + + // Make sure to keep the window onscreen, as AdjustWindowRectEx() may have + // moved part of it offscreen. But, if the original requested bounds are + // offscreen, don't adjust the position. + LayoutDeviceIntRect windowBounds(wr.left, wr.top, wr.right - wr.left, + wr.bottom - wr.top); + + if (aBounds.X() >= 0) { + windowBounds.x = std::max(0, windowBounds.X()); + } + if (aBounds.Y() >= 0) { + windowBounds.y = std::max(0, windowBounds.Y()); + } + ::SetWindowPos(aHWnd, HWND_TOP, windowBounds.X(), windowBounds.Y(), + windowBounds.Width(), windowBounds.Height(), + SWP_NOREPOSITION); + EXPECT_TRUE(::UpdateWindow(aHWnd)); + } + + void CreateNativeWindowWithBounds(LayoutDeviceIntRect aBounds) { + mMockWinWidget = MockWinWidget::Create( + WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle = */ 0, aBounds); + EXPECT_NE(nullptr, mMockWinWidget.get()); + HWND hwnd = mMockWinWidget->GetWnd(); + SetNativeWindowBounds(hwnd, aBounds); + HRGN region = ::CreateRectRgn(0, 0, 0, 0); + EXPECT_NE(nullptr, region); + if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) { + // On Windows 7, the newly created window has a complex region, which + // means it will be ignored during the occlusion calculation. So, force + // it to have a simple region so that we get test coverage on win 7. + RECT boundingRect; + EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect)); + HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect); + EXPECT_NE(nullptr, rectangularRegion); + ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE); + ::DeleteObject(rectangularRegion); + } + ::DeleteObject(region); + + ::ShowWindow(hwnd, SW_SHOWNORMAL); + EXPECT_TRUE(UpdateWindow(hwnd)); + } + + RefPtr<MockWinWidget> CreateTrackedWindowWithBounds( + LayoutDeviceIntRect aBounds) { + RefPtr<MockWinWidget> window = MockWinWidget::Create( + WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle */ 0, aBounds); + EXPECT_NE(nullptr, window.get()); + HWND hwnd = window->GetWnd(); + ::ShowWindow(hwnd, SW_SHOWNORMAL); + WinWindowOcclusionTracker::Get()->Enable(window, window->GetWnd()); + return window; + } + + int GetNumVisibleRootWindows() { + return WinWindowOcclusionTracker::Get()->mNumVisibleRootWindows; + } + + void OnDisplayStateChanged(bool aDisplayOn) { + WinWindowOcclusionTracker::Get()->OnDisplayStateChanged(aDisplayOn); + } + + RefPtr<MockWinWidget> mMockWinWidget; +}; + +// Simple test completely covering a tracked window with a native window. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::OCCLUDED); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Simple test partially covering a tracked window with a native window. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, PartialOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Simple test that a partly off screen tracked window, with the on screen part +// occluded by a native window, is considered occluded. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, OffscreenOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + // Move the tracked window 50 pixels offscreen to the left. + int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN); + ::SetWindowPos(window->GetWnd(), HWND_TOP, screenLeft - 50, 0, 100, 100, + SWP_NOZORDER | SWP_NOSIZE); + + // Create a native window that covers the onscreen part of the tracked window. + CreateNativeWindowWithBounds(LayoutDeviceIntRect(screenLeft, 0, 50, 100)); + window->SetExpectation(widget::OcclusionState::OCCLUDED); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Simple test with a tracked window and native window that do not overlap. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleVisible) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Simple test with a minimized tracked window and native window. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleHidden) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100)); + // Iconify the tracked window and check that its occlusion state is HIDDEN. + ::CloseWindow(window->GetWnd()); + window->SetExpectation(widget::OcclusionState::HIDDEN); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that minimizing and restoring a tracked window results in the occlusion +// tracker re-registering for win events and detecting that a native window +// occludes the tracked window. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, + OcclusionAfterVisibilityToggle) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::HIDDEN); + WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged( + window, /* aVisible = */ false); + + // This makes the window iconic. + ::CloseWindow(window->GetWnd()); + + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + // HIDDEN state is set synchronously by OnWindowVsiblityChanged notification, + // before occlusion is calculated, so the above expectation will be met w/o an + // occlusion calculation. + // Loop until an occlusion calculation has run with no non-hidden windows. + while (GetNumVisibleRootWindows() != 0) { + // Need to pump events in order for UpdateOcclusionState to get called, and + // update the number of non hidden windows. When that number is 0, + // occlusion has been calculated with no visible tracked windows. + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::VISIBLE); + WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged( + window, /* aVisible = */ true); + // This opens the window made iconic above. + ::OpenIcon(window->GetWnd()); + + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + // Open a native window that occludes the visible tracked window. + window->SetExpectation(widget::OcclusionState::OCCLUDED); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that locking the screen causes visible windows to become occluded. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenVisibleOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::OCCLUDED); + // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker + // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but + // actually locking the screen isn't feasible. + DWORD currentSessionId = 0; + ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId); + ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE, + WTS_SESSION_LOCK, currentSessionId); + + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + MSG msg; + bool gotMessage = + ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE); + if (gotMessage) { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + NS_ProcessNextEvent(nullptr, /* aMayWait = */ false); + PR_Sleep(PR_INTERVAL_NO_WAIT); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that locking the screen leaves hidden windows as hidden. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenHiddenOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + // Iconify the tracked window and check that its occlusion state is HIDDEN. + ::CloseWindow(window->GetWnd()); + window->SetExpectation(widget::OcclusionState::HIDDEN); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + // Force the state to VISIBLE. + window->NotifyOcclusionState(widget::OcclusionState::VISIBLE); + + window->SetExpectation(widget::OcclusionState::HIDDEN); + + // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker + // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but + // actually locking the screen isn't feasible. + DWORD currentSessionId = 0; + ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId); + PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE, + WTS_SESSION_LOCK, currentSessionId); + + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + MSG msg; + bool gotMessage = + ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE); + if (gotMessage) { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + NS_ProcessNextEvent(nullptr, /* aMayWait = */ false); + PR_Sleep(PR_INTERVAL_NO_WAIT); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that locking the screen from a different session doesn't mark window +// as occluded. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenDifferentSession) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + // Force the state to OCCLUDED. + window->NotifyOcclusionState(widget::OcclusionState::OCCLUDED); + + // Generate a session change lock screen with a session id that's not + // currentSessionId. + DWORD currentSessionId = 0; + ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId); + ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE, + WTS_SESSION_LOCK, currentSessionId + 1); + + window->SetExpectation(widget::OcclusionState::VISIBLE); + // Create a native window to trigger occlusion calculation. + CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + MSG msg; + bool gotMessage = + ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE); + if (gotMessage) { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + NS_ProcessNextEvent(nullptr, /* aMayWait = */ false); + PR_Sleep(PR_INTERVAL_NO_WAIT); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that display off & on power state notification causes visible windows to +// become occluded, then visible. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, DisplayOnOffHandling) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::OCCLUDED); + + // Turning display off and on isn't feasible, so send a notification. + OnDisplayStateChanged(/* aDisplayOn */ false); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::VISIBLE); + OnDisplayStateChanged(/* aDisplayOn */ true); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} diff --git a/widget/tests/gtest/moz.build b/widget/tests/gtest/moz.build new file mode 100644 index 0000000000..613844fa78 --- /dev/null +++ b/widget/tests/gtest/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES = [ + "TestTimeConverter.cpp", + "TestTouchResampler.cpp", +] + +if CONFIG["OS_ARCH"] == "WINNT": + UNIFIED_SOURCES += [ + "MockWinWidget.cpp", + "TestWinHeaderOnlyUtils.cpp", + "TestWinMessageLoggingUtils.cpp", + "TestWinWindowOcclusionTracker.cpp", + "TestWinWindowOcclusionTrackerInteractive.cpp", + ] + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += [ + "/widget", +] + +DisableStlWrapping() diff --git a/widget/tests/mochitest.toml b/widget/tests/mochitest.toml new file mode 100644 index 0000000000..32dcf5c795 --- /dev/null +++ b/widget/tests/mochitest.toml @@ -0,0 +1,53 @@ +[DEFAULT] +support-files = ["clipboard_helper.js"] + +["test_AltGr_key_events_in_web_content_on_windows.html"] +run-if = ["os == 'win'"] +skip-if = ["headless"] # bug 1410525 + +["test_actionhint.html"] + +["test_assign_event_data.html"] +skip-if = [ + "os == 'mac'", # bug 933303 + "os == 'android' && debug", # bug 1285414 + "android_version == '24'", + "headless && os == 'win'", +] + +["test_autocapitalize.html"] + +["test_clipboard.html"] +skip-if = [ + "headless", # bug 1852983 + "display == 'wayland' && os_version == '22.04'", # Bug 1857075 +] +support-files = ["file_test_clipboard.js"] + +["test_clipboard_asyncGetData.html"] +skip-if = ["display == 'wayland'"] # Bug 1864211 +support-files = ["file_test_clipboard_asyncGetData.js"] + +["test_clipboard_asyncSetData.html"] +support-files = ["file_test_clipboard_asyncSetData.js"] + +["test_contextmenu_by_mouse_on_unix.html"] +run-if = ["os == 'linux'"] +skip-if = ["headless"] # headless widget doesn't dispatch contextmenu event by mouse event. + +["test_keypress_event_with_alt_on_mac.html"] +run-if = ["os == 'mac'"] + +["test_mouse_event_with_control_on_mac.html"] +run-if = ["os == 'mac'"] +support-files = ["!/gfx/layers/apz/test/mochitest/apz_test_utils.js"] + +["test_picker_no_crash.html"] +skip-if = [ + "asan", + "debug", # bug 1267491 +] +support-files = ["window_picker_no_crash_child.html"] + +["test_textScaleFactor_system_font.html"] +skip-if = ["display == 'wayland' && os_version == '22.04'"] # Bug 1857075 diff --git a/widget/tests/moz.build b/widget/tests/moz.build new file mode 100644 index 0000000000..fe0d068efd --- /dev/null +++ b/widget/tests/moz.build @@ -0,0 +1,125 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Widget") + +with Files("browser/browser_test_ContentCache.html"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("unit/*macwebapputils*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("unit/*taskbar_jumplistitems*"): + BUG_COMPONENT = ("Core", "Widget: Win32") + +with Files("TestChromeMargin.cpp"): + BUG_COMPONENT = ("Core", "Widget: Win32") + +with Files("*413277*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*428405*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*429954*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*444800*"): + BUG_COMPONENT = ("Core", "Widget: Win32") + +with Files("*466599*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*478536*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("*485118*"): + BUG_COMPONENT = ("Toolkit", "UI Widgets") + +with Files("*517396*"): + BUG_COMPONENT = ("Toolkit", "UI Widgets") + +with Files("*522217*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*538242*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*565392*"): + BUG_COMPONENT = ("Core", "DOM: Serializers") + +with Files("*586713*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*593307*"): + BUG_COMPONENT = ("Core", "Widget: Win32") + +with Files("*596600*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("*673301*"): + BUG_COMPONENT = ("Firefox", "Bookmarks & History") + +with Files("test_assign_event_data.html"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("test_input_events_on_deactive_window.xhtml"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("*chrome_context_menus_win*"): + BUG_COMPONENT = ("Core", "General") + +with Files("*composition_text_querycontent*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("*key_event_counts*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*imestate*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("*mouse_scroll*"): + BUG_COMPONENT = ("Core", "Widget: Win32") + +with Files("*native*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*panel_mouse_coords*"): + BUG_COMPONENT = ("Core", "Widget: Gtk") + +with Files("*picker_no_crash*"): + BUG_COMPONENT = ("Core", "Widget: Win32") + +with Files("*platform_colors*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*position_on_resize*"): + BUG_COMPONENT = ("Core", "Widget: Gtk") + +with Files("test_sizemode_events.xhtml"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*system_status_bar*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("*taskbar_progress*"): + BUG_COMPONENT = ("Core", "Widget: Win32") + +with Files("*wheeltransaction*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"] +MOCHITEST_MANIFESTS += ["mochitest.toml"] +MOCHITEST_CHROME_MANIFESTS += ["chrome.toml"] +BROWSER_CHROME_MANIFESTS += ["browser/browser.toml"] + +# if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': +# +# Test disabled because it requires the internal API. Re-enabling this test +# is bug 652123. +# CPP_UNIT_TESTS += ['TestChromeMargin'] diff --git a/widget/tests/native_menus_window.xhtml b/widget/tests/native_menus_window.xhtml new file mode 100644 index 0000000000..2b3d3f2aa3 --- /dev/null +++ b/widget/tests/native_menus_window.xhtml @@ -0,0 +1,282 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="NativeMenuWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="300" + height="300" + onload="onLoad();" + title="Native Menu Test"> + + <command id="cmd_FooItem0" oncommand="executedCommandID = 'cmd_FooItem0';"/> + <command id="cmd_FooItem1" oncommand="executedCommandID = 'cmd_FooItem1';"/> + <command id="cmd_BarItem0" oncommand="executedCommandID = 'cmd_BarItem0';"/> + <command id="cmd_BarItem1" oncommand="executedCommandID = 'cmd_BarItem1';"/> + <command id="cmd_NewItem0" oncommand="executedCommandID = 'cmd_NewItem0';"/> + <command id="cmd_NewItem1" oncommand="executedCommandID = 'cmd_NewItem1';"/> + <command id="cmd_NewItem2" oncommand="executedCommandID = 'cmd_NewItem2';"/> + <command id="cmd_NewItem3" oncommand="executedCommandID = 'cmd_NewItem3';"/> + <command id="cmd_NewItem4" oncommand="executedCommandID = 'cmd_NewItem4';"/> + <command id="cmd_NewItem5" oncommand="executedCommandID = 'cmd_NewItem5';"/> + + <!-- We do not modify any menus or menu items defined here in testing. These + serve as a baseline structure for us to test after other modifications. + We add children to the menubar defined here and test by modifying those + children. --> + <menubar id="nativemenubar"> + <menu id="foo" label="Foo"> + <menupopup> + <menuitem label="FooItem0" command="cmd_FooItem0"/> + <menuitem label="FooItem1" command="cmd_FooItem1"/> + <menuseparator/> + <menu label="Bar"> + <menupopup> + <menuitem label="BarItem0" command="cmd_BarItem0"/> + <menuitem label="BarItem1" command="cmd_BarItem1"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menubar> + + <script type="application/javascript"><![CDATA[ + + function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); + } + + function onTestsFinished() { + window.close(); + window.arguments[0].SimpleTest.finish(); + } + + // Force a menu to update itself. All of the menus parents will be updated + // as well. An empty string will force a complete menu system reload. + function forceUpdateNativeMenuAt(location) { + var utils = window.windowUtils; + try { + utils.forceUpdateNativeMenuAt(location); + } + catch (e) { + dump(e + "\n"); + } + } + + var executedCommandID = ""; + + function testItem(location, targetID) { + var utils = window.windowUtils; + var correctCommandHandler = false; + try { + utils.activateNativeMenuItemAt(location); + correctCommandHandler = executedCommandID == targetID; + } + catch (e) { + dump(e + "\n"); + } + finally { + executedCommandID = ""; + } + return correctCommandHandler; + } + + function createXULMenuPopup() { + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var item = document.createElementNS(XUL_NS, "menupopup"); + return item; + } + + function createXULMenu(aLabel) { + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var item = document.createElementNS(XUL_NS, "menu"); + item.setAttribute("label", aLabel); + return item; + } + + function createXULMenuItem(aLabel, aCommandId) { + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var item = document.createElementNS(XUL_NS, "menuitem"); + item.setAttribute("label", aLabel); + item.setAttribute("command", aCommandId); + return item; + } + + function runBaseMenuTests() { + forceUpdateNativeMenuAt("0|3"); + return testItem("0|0", "cmd_FooItem0") && + testItem("0|1", "cmd_FooItem1") && + testItem("0|3|0", "cmd_BarItem0") && + testItem("0|3|1", "cmd_BarItem1"); + } + + function onLoad() { + var _delayedOnLoad = function() { + // First let's run the base menu tests. + ok(runBaseMenuTests()); + + // Set up some nodes that we'll use. + var menubarNode = document.getElementById("nativemenubar"); + var newMenu0 = createXULMenu("NewMenu0"); + var newMenu1 = createXULMenu("NewMenu1"); + var newMenuPopup0 = createXULMenuPopup(); + var newMenuPopup1 = createXULMenuPopup(); + var newMenuItem0 = createXULMenuItem("NewMenuItem0", "cmd_NewItem0"); + var newMenuItem1 = createXULMenuItem("NewMenuItem1", "cmd_NewItem1"); + var newMenuItem2 = createXULMenuItem("NewMenuItem2", "cmd_NewItem2"); + var newMenuItem3 = createXULMenuItem("NewMenuItem3", "cmd_NewItem3"); + var newMenuItem4 = createXULMenuItem("NewMenuItem4", "cmd_NewItem4"); + var newMenuItem5 = createXULMenuItem("NewMenuItem5", "cmd_NewItem5"); + + // Create another submenu with hierarchy via DOM manipulation. + // ****************** + // * Foo * NewMenu0 * <- Menu bar + // ****************** + // **************** + // * NewMenuItem0 * <- NewMenu0 submenu + // **************** + // * NewMenuItem1 * + // **************** + // * NewMenuItem2 * + // ******************************* + // * NewMenu1 > * NewMenuItem3 * <- NewMenu1 submenu + // ******************************* + // * NewMenuItem4 * + // **************** + // * NewMenuItem5 * + // **************** + newMenu0.appendChild(newMenuPopup0); + newMenuPopup0.appendChild(newMenuItem0); + newMenuPopup0.appendChild(newMenuItem1); + newMenuPopup0.appendChild(newMenuItem2); + newMenuPopup0.appendChild(newMenu1); + newMenu1.appendChild(newMenuPopup1); + newMenuPopup1.appendChild(newMenuItem3); + newMenuPopup1.appendChild(newMenuItem4); + newMenuPopup1.appendChild(newMenuItem5); + //XXX - we have to append the menu to the top-level of the menu bar + // only after constructing it. If we append before construction, it is + // invalid because it has no children and we don't validate it if we add + // children later. + menubarNode.appendChild(newMenu0); + forceUpdateNativeMenuAt("1|3"); + // Run basic tests again. + ok(runBaseMenuTests()); + + // Error strings. + var sa = "Command handler(s) should have activated"; + var sna = "Command handler(s) should not have activated"; + + // Test middle items. + ok(testItem("1|1", "cmd_NewItem1"), sa); + ok(testItem("1|3|1", "cmd_NewItem4"), sa); + + // Hide newMenu0. + newMenu0.setAttribute("hidden", "true"); + ok(runBaseMenuTests(), sa); // the base menu should still be unhidden + ok(!testItem("1|0", ""), sna); + ok(!testItem("1|1", ""), sna); + ok(!testItem("1|2", ""), sna); + ok(!testItem("1|3|0", ""), sna); + ok(!testItem("1|3|1", ""), sna); + ok(!testItem("1|3|2", ""), sna); + + // Show newMenu0. + newMenu0.setAttribute("hidden", "false"); + forceUpdateNativeMenuAt("1|3"); + ok(runBaseMenuTests(), sa); + ok(testItem("1|0", "cmd_NewItem0"), sa); + ok(testItem("1|1", "cmd_NewItem1"), sa); + ok(testItem("1|2", "cmd_NewItem2"), sa); + ok(testItem("1|3|0", "cmd_NewItem3"), sa); + ok(testItem("1|3|1", "cmd_NewItem4"), sa); + ok(testItem("1|3|2", "cmd_NewItem5"), sa); + + // Hide items. + newMenuItem1.setAttribute("hidden", "true"); + newMenuItem4.setAttribute("hidden", "true"); + forceUpdateNativeMenuAt("1|2"); + ok(runBaseMenuTests(), sa); + ok(testItem("1|0", "cmd_NewItem0"), sa); + ok(testItem("1|1", "cmd_NewItem2"), sa); + ok(!testItem("1|2", ""), sna); + ok(testItem("1|2|0", "cmd_NewItem3"), sa); + ok(testItem("1|2|1", "cmd_NewItem5"), sa); + ok(!testItem("1|2|2", ""), sna); + + // Show items. + newMenuItem1.setAttribute("hidden", "false"); + newMenuItem4.setAttribute("hidden", "false"); + forceUpdateNativeMenuAt("1|3"); + ok(runBaseMenuTests(), sa); + ok(testItem("1|0", "cmd_NewItem0"), sa); + ok(testItem("1|1", "cmd_NewItem1"), sa); + ok(testItem("1|2", "cmd_NewItem2"), sa); + ok(testItem("1|3|0", "cmd_NewItem3"), sa); + ok(testItem("1|3|1", "cmd_NewItem4"), sa); + ok(testItem("1|3|2", "cmd_NewItem5"), sa); + + // At this point in the tests the state of the menus has been returned + // to the originally diagramed state. + + // Test command disabling + var cmd_NewItem0 = document.getElementById("cmd_NewItem0"); + ok(testItem("1|0", "cmd_NewItem0"), sa); + cmd_NewItem0.setAttribute("disabled", "true"); + ok(!testItem("1|0", "cmd_NewItem0"), sna); + cmd_NewItem0.removeAttribute("disabled"); + ok(testItem("1|0", "cmd_NewItem0"), sa); + + // Remove menu. + menubarNode.removeChild(newMenu0); + ok(runBaseMenuTests(), sa); + ok(!testItem("1|0", ""), sna); + ok(!testItem("1|1", ""), sna); + ok(!testItem("1|2", ""), sna); + ok(!testItem("1|3|0", ""), sna); + ok(!testItem("1|3|1", ""), sna); + ok(!testItem("1|3|2", ""), sna); + // return state to original diagramed state + menubarNode.appendChild(newMenu0); + + // Test for bug 447042, make sure that adding a menu node with no children + // to the menu bar and then adding another menu node with children works. + // Menus with no children don't get their native menu items shown and that + // caused internal arrays to get out of sync and an append crashed. + var tmpMenu0 = createXULMenu("tmpMenu0"); + menubarNode.removeChild(newMenu0); + menubarNode.appendChild(tmpMenu0); + menubarNode.appendChild(newMenu0); + forceUpdateNativeMenuAt("1|3"); + ok(runBaseMenuTests()); + ok(testItem("1|0", "cmd_NewItem0"), sa); + ok(testItem("1|1", "cmd_NewItem1"), sa); + ok(testItem("1|2", "cmd_NewItem2"), sa); + ok(testItem("1|3|0", "cmd_NewItem3"), sa); + ok(testItem("1|3|1", "cmd_NewItem4"), sa); + ok(testItem("1|3|2", "cmd_NewItem5"), sa); + // return state to original diagramed state + menubarNode.removeChild(tmpMenu0); + + // This test is basically a crash test for bug 433858. + newMenuItem1.setAttribute("hidden", "true"); + newMenuItem2.setAttribute("hidden", "true"); + newMenu1.setAttribute("hidden", "true"); + forceUpdateNativeMenuAt("1"); + newMenuItem1.setAttribute("hidden", "false"); + newMenuItem2.setAttribute("hidden", "false"); + newMenu1.setAttribute("hidden", "false"); + forceUpdateNativeMenuAt("1"); + + onTestsFinished(); + } + + setTimeout(_delayedOnLoad, 1000); + } + + ]]></script> +</window> diff --git a/widget/tests/standalone_native_menu_window.xhtml b/widget/tests/standalone_native_menu_window.xhtml new file mode 100644 index 0000000000..4155126ad5 --- /dev/null +++ b/widget/tests/standalone_native_menu_window.xhtml @@ -0,0 +1,374 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="StandaloneNativeMenuWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="300" + height="300" + onload="onLoad();" + title="nsIStandaloneNativeMenu Test"> + + <command id="cmd_FooItem0" oncommand="gExecutedCommandID = 'cmd_FooItem0';"/> + <command id="cmd_FooItem1" oncommand="gExecutedCommandID = 'cmd_FooItem1';"/> + <command id="cmd_BarItem0" oncommand="gExecutedCommandID = 'cmd_BarItem0';"/> + <command id="cmd_BarItem1" oncommand="gExecutedCommandID = 'cmd_BarItem1';"/> + <command id="cmd_NewItem0" oncommand="gExecutedCommandID = 'cmd_NewItem0';"/> + <command id="cmd_NewItem1" oncommand="gExecutedCommandID = 'cmd_NewItem1';"/> + <command id="cmd_NewItem2" oncommand="gExecutedCommandID = 'cmd_NewItem2';"/> + <command id="cmd_NewItem3" oncommand="gExecutedCommandID = 'cmd_NewItem3';"/> + <command id="cmd_NewItem4" oncommand="gExecutedCommandID = 'cmd_NewItem4';"/> + <command id="cmd_NewItem5" oncommand="gExecutedCommandID = 'cmd_NewItem5';"/> + + <!-- We do not modify any menus or menu items defined here in testing. These + serve as a baseline structure for us to test after other modifications. + We add children to the menubar defined here and test by modifying those + children. --> + <popupset> + <menupopup id="standalonenativemenu"> + <menu id="foo" label="Foo"> + <menupopup> + <menuitem label="FooItem0" command="cmd_FooItem0"/> + <menuitem label="FooItem1" command="cmd_FooItem1"/> + <menuseparator/> + <menu label="Bar"> + <menupopup> + <menuitem label="BarItem0" command="cmd_BarItem0"/> + <menuitem label="BarItem1" command="cmd_BarItem1"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> + </popupset> + + <script type="application/javascript"><![CDATA[ + + function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); + } + + function is(a, b, message) { + window.arguments[0].SimpleTest.is(a, b, message); + } + + function isnot(a, b, message) { + window.arguments[0].SimpleTest.isnot(a, b, message); + } + + function todo(condition, message) { + window.arguments[0].SimpleTest.todo(condition, message); + } + + function todo_is(a, b, message) { + window.arguments[0].SimpleTest.todo_is(a, b, message); + } + + function todo_isnot(a, b, message) { + window.arguments[0].SimpleTest.todo_isnot(a, b, message); + } + + function onTestsFinished() { + window.close(); + window.arguments[0].SimpleTest.finish(); + } + + var gExecutedCommandID; + + // returns the executed command ID, or the empty string if nothing was executed + function activateItem(menu, location) { + try { + gExecutedCommandID = ""; + menu.menuWillOpen(); + menu.activateNativeMenuItemAt(location); + return gExecutedCommandID; + } + catch (e) { + // activateNativeMenuItemAt throws an exception if the item was not found + dump(e + "\n"); + return ""; + } + } + + function testItem(menu, location, targetID) { + var activatedCommandID = activateItem(menu, location); + return activatedCommandID != "" && activatedCommandID == targetID; + } + + var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + function createXULMenuPopup() { + return document.createElementNS(XUL_NS, "menupopup"); + } + + function createXULMenu(aLabel) { + var item = document.createElementNS(XUL_NS, "menu"); + item.setAttribute("label", aLabel); + return item; + } + + function createXULMenuItem(aLabel, aCommandId) { + var item = document.createElementNS(XUL_NS, "menuitem"); + item.setAttribute("label", aLabel); + item.setAttribute("command", aCommandId); + return item; + } + + function runBaseMenuTests(menu) { + menu.forceUpdateNativeMenuAt("0|3"); + return testItem(menu, "0|0", "cmd_FooItem0") && + testItem(menu, "0|1", "cmd_FooItem1") && + testItem(menu, "0|3|0", "cmd_BarItem0") && + testItem(menu, "0|3|1", "cmd_BarItem1"); + } + + function createStandaloneNativeMenu(menuNode) { + try { + let menu = Cc["@mozilla.org/widget/standalonenativemenu;1"].createInstance(Ci.nsIStandaloneNativeMenu); + menu.init(menuNode); + return menu; + } catch (e) { + ok(false, "Failed creating nsIStandaloneNativeMenu instance"); + throw e; + } + } + + function runDetachedMenuTests(addMenupopupBeforeCreatingSNM) { + let menu = createXULMenu("Detached menu"); + menu.setAttribute("image", 'data:image/svg+xml,<svg%20xmlns="http://www.w3.org/2000/svg"%20width="32"%20height="32"><circle%20cx="16"%20cy="16"%20r="16"/></svg>'); + let menupopup = createXULMenuPopup(); + + let popupShowingFired = false; + let itemActivated = false; + + menupopup.addEventListener("popupshowing", function (e) { + popupShowingFired = true; + + let menuitem = document.createElementNS(XUL_NS, "menuitem"); + menuitem.setAttribute("label", "detached menu item"); + /* eslint-disable-next-line no-shadow */ + menuitem.addEventListener("command", function (e) { + itemActivated = true; + }) + menupopup.appendChild(menuitem); + }) + + // It shouldn't make a difference whether the menupopup is added to the + // menu element before or after we create the nsIStandaloneNativeMenu + // instance with it. We test both orders by calling this function twice + // with different values for addMenupopupBeforeCreatingSNM. + + var menuSNM = null; // the nsIStandaloneNativeMenu object for "menu" + if (addMenupopupBeforeCreatingSNM) { + menu.appendChild(menupopup); + menuSNM = createStandaloneNativeMenu(menu); + } else { + menuSNM = createStandaloneNativeMenu(menu); + menu.appendChild(menupopup); + } + + try { + ok(!popupShowingFired, "popupshowing shouldn't have fired before our call to menuWillOpen()"); + menuSNM.menuWillOpen(); + ok(popupShowingFired, "calling menuWillOpen() should have notified our popupshowing listener"); + + ok(!itemActivated, "our dynamically-added menuitem shouldn't have been activated yet"); + menuSNM.activateNativeMenuItemAt("0"); + ok(itemActivated, "the new menu item should have been activated now"); + } catch (ex) { + ok(false, "dynamic menu test failed: " + ex); + } + } + + function onLoad() { + var _delayedOnLoad = function() { + try { + + var menuNode = document.getElementById("standalonenativemenu"); + var menu = createStandaloneNativeMenu(menuNode); + + // First let's run the base menu tests. + ok(runBaseMenuTests(menu), "base tests #1"); + + // Set up some nodes that we'll use. + var newMenu0 = createXULMenu("NewMenu0"); + var newMenu1 = createXULMenu("NewMenu1"); + var newMenuPopup0 = createXULMenuPopup(); + var newMenuPopup1 = createXULMenuPopup(); + var newMenuItem0 = createXULMenuItem("NewMenuItem0", "cmd_NewItem0"); + var newMenuItem1 = createXULMenuItem("NewMenuItem1", "cmd_NewItem1"); + var newMenuItem2 = createXULMenuItem("NewMenuItem2", "cmd_NewItem2"); + var newMenuItem3 = createXULMenuItem("NewMenuItem3", "cmd_NewItem3"); + var newMenuItem4 = createXULMenuItem("NewMenuItem4", "cmd_NewItem4"); + var newMenuItem5 = createXULMenuItem("NewMenuItem5", "cmd_NewItem5"); + + // Create another submenu with hierarchy via DOM manipulation. + // ****************** + // * Foo * NewMenu0 * <- Menu bar + // ****************** + // **************** + // * NewMenuItem0 * <- NewMenu0 submenu + // **************** + // * NewMenuItem1 * + // **************** + // * NewMenuItem2 * + // ******************************* + // * NewMenu1 > * NewMenuItem3 * <- NewMenu1 submenu + // ******************************* + // * NewMenuItem4 * + // **************** + // * NewMenuItem5 * + // **************** + newMenu0.appendChild(newMenuPopup0); + newMenuPopup0.appendChild(newMenuItem0); + newMenuPopup0.appendChild(newMenuItem1); + newMenuPopup0.appendChild(newMenuItem2); + newMenuPopup0.appendChild(newMenu1); + newMenu1.appendChild(newMenuPopup1); + newMenuPopup1.appendChild(newMenuItem3); + newMenuPopup1.appendChild(newMenuItem4); + newMenuPopup1.appendChild(newMenuItem5); + //XXX - we have to append the menu to the top-level of the menu bar + // only after constructing it. If we append before construction, it is + // invalid because it has no children and we don't validate it if we add + // children later. + menuNode.appendChild(newMenu0); + menu.forceUpdateNativeMenuAt("1|3"); + // Run basic tests again. + ok(runBaseMenuTests(menu), "base tests #2"); + + // Error strings. + var sa = "Command handler(s) should have activated"; + var sna = "Command handler(s) should not have activated"; + + // Test middle items. + is(activateItem(menu, "1|1"), "cmd_NewItem1", "#1:" + sa); + is(activateItem(menu, "1|3|1"), "cmd_NewItem4", "#2:" + sa); + + // Hide newMenu0. + newMenu0.setAttribute("hidden", "true"); + ok(runBaseMenuTests(menu), "base tests #3: " + sa); // the base menu should still be unhidden + is(activateItem(menu, "1|0"), "", "#3:" + sna); + is(activateItem(menu, "1|1"), "", "#4:" + sna); + is(activateItem(menu, "1|2"), "", "#5:" + sna); + is(activateItem(menu, "1|3|0"), "", "#6:" + sna); + is(activateItem(menu, "1|3|1"), "", "#7:" + sna); + is(activateItem(menu, "1|3|2"), "", "#8:" + sna); + + // Show newMenu0. + newMenu0.setAttribute("hidden", "false"); + menu.forceUpdateNativeMenuAt("1|3"); + ok(runBaseMenuTests(menu), "base tests #4:" + sa); + is(activateItem(menu, "1|0"), "cmd_NewItem0", "#9:" + sa); + is(activateItem(menu, "1|1"), "cmd_NewItem1", "#10:" + sa); + is(activateItem(menu, "1|2"), "cmd_NewItem2", "#11:" + sa); + is(activateItem(menu, "1|3|0"), "cmd_NewItem3", "#12:" + sa); + is(activateItem(menu, "1|3|1"), "cmd_NewItem4", "#13:" + sa); + is(activateItem(menu, "1|3|2"), "cmd_NewItem5", "#14:" + sa); + + // Hide items. + newMenuItem1.setAttribute("hidden", "true"); + newMenuItem4.setAttribute("hidden", "true"); + menu.forceUpdateNativeMenuAt("1|2"); + ok(runBaseMenuTests(menu), "base tests #5:" + sa); + is(activateItem(menu, "1|0"), "cmd_NewItem0", "#15:" + sa); + is(activateItem(menu, "1|1"), "cmd_NewItem2", "#16:" + sa); + is(activateItem(menu, "1|2"), "", "#17:" + sna); + is(activateItem(menu, "1|2|0"), "cmd_NewItem3", "#18:" + sa); + is(activateItem(menu, "1|2|1"), "cmd_NewItem5", "#19:" + sa); + is(activateItem(menu, "1|2|2"), "", "#20:" + sna); + + // Show items. + newMenuItem1.setAttribute("hidden", "false"); + newMenuItem4.setAttribute("hidden", "false"); + //forceUpdateNativeMenuAt("1|3"); + ok(runBaseMenuTests(menu), "base tests #6:" + sa); + is(activateItem(menu, "1|0"), "cmd_NewItem0", "#21:" + sa); + is(activateItem(menu, "1|1"), "cmd_NewItem1", "#22:" + sa); + is(activateItem(menu, "1|2"), "cmd_NewItem2", "#23:" + sa); + is(activateItem(menu, "1|3|0"), "cmd_NewItem3", "#24:" + sa); + is(activateItem(menu, "1|3|1"), "cmd_NewItem4", "#25:" + sa); + is(activateItem(menu, "1|3|2"), "cmd_NewItem5", "#26:" + sa); + + // At this point in the tests the state of the menus has been returned + // to the originally diagramed state. + + // Remove menu. + menuNode.removeChild(newMenu0); + ok(runBaseMenuTests(menu), "base tests #7:" + sa); + is(activateItem(menu, "1|0"), "", "#27:" + sna); + is(activateItem(menu, "1|1"), "", "#28:" + sna); + is(activateItem(menu, "1|2"), "", "#29:" + sna); + is(activateItem(menu, "1|3|0"), "", "#30:" + sna); + is(activateItem(menu, "1|3|1"), "", "#31:" + sna); + is(activateItem(menu, "1|3|2"), "", "#32:" + sna); + // return state to original diagramed state + menuNode.appendChild(newMenu0); + + // The following is based on a similar test bug 447042 from the native + // menu bar test: Make sure that adding a menu node with no children + // to the menu bar and then adding another menu node with children works. + // In the menubar, root menus with no children are skipped - they're not + // visible in the menubar. + // Regular menus currently treat submenus without children differently: + // submenus without children *are* visible. + // We may want to change this in the future. + // After the mutation below we have the following root menu content: + // - [0] Foo (with submenu) + // - [1] tmpMenu0 (with empty submenu) + // - [2] NewMenu0 (with submenu) + // Since the empty tmpMenu0 item is not skipped, NewMenu0 has index 2, + // so we use "2|..." below, rather than the "1|..." that's used in the + // menubar test. + var tmpMenu0 = createXULMenu("tmpMenu0"); + menuNode.removeChild(newMenu0); + menuNode.appendChild(tmpMenu0); + menuNode.appendChild(newMenu0); + menu.forceUpdateNativeMenuAt("2|3"); + ok(runBaseMenuTests(menu), "base tests #8"); + is(activateItem(menu, "2|0"), "cmd_NewItem0", "#33:" + sa); + is(activateItem(menu, "2|1"), "cmd_NewItem1", "#34:" + sa); + is(activateItem(menu, "2|2"), "cmd_NewItem2", "#35:" + sa); + is(activateItem(menu, "2|3|0"), "cmd_NewItem3", "#36:" + sa); + is(activateItem(menu, "2|3|1"), "cmd_NewItem4", "#37:" + sa); + is(activateItem(menu, "2|3|2"), "cmd_NewItem5", "#38:" + sa); + // return state to original diagramed state + menuNode.removeChild(tmpMenu0); + + // This test is basically a crash test for bug 433858. + newMenuItem1.setAttribute("hidden", "true"); + newMenuItem2.setAttribute("hidden", "true"); + newMenu1.setAttribute("hidden", "true"); + menu.forceUpdateNativeMenuAt("1"); + newMenuItem1.setAttribute("hidden", "false"); + newMenuItem2.setAttribute("hidden", "false"); + newMenu1.setAttribute("hidden", "false"); + menu.forceUpdateNativeMenuAt("1"); + + // Check that "path components" which are out-of-range are not ignored. + // There are only two menu items in the root menu (with index 0 and 1), + // so index 2 is out of range. + is(activateItem(menu, "2|1|0"), "", "#39:" + sna); + + // Check that hiding and then un-hiding the root menu doesn't result in + // a cyclic native menu structure. + menuNode.setAttribute("collapsed", "true"); + menuNode.removeAttribute("collapsed"); + ok(runBaseMenuTests(menu), "base tests #9"); + + // Run tests where the menu nodes are not in the document's node tree. + runDetachedMenuTests(false); + runDetachedMenuTests(true); + + } catch (e) { + ok(false, "Caught an exception: " + e); + } finally { + onTestsFinished(); + } + } + + setTimeout(_delayedOnLoad, 1000); + } + + ]]></script> +</window> diff --git a/widget/tests/system_font_changes.xhtml b/widget/tests/system_font_changes.xhtml new file mode 100644 index 0000000000..1cef650015 --- /dev/null +++ b/widget/tests/system_font_changes.xhtml @@ -0,0 +1,63 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="system_font_changes_window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="300" + height="300" + onload="start();"> + +<span id="target" style="font:menu">Hello world</span> + +<script type="application/javascript"><![CDATA[ + function is(condition, message) { + window.arguments[0].SimpleTest.is(condition, message); + } + function registerCleanupFunction(func) { + window.arguments[0].SimpleTest.registerCleanupFunction(func); + } + + async function waitForFrame() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); + } + + let windowUtils = window.windowUtils; + async function start() { + await waitForFrame(); + + const originalSystemFont = windowUtils.systemFont; + registerCleanupFunction(() => { + windowUtils.systemFont = originalSystemFont; + }); + + windowUtils.systemFont = 'Sans 11'; + is(windowUtils.systemFont, 'Sans 11'); + + // Wait for two frames for the safety since the notification for system + // font changes is asynchronously processed. + await waitForFrame(); + await waitForFrame(); + + const target = document.getElementById('target'); + is(getComputedStyle(target).fontFamily, 'Sans'); + + windowUtils.systemFont = 'Serif 11'; + is(windowUtils.systemFont, 'Serif 11'); + + await waitForFrame(); + await waitForFrame(); + + is(getComputedStyle(target).fontFamily, 'Serif'); + + window.close(); + window.arguments[0].SimpleTest.finish(); + } +]]></script> +</window> diff --git a/widget/tests/taskbar_previews.xhtml b/widget/tests/taskbar_previews.xhtml new file mode 100644 index 0000000000..b220d5662b --- /dev/null +++ b/widget/tests/taskbar_previews.xhtml @@ -0,0 +1,116 @@ +<?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 title="Taskbar Previews Test" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="loaded();"> + + <title>Previews - yeah!</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script class="testbody" type="application/javascript"> + <![CDATA[ + let taskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(Ci.nsIWinTaskbar); + + function IsWin7OrHigher() { + try { + var ver = parseFloat(Services.sysinfo.getProperty("version")); + if (ver >= 6.1) + return true; + } catch (ex) { } + return false; + } + isnot(taskbar, null, "Taskbar service is defined"); + is(taskbar.available, IsWin7OrHigher(), "Expected availability of taskbar"); + + SimpleTest.waitForExplicitFinish(); + + function stdPreviewSuite(p) { + p.visible = !p.visible; + p.visible = !p.visible; + p.visible = true; + p.invalidate(); + p.visible = false; + } + + function loaded() + { + if (!taskbar.available) + SimpleTest.finish(); + let controller = { + width: 400, + height: 400, + thumbnailAspectRatio: 1.0, + get wrappedJSObject() { return this; } + } + // HACK from mconnor: + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let docShell = win.gBrowser.docShell; + + let winPreview = taskbar.getTaskbarWindowPreview(docShell); + isnot(winPreview, null, "Window preview is not null"); + winPreview.controller = controller; + let button = winPreview.getButton(0); + isnot(button, null, "Could get button at valid index"); + try { + winPreview.getButton(-1); + ok(false, "Got button at negative index"); + } catch (ex) {} + try { + winPreview.getButton(Ci.nsITaskbarWindowPreview.NUM_TOOLBAR_BUTTONS); + ok(false, "Got button at index that is too large"); + } catch (ex) {} + button.image = null; + stdPreviewSuite(winPreview); + // Let's not perma-hide this window from the taskbar + winPreview.visible = true; + + let tabP = taskbar.createTaskbarTabPreview(docShell, controller); + isnot(tabP, null, "Tab preview is not null"); + is(tabP.controller.wrappedJSObject, controller, "Controllers match"); + is(tabP.icon, null, "Default icon is null (windows default)"); + tabP.icon = null; + tabP.move(null); + try { + tabP.move(tabP); + ok(false, "Moved a preview next to itself!"); + } catch (ex) {} + stdPreviewSuite(tabP); + + let tabP2 = taskbar.createTaskbarTabPreview(docShell, controller); + tabP.visible = true; + tabP2.visible = true; + + isnot(tabP2, null, "2nd Tab preview is not null"); + isnot(tabP,tabP2, "Tab previews are different"); + tabP.active = true; + ok(tabP.active && !tabP2.active, "Only one tab is active (part 1)"); + tabP2.active = true; + ok(!tabP.active && tabP2.active, "Only one tab is active (part 2)"); + tabP.active = true; + ok(tabP.active && !tabP2.active, "Only one tab is active (part 3)"); + tabP.active = false; + ok(!tabP.active && !tabP2.active, "Neither tab is active"); + is(winPreview.active, false, "Window preview is not active"); + tabP.active = true; + winPreview.active = true; + ok(winPreview.active && !tabP.active, "Tab preview takes activation from window"); + tabP.active = true; + ok(tabP.active && !winPreview.active, "Tab preview takes activation from window"); + + tabP.visible = false; + tabP2.visible = false; + + SimpleTest.finish(); + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/widget/tests/test_AltGr_key_events_in_web_content_on_windows.html b/widget/tests/test_AltGr_key_events_in_web_content_on_windows.html new file mode 100644 index 0000000000..ff89f8fdad --- /dev/null +++ b/widget/tests/test_AltGr_key_events_in_web_content_on_windows.html @@ -0,0 +1,106 @@ +<!DOCTYPE html> +<html> +<head> + <title>Testing if AltGr keydown and keyup events are fired in web content on Windows</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/NativeKeyCodes.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> + <input id="input"> +</div> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +function checkEvent(aEvent, aExpectedEvents, aDescription) { + if (!aExpectedEvents.length) { + ok(false, `${aDescription}: no more expected events ` + + `(type: ${aEvent.type}, code: ${aEvent.code}, key: ${aEvent.key}, keyCode: ${aEvent.keyCode}`); + } + let expectedEvent = aExpectedEvents.shift(); + for (let property in expectedEvent) { + is(aEvent[property], expectedEvent[property], `${aDescription}: ${property}`); + } +} + +async function runAltGrKeyTest() { + return new Promise(resolve => { + let target = document.getElementById("input"); + target.focus(); + + let events = [ + { type: "keydown", code: "ControlLeft", key: "Control", keyCode: KeyboardEvent.DOM_VK_CONTROL }, + { type: "keydown", code: "AltRight", key: "AltGraph", keyCode: KeyboardEvent.DOM_VK_ALT }, + { type: "keyup", code: "ControlLeft", key: "Control", keyCode: KeyboardEvent.DOM_VK_CONTROL }, + { type: "keyup", code: "AltRight", key: "AltGraph", keyCode: KeyboardEvent.DOM_VK_ALT }, + ]; + function handleEvent(aEvent) { + checkEvent(aEvent, events, "runAltGrKeyTest"); + if (aEvent.type === "keyup" && aEvent.code === "AltRight") { + is(events.length, 0, "runAltGrKeyTest: all expected events are fired"); + SimpleTest.executeSoon(() => { + target.removeEventListener("keydown", handleEvent); + target.removeEventListener("keypress", handleEvent); + target.removeEventListener("keyup", handleEvent); + resolve(); + }); + } + } + target.addEventListener("keydown", handleEvent); + target.addEventListener("keypress", handleEvent); + target.addEventListener("keyup", handleEvent); + + synthesizeNativeKey(KEYBOARD_LAYOUT_SPANISH, WIN_VK_RMENU, {}, + "", ""); + }); +} + +async function runEmulatingAltGrKeyTest() { + return new Promise(resolve => { + let target = document.getElementById("input"); + target.focus(); + + let events = [ + { type: "keydown", code: "ControlLeft", key: "Control", keyCode: KeyboardEvent.DOM_VK_CONTROL }, + { type: "keydown", code: "AltLeft", key: "Alt", keyCode: KeyboardEvent.DOM_VK_ALT }, + { type: "keyup", code: "AltLeft", key: "Alt", keyCode: KeyboardEvent.DOM_VK_ALT }, + { type: "keyup", code: "ControlLeft", key: "Control", keyCode: KeyboardEvent.DOM_VK_CONTROL }, + ]; + function handleEvent(aEvent) { + checkEvent(aEvent, events, "runEmulatingAltGrKeyTest"); + if (aEvent.type === "keyup" && aEvent.code === "ControlLeft") { + is(events.length, 0, "runAltGrKeyTest: all expected events are fired"); + SimpleTest.executeSoon(() => { + target.removeEventListener("keydown", handleEvent); + target.removeEventListener("keypress", handleEvent); + target.removeEventListener("keyup", handleEvent); + resolve(); + }); + } + } + target.addEventListener("keydown", handleEvent); + target.addEventListener("keypress", handleEvent); + target.addEventListener("keyup", handleEvent); + + synthesizeNativeKey(KEYBOARD_LAYOUT_SPANISH, WIN_VK_LMENU, { ctrlKey: true }, + "", ""); + }); +} + +async function runTests() { + await runAltGrKeyTest(); + await runEmulatingAltGrKeyTest(); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); +</script> +</body> +</html>
\ No newline at end of file diff --git a/widget/tests/test_actionhint.html b/widget/tests/test_actionhint.html new file mode 100644 index 0000000000..6ab2cf40de --- /dev/null +++ b/widget/tests/test_actionhint.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<html> +<head> +<title>Tests for action hint that is used by software keyboard</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/SpecialPowers.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<div> +<form><input type="text" id="a1"><input type="text" id="a2"><input type="submit"></form> +<form><input type="search" id="b1"><input type="submit"></form> +<form><input type="text" id="c1"></form> +<form><input type="text" id="d1"><textarea></textarea><input type="submit"></form> +<form><input type="text" id="e1"><input type="number"><input type="submit"></form> +<form><input type="text" id="f1"><input type="date"><input type="submit"></form> +<form><input type="text" id="g1"><input type="radio"><input type="submit"></form> +<form><input type="text" id="h1"><input type="text" readonly><input type="submit"></form> +<form><input type="text" id="i1"><input type="text" disabled><input type="submit"></form> +<input type="text" id="j1"><input type="text"><input type="button"> +<form><input type="text" id="k1"><a href="#foo">foo</a><input type="text"><input type="submit"></form> +<form> + <input id="l1" enterkeyhint="enter"> + <input id="l2" enterkeyhint="DONE"> + <input id="l3" enterkeyhint="go"> + <input id="l4" enterkeyhint="Next"> + <input id="l5" enterkeyhint="Previous"> + <input id="l6" enterkeyhint="search"> + <textarea id="l7" enterkeyhint="send"></textarea> + <input id="l8" type="number" enterkeyhint="previous"> + <input id="l9" type="date" enterkeyhint="done"> + <input id="l10" type="time" enterkeyhint="done"> + <input id="l11" enterkeyhint="NONE"> +</form> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> +add_task(async function setup() { + await new Promise(r => SimpleTest.waitForFocus(r)); +}); + +add_task(async function basic() { + const tests = [ + { id: "a1", hint: "maybenext", desc: "next element is type=text" }, + { id: "a2", hint: "go", desc: "next element is type=submit" }, + { id: "b1", hint: "search", desc: "current is type=search" }, + { id: "c1", hint: "go", desc: "only this element" }, + { id: "d1", hint: "maybenext", desc: "next element is textarea" }, + { id: "e1", hint: "maybenext", desc: "next element is type=number" }, + { id: "h1", hint: "go", desc: "next element is readonly" }, + // XXX Feel free to change this result if you get some bugs reports + { id: "i1", hint: "go", desc: "next element is disabled" }, + { id: "j1", hint: "", desc: "no form element" }, + { id: "l1", hint: "enter", desc: "enterkeyhint=enter" }, + { id: "l2", hint: "done", desc: "enterkeyhint=DONE" }, + { id: "l3", hint: "go", desc: "enterkeyhint=go" }, + { id: "l4", hint: "next", desc: "enterkeyhint=Next" }, + { id: "l5", hint: "previous", desc: "enterkeyhint=Previous" }, + { id: "l6", hint: "search", desc: "enterkeyhint=search" }, + { id: "l7", hint: "send", desc: "enterkeyhint=send" }, + { id: "l8", hint: "previous", desc: "type=number enterkeyhint=previous" }, + // type=date is readonly content + { id: "l9", hint: "", desc: "type=date enterkeyhint=done" }, + // type=time is readonly content + { id: "l10", hint: "", desc: "type=time enterkeyhint=done" }, + // Since enterkeyhint is invalid, we infer action hint. So feel free to change this. + { id: "l11", hint: "go", desc: "enterkeyhint is invalid" }, + ]; + + const todo_tests = [ + { id: "f1", hint: "maybenext", desc: "next element is type=date" }, + { id: "k1", hint: "", desc: "next is anchor link" }, + ]; + + + for (let test of tests) { + document.getElementById(test.id).focus(); + is(SpecialPowers.DOMWindowUtils.focusedActionHint, test.hint, test.desc); + } + + for (let test of todo_tests) { + document.getElementById(test.id).focus(); + todo_is(SpecialPowers.DOMWindowUtils.focusedActionHint, test.hint, test.desc); + } +}); + +add_task(async function dynamicChange() { + let element = document.getElementById("l1"); + element.focus(); + is(SpecialPowers.DOMWindowUtils.focusedActionHint, "enter", + "Initial enterKeyHint"); + + element.setAttribute("enterkeyhint", "next"); + is(SpecialPowers.DOMWindowUtils.focusedActionHint, "next", + "enterKeyHint in InputContext has to sync with enterkeyhint attribute"); + + element.enterKeyHint = "search"; + is(SpecialPowers.DOMWindowUtils.focusedActionHint, "search", + "enterKeyHint in InputContext has to sync with enterKeyHint setter"); + + document.getElementById("l2").setAttribute("enterkeyhint", "send"); + is(SpecialPowers.DOMWindowUtils.focusedActionHint, "search", + "enterKeyHint in InputContext keeps focused enterKeyHint value"); + + // Storing the original value may be safer. + element.enterkeyhint = "enter"; + document.getElementById("l2").enterKeyHint = "done"; +}); +</script> +</pre> +</body> +</html> diff --git a/widget/tests/test_alwaysontop_focus.xhtml b/widget/tests/test_alwaysontop_focus.xhtml new file mode 100644 index 0000000000..b9cc3ee33c --- /dev/null +++ b/widget/tests/test_alwaysontop_focus.xhtml @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta charset="utf-8" /> + <title>Test that alwaysontop windows do not pull focus when opened.</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script><![CDATA[ + add_task(async function testAlwaysOnTop() { + let topWin = window.docShell.rootTreeItem.domWindow; + await SimpleTest.promiseFocus(topWin); + is(Services.focus.activeWindow, topWin, "Top level window is focused"); + + let newWin = Services.ww.openWindow( + null, + "about:blank", + null, + "chrome,alwaysontop,width=300,height=300", + null + ); + await new Promise(resolve => { + newWin.addEventListener("load", resolve, { once: true }); + }); + + // Wait one tick of the event loop to give the window a chance to focus. + await new Promise(resolve => { SimpleTest.executeSoon(resolve); }); + + is(Services.focus.activeWindow, topWin, "Top level window is still focused"); + newWin.close(); + }); + ]]></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/widget/tests/test_assign_event_data.html b/widget/tests/test_assign_event_data.html new file mode 100644 index 0000000000..1da9bb535f --- /dev/null +++ b/widget/tests/test_assign_event_data.html @@ -0,0 +1,708 @@ +<!DOCTYPE html> +<html> +<head> + <title>Testing ns*Event::Assign*EventData()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/NativeKeyCodes.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <style> + #a { + background-color: transparent; + transition: background-color 0.1s linear; + } + #a:focus { + background-color: red; + } + .slidin { + border: green 1px solid; + width: 10px; + height: 10px; + animation-name: slidein; + animation-duration: 1s; + } + @keyframes slidein { + from { + margin-left: 100%; + } + to { + margin-left: 0; + } + } + #pointer-target { + border: 1px dashed red; + background: yellow; + margin: 0px 10px; + padding: 0px 10px; + } + #scrollable-div { + background: green; + overflow: auto; + width: 30px; + height: 30px; + } + #scrolled-div { + background: magenta; + width: 10px; + height: 10px; + } + #form { + background: silver; + padding: 0px 10px; + } + #animated-div { + background: cyan; + padding: 0px 10px; + } + </style> +</head> +<body> +<div id="display"> + <input id="input-text"> + <button id="button">button</button> + <a id="a" href="about:blank">hyper link</a> + <span id="pointer-target">span</span> + <div id="scrollable-div"><div id="scrolled-div"></div></div> + <form id="form">form</form> + <div id="animated-div"> </div> +</div> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectAssertions(0, 34); + +const kIsMac = (navigator.platform.indexOf("Mac") == 0); +const kIsWin = (navigator.platform.indexOf("Win") == 0); + +var gDescription = ""; +var gEvent = null; +var gCopiedEvent = []; +var gCallback = null; +var gCallPreventDefault = false; + +function onEvent(aEvent) { + if (gCallPreventDefault) { + aEvent.preventDefault(); + } + ok(gEvent === null, gDescription + `: We should receive only one event to check per test: already got: ${gEvent ? gEvent.type : "null"}, got: ${aEvent.type}`); + gEvent = aEvent; + for (var attr in aEvent) { + if (!attr.match(/^[A-Z0-9_]+$/) && // ignore const attributes + attr != "multipleActionsPrevented" && // multipleActionsPrevented isn't defined in any DOM event specs. + typeof(aEvent[attr]) != "function") { + gCopiedEvent.push({ name: attr, value: aEvent[attr]}); + } + } + setTimeout(gCallback, 0); +} + +function observeKeyUpOnContent(aKeyCode, aCallback) { + document.addEventListener("keyup", function keyUp(ev) { + if (ev.keyCode == aKeyCode) { + document.removeEventListener("keyup", keyUp); + SimpleTest.executeSoon(aCallback); + } + }); +} + +const kTests = [ + { description: "InternalScrollPortEvent (overflow, vertical)", + targetID: "scrollable-div", eventType: "overflow", + dispatchEvent() { + document.getElementById("scrolled-div").style.height = "500px"; + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "InternalScrollPortEvent (overflow, horizontal)", + targetID: "scrollable-div", eventType: "overflow", + dispatchEvent() { + document.getElementById("scrolled-div").style.width = "500px"; + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "InternalScrollAreaEvent (MozScrolledAreaChanged, spreading)", + target() { return document; }, eventType: "MozScrolledAreaChanged", + dispatchEvent() { + document.getElementById("scrollable-div").style.width = "50000px"; + document.getElementById("scrollable-div").style.height = "50000px"; + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "InternalScrollAreaEvent (MozScrolledAreaChanged, shrinking)", + target() { return document; }, eventType: "MozScrolledAreaChanged", + dispatchEvent() { + document.getElementById("scrollable-div").style.width = "30px"; + document.getElementById("scrollable-div").style.height = "30px"; + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "WidgetKeyboardEvent (keydown of 'a' key without modifiers)", + targetID: "input-text", eventType: "keydown", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + document.getElementById(this.targetID).focus(); + synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_A : MAC_VK_ANSI_A, + {}, "a", "a"); + observeKeyUpOnContent(KeyboardEvent.DOM_VK_A, runNextTest); + return true; + }, + canRun() { + return (kIsMac || kIsWin); + }, + todoMismatch: [], + }, + { description: "WidgetKeyboardEvent (keyup of 'a' key without modifiers)", + targetID: "input-text", eventType: "keydown", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + document.getElementById(this.targetID).focus(); + synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_A : MAC_VK_ANSI_A, + {}, "a", "a"); + observeKeyUpOnContent(KeyboardEvent.DOM_VK_A, runNextTest); + return true; + }, + canRun() { + return (kIsMac || kIsWin); + }, + todoMismatch: [], + }, + { description: "WidgetKeyboardEvent (keypress of 'b' key with Shift)", + targetID: "input-text", eventType: "keypress", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + document.getElementById(this.targetID).focus(); + synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_B : MAC_VK_ANSI_B, + { shiftKey: true }, "B", "B"); + + // On Windows, synthesizeNativeKey will also fire keyup for shiftKey. + // We have to wait for it to prevent the key event break the next test case. + let waitKeyCode = _EU_isWin(window) ? KeyboardEvent.DOM_VK_SHIFT : + KeyboardEvent.DOM_VK_B; + observeKeyUpOnContent(waitKeyCode, runNextTest); + return true; + }, + canRun() { + return (kIsMac || kIsWin); + }, + todoMismatch: [], + }, + { description: "WidgetKeyboardEvent (keyup during composition)", + targetID: "input-text", eventType: "keyup", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + document.getElementById(this.targetID).focus(); + synthesizeCompositionChange({ "composition": + { "string": "\u306D", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + synthesizeComposition({ type: "compositioncommitasis", key: {} }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetKeyboardEvent (keydown during composition)", + targetID: "input-text", eventType: "keydown", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + document.getElementById(this.targetID).focus(); + synthesizeCompositionChange({ "composition": + { "string": "\u306D", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": 1, "length": 0 }, + "key": {}, + }); + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Enter" } }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetMouseEvent (mousedown of left button without modifier)", + targetID: "button", eventType: "mousedown", + dispatchEvent() { + synthesizeMouseAtCenter(document.getElementById(this.targetID), + { button: 0 }); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "WidgetMouseEvent (click of middle button with Shift)", + targetID: "button", eventType: "auxclick", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + synthesizeMouseAtCenter(document.getElementById(this.targetID), + { button: 1, shiftKey: true, pressure: 0.5 }); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "WidgetMouseEvent (mouseup of right button with Alt)", + targetID: "button", eventType: "mouseup", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + synthesizeMouseAtCenter(document.getElementById(this.targetID), + { button: 2, altKey: true }); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "WidgetDragEvent", + targetID: "input-text", eventType: "dragstart", + dispatchEvent() { + + }, + canRun() { + todo(false, "WidgetDragEvent isn't tested"); + return false; + }, + todoMismatch: [], + }, + { description: "WidgetCompositionEvent (compositionupdate)", + targetID: "input-text", eventType: "compositionupdate", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + document.getElementById(this.targetID).focus(); + synthesizeComposition({ type: "compositioncommit", data: "\u30E9\u30FC\u30E1\u30F3", key: { key: "KEY_Enter" } }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "InternalEditorInputEvent (input at key input)", + targetID: "input-text", eventType: "input", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + document.getElementById(this.targetID).focus(); + synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_B : MAC_VK_ANSI_B, + { shiftKey: true }, "B", "B"); + observeKeyUpOnContent(KeyboardEvent.DOM_VK_B, runNextTest); + return true; + }, + canRun() { + return (kIsMac || kIsWin); + }, + todoMismatch: [], + }, + { description: "InternalEditorInputEvent (input at composing)", + targetID: "input-text", eventType: "input", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + document.getElementById(this.targetID).focus(); + synthesizeCompositionChange({ "composition": + { "string": "\u30E9\u30FC\u30E1\u30F3", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": 4, "length": 0 }, + "key": { key: "y" }, + }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "InternalEditorInputEvent (input at committing)", + targetID: "input-text", eventType: "input", + dispatchEvent() { + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetMouseScrollEvent (DOMMouseScroll, vertical)", + targetID: "input-text", eventType: "DOMMouseScroll", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + synthesizeWheel(document.getElementById(this.targetID), 3, 4, + { deltaY: 30, lineOrPageDeltaY: 2 }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetMouseScrollEvent (DOMMouseScroll, horizontal)", + targetID: "input-text", eventType: "DOMMouseScroll", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + synthesizeWheel(document.getElementById(this.targetID), 4, 5, + { deltaX: 30, lineOrPageDeltaX: 2, shiftKey: true }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetMouseScrollEvent (MozMousePixelScroll, vertical)", + targetID: "input-text", eventType: "MozMousePixelScroll", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + synthesizeWheel(document.getElementById(this.targetID), 3, 4, + { deltaY: 20, lineOrPageDeltaY: 1, altKey: true }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetMouseScrollEvent (MozMousePixelScroll, horizontal)", + targetID: "input-text", eventType: "MozMousePixelScroll", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + synthesizeWheel(document.getElementById(this.targetID), 4, 5, + { deltaX: 20, lineOrPageDeltaX: 1, ctrlKey: true }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetWheelEvent (wheel, vertical)", + targetID: "input-text", eventType: "wheel", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + synthesizeWheel(document.getElementById(this.targetID), 3, 4, + { deltaY: 20, lineOrPageDeltaY: 1, altKey: true }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetWheelEvent (wheel, horizontal)", + targetID: "input-text", eventType: "wheel", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + synthesizeWheel(document.getElementById(this.targetID), 4, 5, + { deltaX: 20, lineOrPageDeltaX: 1, ctrlKey: true }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetWheelEvent (wheel, both)", + targetID: "input-text", eventType: "wheel", + dispatchEvent() { + document.getElementById(this.targetID).value = ""; + synthesizeWheel(document.getElementById(this.targetID), 4, 5, + { deltaX: 20, deltaY: 10, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetTouchEvent (touchstart)", + target() { return document; }, eventType: "touchstart", + dispatchEvent() { + synthesizeTouchAtPoint(1, 2, { id: 10, rx: 4, ry: 3, angle: 0, force: 1, shiftKey: true}); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetTouchEvent (touchend)", + target() { return document; }, eventType: "touchend", + dispatchEvent() { + synthesizeTouchAtPoint(4, 6, { id: 5, rx: 1, ry: 2, angle: 0.5, force: 0.8, ctrlKey: true}); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "InternalFormEvent (reset)", + targetID: "form", eventType: "reset", + dispatchEvent() { + document.getElementById("form").reset(); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "WidgetCommandEvent", + targetID: "input-text", eventType: "", + dispatchEvent() { + + }, + canRun() { + todo(false, "WidgetCommandEvent isn't tested"); + return false; + }, + todoMismatch: [], + }, + { description: "InternalClipboardEvent (copy)", + targetID: "input-text", eventType: "copy", + dispatchEvent() { + document.getElementById("input-text").value = "go to clipboard!"; + document.getElementById("input-text").focus(); + document.getElementById("input-text").select(); + synthesizeKey("c", { accelKey: true }); + }, + canRun() { + return true; + }, + todoMismatch: [ ], + }, + { description: "InternalUIEvent (DOMActivate)", + targetID: "button", eventType: "DOMActivate", + dispatchEvent() { + synthesizeMouseAtCenter(document.getElementById(this.targetID), + { button: 0, shiftKey: true }); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "InternalFocusEvent (focus)", + targetID: "input-text", eventType: "focus", + dispatchEvent() { + document.getElementById(this.targetID).focus(); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "InternalFocusEvent (blur)", + targetID: "input-text", eventType: "blur", + dispatchEvent() { + document.getElementById(this.targetID).blur(); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "WidgetSimpleGestureEvent", + targetID: "", eventType: "", + dispatchEvent() { + + }, + canRun() { + // Simple gesture event may be handled before it comes content. + // So, we cannot test it in this test. + todo(false, "WidgetSimpleGestureEvent isn't tested"); + return false; + }, + todoMismatch: [], + }, + { description: "InternalTransitionEvent (transitionend)", + targetID: "a", eventType: "transitionend", + dispatchEvent() { + document.getElementById(this.targetID).focus(); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "InternalAnimationEvent (animationend)", + targetID: "animated-div", eventType: "animationend", + dispatchEvent() { + document.getElementById(this.targetID).className = "slidin"; + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "InternalMutationEvent (DOMAttrModified)", + targetID: "animated-div", eventType: "DOMAttrModified", + dispatchEvent() { + document.getElementById(this.targetID).setAttribute("x-data", "foo"); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "InternalMutationEvent (DOMNodeInserted)", + targetID: "animated-div", eventType: "DOMNodeInserted", + dispatchEvent() { + var div = document.createElement("div"); + div.id = "inserted-div"; + document.getElementById("animated-div").appendChild(div); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "InternalMutationEvent (DOMNodeRemoved)", + targetID: "animated-div", eventType: "DOMNodeRemoved", + dispatchEvent() { + document.getElementById("animated-div").removeChild(document.getElementById("inserted-div")); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "PointerEvent (pointerdown)", + targetID: "pointer-target", eventType: "mousedown", + dispatchEvent() { + var elem = document.getElementById(this.targetID); + var rect = elem.getBoundingClientRect(); + synthesizeMouse(elem, rect.width / 2, rect.height / 2, + { type: this.eventType, button: 1, clickCount: 1, inputSource: 2, pressure: 0.25, isPrimary: true }); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, + { description: "PointerEvent (pointerup)", + targetID: "pointer-target", eventType: "mouseup", + dispatchEvent() { + var elem = document.getElementById(this.targetID); + var rect = elem.getBoundingClientRect(); + synthesizeMouse(elem, rect.width / 2, rect.height / 2, + { type: this.eventType, button: -1, ctrlKey: true, shiftKey: true, altKey: true, isSynthesized: false }); + }, + canRun() { + return true; + }, + todoMismatch: [], + }, +]; + +function doTest(aTest) { + if (!aTest.canRun()) { + SimpleTest.executeSoon(runNextTest); + return; + } + gEvent = null; + gCopiedEvent = []; + gDescription = aTest.description + " (gCallPreventDefault=" + gCallPreventDefault + ")"; + var target = aTest.target ? aTest.target() : document.getElementById(aTest.targetID); + target.addEventListener(aTest.eventType, onEvent, true); + gCallback = function() { + target.removeEventListener(aTest.eventType, onEvent, true); + ok(gEvent !== null, gDescription + ": failed to get duplicated event"); + ok(!!gCopiedEvent.length, gDescription + ": count of attribute of the event must be larger than 0"); + for (var i = 0; i < gCopiedEvent.length; ++i) { + var name = gCopiedEvent[i].name; + if (name == "rangeOffset") { + todo(false, gDescription + ": " + name + " attribute value is never reset (" + gEvent[name] + ")"); + } else if (name == "eventPhase") { + is(gEvent[name], 0, gDescription + ": mismatch with fixed value (" + name + ")"); + } else if (name == "rangeParent" || name == "currentTarget") { + is(gEvent[name], null, gDescription + ": mismatch with fixed value (" + name + ")"); + } else if (aTest.todoMismatch.includes(name)) { + todo_is(gEvent[name], gCopiedEvent[i].value, gDescription + ": mismatch (" + name + ")"); + } else if (name == "offsetX" || name == "offsetY") { + // do nothing; these are defined to return different values during event dispatch + // vs not during event dispatch + } else { + is(gEvent[name], gCopiedEvent[i].value, gDescription + ": mismatch (" + name + ")"); + } + } + if (!testWillCallRunNextTest) { + runNextTest(); + } + }; + var testWillCallRunNextTest = aTest.dispatchEvent(); +} + +var gIndex = -1; +function runNextTest() { + if (++gIndex == kTests.length) { + if (gCallPreventDefault) { + finish(); + return; + } + // Test with a call of preventDefault() of the events. + gCallPreventDefault = true; + gIndex = -1; + // Restoring the initial state of all elements. + document.getElementById("scrollable-div").style.height = "30px"; + document.getElementById("scrollable-div").style.width = "30px"; + document.getElementById("scrolled-div").style.height = "10px"; + document.getElementById("scrolled-div").style.width = "10px"; + document.getElementById("input-text").value = ""; + document.getElementById("animated-div").className = ""; + document.getElementById("animated-div").removeAttribute("x-data"); + if (document.activeElement) { + document.activeElement.blur(); + } + window.requestAnimationFrame(function() { + setTimeout(runNextTest, 0); + }); + return; + } + doTest(kTests[gIndex]); +} + +function init() { + SpecialPowers.pushPrefEnv({"set": [["middlemouse.contentLoadURL", false], + ["middlemouse.paste", false], + ["general.autoScroll", false], + ["mousewheel.default.action", 0], + ["mousewheel.default.action.override_x", -1], + ["mousewheel.with_shift.action", 0], + ["mousewheel.with_shift.action.override_x", -1], + ["mousewheel.with_control.action", 0], + ["mousewheel.with_control.action.override_x", -1], + ["mousewheel.with_alt.action", 0], + ["mousewheel.with_alt.action.override_x", -1], + ["mousewheel.with_meta.action", 0], + ["mousewheel.with_meta.action.override_x", -1]]}, runNextTest); +} + +function finish() { + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(init); + +</script> +</body> diff --git a/widget/tests/test_autocapitalize.html b/widget/tests/test_autocapitalize.html new file mode 100644 index 0000000000..de628d1190 --- /dev/null +++ b/widget/tests/test_autocapitalize.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html> +<head> +<title>Tests for autocapitalize that is used by software keyboard</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/SpecialPowers.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> + +<div> +<input type="text" id="a1"><br> +<input type="text" id="a2" autocapitalize="characters"><br> +<input type="text" id="a3" autocapitalize="sentences"><br> +<input type="text" id="a4" autocapitalize="words"><br> +<input type="text" id="a5" autocapitalize="off"><br> +<input type="text" id="a6" autocapitalize="on"><br> +<input type="url" id="a7" autocapitalize="on"><br> +<input type="email" id="a8" autocapitalize="on"><br> +<input type="password" id="a9" autocapitalize="on"><br> +<textarea id="b1" autocapitalize="characters"></textarea><br> +<div contenteditable id="c1" autocapitalize="sentences"></div><br> +<form><input type="text" id="d1" autocapitalize="words"></form><br> +<form autocapitalize="on"><input type="text" id="d2"></form><br> +<form autocapitalize="off"><input type="text" id="d3" autocapitalize="on"></form><br> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(async () => { + const tests = [ + { id: "a1", autocapitalize: "", desc: "input without autocapitalize" }, + { id: "a2", autocapitalize: "characters", desc: "input with autocapitalize=characters" }, + { id: "a3", autocapitalize: "sentences", desc: "input with autocapitalize=sentences" }, + { id: "a4", autocapitalize: "words", desc: "input with autocapitalize=words" }, + { id: "a5", autocapitalize: "none", desc: "input with autocapitalize=off" }, + { id: "a6", autocapitalize: "sentences", desc: "input with autocapitalize=on" }, + { id: "a7", autocapitalize: "", desc: "input with type=url and autocapitalize=on" }, + { id: "a8", autocapitalize: "", desc: "input with type=email and autocapitalize=on" }, + { id: "a9", autocapitalize: "", desc: "input with type=password and autocapitalize=on" }, + { id: "b1", autocapitalize: "characters", desc: "textarea with autocapitalize=characters" }, + { id: "c1", autocapitalize: "sentences", desc: "contenteditable with autocapitalize=sentences" }, + { id: "d1", autocapitalize: "words", desc: "input with autocapitalize=words in form" }, + { id: "d2", autocapitalize: "sentences", desc: "input in form with autocapitalize=on" }, + { id: "d3", autocapitalize: "sentences", desc: "input with autocapitalize=on in form" }, + ]; + + for (let test of tests) { + document.getElementById(test.id).focus(); + is(SpecialPowers.DOMWindowUtils.focusedAutocapitalize, test.autocapitalize, test.desc); + } + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/widget/tests/test_bug1123480.xhtml b/widget/tests/test_bug1123480.xhtml new file mode 100644 index 0000000000..18a40202fd --- /dev/null +++ b/widget/tests/test_bug1123480.xhtml @@ -0,0 +1,155 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1123480 +--> +<window title="Mozilla Bug 1123480" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="RunTest();"> + <title>nsTransferable PBM Overflow Selection Test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + // Create over 1 MB of sample garbage text. JavaScript strings are represented by + // UTF16 strings, so the size is twice as much as the actual string length. + // This value is chosen such that the size of the memory for the string exceeds + // the kLargeDatasetSize threshold in nsTransferable.h. + // It is also not a round number to reduce the odds of having an accidental + // collisions with another file (since the test below looks at the file size + // to identify the file). + var Ipsum = "0123456789".repeat(1234321); + var IpsumByteLength = Ipsum.length * 2; + var SHORT_STRING_NO_CACHE = "short string that will never be cached to the disk"; + + function isWindows() { + const {AppConstants} = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + return AppConstants.platform === 'win'; + } + + // Get a list of open file descriptors that refer to a file with the same size as + // the expected data (and assume that any mutations in file descriptor counts + // are caused by our test). + function getClipboardCacheFDCount() { + var dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + if (isWindows()) { + // On Windows, nsAnonymousTemporaryFile does not immediately delete the file. + // Instead, the Windows-specific FILE_FLAG_DELETE_ON_CLOSE flag is used, + // which means that the file is deleted when the last handle is closed. + // Apparently, this flag is unreliable (e.g. when the application crashes), + // so nsAnonymousTemporaryFile stores the temporary files in a subdirectory, + // which is cleaned up some time after start-up. + + // This is just a test, and during the test we deterministically close the + // handles, so if FILE_FLAG_DELETE_ON_CLOSE does the thing it promises, the + // file is actually removed when the handle is closed. + + // Path from nsAnonymousTemporaryFile.cpp, GetTempDir. + dir.initWithPath(PathUtils.join(PathUtils.tempDir, "mozilla-temp-files")); + } else { + dir.initWithPath("/dev/fd"); + } + var count = 0; + for (var de = dir.directoryEntries; de.hasMoreElements(); ) { + var fdFile = de.nextFile; + var fileSize; + try { + fileSize = fdFile.fileSize; + } catch (e) { + // This can happen on macOS. + continue; + } + if (fileSize === IpsumByteLength) { + // Assume that the file was created by us if the size matches. + ++count; + } + } + return count; + } + + async function RunTest() { + SimpleTest.waitForExplicitFinish(); + const gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); + + // Sanitize environment + gClipboardHelper.copyString(SHORT_STRING_NO_CACHE); + await new Promise(resolve => setTimeout(resolve, 0)); + + var initialFdCount = getClipboardCacheFDCount(); + + // Overflow a nsTransferable region by using the clipboard helper + gClipboardHelper.copyString(Ipsum); + + // gClipboardHelper.copyString also puts the data on the selection + // clipboard if the platform supports it. + var expectedFdDelta = Services.clipboard.isClipboardTypeSupported(Services.clipboard.kSelectionClipboard) ? 2 : 1; + // Undefined private browsing mode should cache to disk + is(getClipboardCacheFDCount(), initialFdCount + expectedFdDelta, "should cache to disk when PBM is undefined"); + + // Sanitize environment again. + gClipboardHelper.copyString(SHORT_STRING_NO_CACHE); + await new Promise(resolve => setTimeout(resolve, 0)); + + is(getClipboardCacheFDCount(), initialFdCount, "should have cleared the clipboard data"); + + // Repeat procedure of plain text selection with private browsing + // disabled and enabled + const {PrivateBrowsingUtils} = ChromeUtils.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" + ); + for (let private of [false, true]) { + var win = window.browsingContext.topChromeWindow.open("about:blank", "_blank", "chrome, width=500, height=200" + (private ? ", private" : "")); + ok(win, private ? "should open private window" : "should open non-private window"); + is(PrivateBrowsingUtils.isContentWindowPrivate(win), private, "used correct window context"); + + // Select plaintext in private/non-private channel + const nsTransferable = Components.Constructor("@mozilla.org/widget/transferable;1", "nsITransferable"); + const nsSupportsString = Components.Constructor("@mozilla.org/supports-string;1", "nsISupportsString"); + var Loadctx = PrivateBrowsingUtils.privacyContextFromWindow(win); + var Transfer = nsTransferable(); + var Suppstr = nsSupportsString(); + Suppstr.data = Ipsum; + Transfer.init(Loadctx); + Transfer.addDataFlavor("text/plain"); + Transfer.setTransferData("text/plain", Suppstr); + + // Enabled private browsing mode should not cache any selection to disk; disabled should + if (private) { + is(getClipboardCacheFDCount(), initialFdCount, "did not violate private browsing mode"); + } else { + is(getClipboardCacheFDCount(), initialFdCount + 1, "should save memory by caching non-private clipboard data to disk"); + } + + // Share the transferable with the system. + Services.clipboard.setData(Transfer, null, Services.clipboard.kGlobalClipboard); + if (private) { + is(getClipboardCacheFDCount(), initialFdCount, "did not violate private browsing mode"); + } else { + is(getClipboardCacheFDCount(), initialFdCount + 1, "should save memory by caching non-private clipboard data to disk"); + } + + // Sanitize the environment. + Suppstr = nsSupportsString(); + Suppstr.data = SHORT_STRING_NO_CACHE; + Transfer.setTransferData("text/plain", Suppstr); + await new Promise(resolve => setTimeout(resolve, 0)); + is(getClipboardCacheFDCount(), initialFdCount, "should drop the cache file, if any."); + + Services.clipboard.setData(Transfer, null, Services.clipboard.kGlobalClipboard); + is(getClipboardCacheFDCount(), initialFdCount, "should postsanitize the environment"); + } + + SimpleTest.finish(); + } + ]]> + </script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1123480" + target="_blank">Mozilla Bug 1123480</a> + </body> +</window> diff --git a/widget/tests/test_bug343416.xhtml b/widget/tests/test_bug343416.xhtml new file mode 100644 index 0000000000..bf4dbbb9b4 --- /dev/null +++ b/widget/tests/test_bug343416.xhtml @@ -0,0 +1,191 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=343416 +--> +<window title="Mozilla Bug 343416" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=343416">Mozilla Bug 343416</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 343416 */ +SimpleTest.waitForExplicitFinish(); + +// Observer: +var idleObserver = +{ + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe: function _observe(subject, topic, data) + { + if (topic != "idle") + return; + +// var diff = Math.abs(data - newIdleSeconds * 1000); +// ok (diff < 5000, "The idle time should have increased by roughly 6 seconds, " + +// "as that's when we told this listener to fire."); +// if (diff >= 5000) +// alert(data + " " + newIdleSeconds); + + // Attempt to get to the nsIUserIdleService + var subjectOK = false; + try { + var idleService = subject.QueryInterface(nsIUserIdleService); + subjectOK = true; + } + catch (ex) + {} + ok(subjectOK, "The subject of the notification should be the " + + "nsIUserIdleService."); + + // Attempt to remove ourselves. + var removedObserver = false; + try { + idleService.removeIdleObserver(this, newIdleSeconds); + removedObserver = true; + } + catch (ex) + {} + ok(removedObserver, "We should be able to remove our observer here."); + finishedListenerOK = true; + if (finishedTimeoutOK) + { + clearTimeout(testBailout); + finishThisTest(); + } + } +}; + + +const nsIUserIdleService = Ci.nsIUserIdleService; +const nsIISCID = "@mozilla.org/widget/useridleservice;1"; +var idleService = null; +try +{ + idleService = Cc[nsIISCID].getService(nsIUserIdleService); +} +catch (ex) +{} + +ok(idleService, "nsIUserIdleService should exist and be implemented on all tier 1 platforms."); + +var idleTime = null; +var gotIdleTime = false; +try +{ + idleTime = idleService.idleTime; + gotIdleTime = true; +} +catch (ex) +{} + +ok (gotIdleTime, "Getting the idle time should not fail " + + "in normal circumstances on any tier 1 platform."); + +// Now we set up a timeout to sanity-test the idleTime after 5 seconds +setTimeout(testIdleTime, 5000); +var startTimeStamp = Date.now(); + +// Now we add the listener: +var newIdleSeconds = Math.floor(idleTime / 1000) + 6; +var addedObserver = false; +try +{ + idleService.addIdleObserver(idleObserver, newIdleSeconds); + addedObserver = true; +} +catch (ex) +{} + +ok(addedObserver, "The nsIUserIdleService should allow us to add an observer."); + +addedObserver = false; +try +{ + idleService.addIdleObserver(idleObserver, newIdleSeconds); + addedObserver = true; +} +catch (ex) +{} + +ok(addedObserver, "The nsIUserIdleService should allow us to add the same observer again."); + +var removedObserver = false; +try +{ + idleService.removeIdleObserver(idleObserver, newIdleSeconds); + removedObserver = true; +} +catch (ex) +{} + +ok(removedObserver, "The nsIUserIdleService should allow us to remove the observer just once."); + +function testIdleTime() +{ + /* eslint-disable-next-line no-shadow */ + var gotIdleTime = false + try + { + var newIdleTime = idleService.idleTime; + gotIdleTime = true + } + catch (ex) + {} + ok(gotIdleTime, "Getting the idle time should not fail " + + "in normal circumstances on any tier 1 platform."); + // Get the time difference, remove the approx. 5 seconds that we've waited, + // should be very close to 0 left. + var timeDiff = Math.abs((newIdleTime - idleTime) - + (Date.now() - startTimeStamp)); + + // 1.5 second leniency. + ok(timeDiff < 1500, "The idle time should have increased by roughly the " + + "amount of time it took for the timeout to fire. " + + "You didn't touch the mouse or keyboard during the " + + "test did you?"); + finishedTimeoutOK = true; +} + +// make sure we still exit when the listener and/or setTimeout don't fire: +var testBailout = setTimeout(finishThisTest, 12000); +var finishedTimeoutOK = false, finishedListenerOK = false; +function finishThisTest() +{ + ok(finishedTimeoutOK, "We set a timeout and it should have fired by now."); + ok(finishedListenerOK, "We added a listener and it should have been called by now."); + if (!finishedListenerOK) + { + var removedListener = false; + try + { + idleService.removeIdleObserver(idleObserver, newIdleSeconds); + removedListener = true; + } + catch (ex) + {} + + ok(removedListener, "We added a listener and we should be able to remove it."); + } + // Done: + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug428405.xhtml b/widget/tests/test_bug428405.xhtml new file mode 100644 index 0000000000..25d796ab7b --- /dev/null +++ b/widget/tests/test_bug428405.xhtml @@ -0,0 +1,168 @@ +<?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 Bug 428405" + onload="setGlobals(); loadFirstTab();" + 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"/> + + <tabbox id="tabbox" style="-moz-box-flex: 100"> + <tabs> + <tab label="Tab 1"/> + <tab label="Tab 2"/> + </tabs> + <tabpanels style="-moz-box-flex: 100"> + <browser onload="configureFirstTab();" id="tab1browser" style="-moz-box-flex: 100"/> + <browser onload="configureSecondTab();" id="tab2browser" style="-moz-box-flex: 100"/> + </tabpanels> + </tabbox> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + + SimpleTest.waitForExplicitFinish(); + + var gCmdOptYReceived = false; + + // Look for a cmd-opt-y event. + function onKeyPress(aEvent) { + gCmdOptYReceived = false; + if (String.fromCharCode(aEvent.charCode) != 'y') + return; + if (aEvent.ctrlKey || aEvent.shiftKey || !aEvent.metaKey || !aEvent.altKey) + return; + gCmdOptYReceived = true; + } + + function setGlobals() { + let chromeWindow = Services.wm.getMostRecentWindow("navigator:browser"); + // For some reason, a global <key> element's oncommand handler only gets + // invoked if the focus is outside both of the <browser> elements + // (tab1browser and tab2browser). So, to make sure we can see a + // cmd-opt-y event in window1 (if one is available), regardless of where + // the focus is in this window, we need to add a "keypress" event + // listener to gChromeWindow, and then check (in onKeyPress()) to see if + // it's a cmd-opt-y event. + chromeWindow.addEventListener("keypress", onKeyPress); + } + + // 1) Start loading first tab. + // 6) Start reloading first tab. + function loadFirstTab() { + var browser = document.getElementById("tab1browser"); + BrowserTestUtils.startLoadingURIString(browser, "data:text/html;charset=utf-8,<body><h2>First Tab</h2><p><input type='submit' value='Button' id='button1'/></body>"); + } + + function configureFirstTab() { + try { + var button = document.getElementById("tab1browser").contentDocument.getElementById("button1"); + button.addEventListener("click", onFirstTabButtonClicked); + button.focus(); + if (document.getElementById("tabbox").selectedIndex == 0) { + // 2) When first tab has finished loading (while first tab is + // focused), hit Return to trigger the action of first tab's + // button. + synthesizeNativeReturnKey(); + } else { + // 7) When first tab has finished reloading (while second tab is + // focused), start loading second tab. + loadSecondTab(); + } + } catch(e) { + } + } + + // 8) Start loading second tab. + function loadSecondTab() { + var browser = document.getElementById("tab2browser"); + BrowserTestUtils.startLoadingURIString(browser, "data:text/html;charset=utf-8,<body><h2>Second Tab</h2><p><input type='submit' value='Button' id='button1'/></body>"); + } + + function configureSecondTab() { + try { + var button = document.getElementById("tab2browser").contentDocument.getElementById("button1"); + button.addEventListener("click", onSecondTabButtonClicked); + button.focus(); + if (document.getElementById("tabbox").selectedIndex == 1) { + // 9) When second tab has finished loading (while second tab is + // focused), hit Return to trigger action of second tab's + // button. + synthesizeNativeReturnKey(); + } + } catch(e) { + } + } + + // 3) First tab's button clicked. + function onFirstTabButtonClicked() { + switchToSecondTabAndReloadFirst(); + } + + // 10) Second tab's button clicked. + function onSecondTabButtonClicked() { + switchToFirstTab(); + } + + function switchToSecondTabAndReloadFirst() { + // 4) Switch to second tab. + document.getElementById("tabbox").selectedIndex = 1; + // 5) Start reloading first tab (while second tab is focused). + loadFirstTab(); + } + + function switchToFirstTab() { + // 11) Switch back to first tab. + document.getElementById("tabbox").selectedIndex = 0; + doCmdY(); + } + + function doCmdY() { + // 12) Back in first tab, try cmd-y. + gCmdOptYReceived = false; + if (!synthesizeNativeCmdOptY(finishTest)) { + ok(false, "Failed to synthesize native key"); + finishTest(); + } + } + + function finishTest() { + // 13) Check result. + is(gCmdOptYReceived, true); + + SimpleTest.finish(); + } + + // synthesizeNativeReturnKey() and synthesizeNativeCmdOptY() are needed + // because their synthesizeKey() counterparts don't work properly -- the + // latter make this test succeed when it should fail. + + // The 'aNativeKeyCode', 'aCharacters' and 'aUnmodifiedCharacters' + // parameters used below (in synthesizeNativeReturnKey() and + // synthesizeNativeCmdOptY()) were confirmed accurate using the + // DebugEventsPlugin v1.01 from bmo bug 441880. + + function synthesizeNativeReturnKey() { + synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_Return, {}, "\u000a", "\u000a"); + } + + function synthesizeNativeCmdOptY(aCallback) { + return synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_Y, {metaKey:1, altKey:1}, "y", "y", aCallback); + } + + ]]></script> + + <!-- 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> + +</window> diff --git a/widget/tests/test_bug429954.xhtml b/widget/tests/test_bug429954.xhtml new file mode 100644 index 0000000000..40de88cd32 --- /dev/null +++ b/widget/tests/test_bug429954.xhtml @@ -0,0 +1,42 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=429954 +--> +<window title="Mozilla Bug 429954" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function () { + var win = Services.wm.getMostRecentWindow("navigator:browser"); + win.maximize(); + var maxX = win.screenX, maxY = win.screenY; + var maxWidth = win.outerWidth, maxHeight = win.outerHeight; + win.restore(); + + window.openDialog("window_bug429954.xhtml", "_blank", + "chrome,noopener,resizable,width=" + maxWidth + ",height=" + maxHeight + + ",screenX=" + maxX + "screenY=" + maxY, + window); +}); + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug444800.xhtml b/widget/tests/test_bug444800.xhtml new file mode 100644 index 0000000000..f6d3535d72 --- /dev/null +++ b/widget/tests/test_bug444800.xhtml @@ -0,0 +1,97 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=444800 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 444800" onload="initAndRunTests()"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=444800" + target="_blank">Mozilla Bug 444800</a> + <p/> + <img id="bitmapImage" src="data:image/bmp;base64,Qk2KAwAAAAAAAIoAAAB8AAAADwAAABAAAAABABgAAAAAAAADAAASCwAAEgsAAAAAAAAAAAAAAAD%2FAAD%2FAAD%2FAAAAAAAA%2FwEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F0vf%2FAABc8tKY%2F%2F%2F%2FyNfq3Mi9%2F%2F%2F70vf%2FAABP8s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABB8s2R5f%2F%2FAAB5LgAA%2F%2B7Czff%2FAABB7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABB99KRpdz%2FAAAAAAAA4Ktm0vv%2FAABB7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABB7teYQZHNkS4AebfImAAA1%2FfyAABP7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABByMiYAAB5159P0v%2F%2FAABBwtKrAABc7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABPcIJwAAAA%2B%2BW3%2F%2F%2F%2FAHC3gnBBAABP7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2Fzff%2FAABcAAAAmE8A%2F%2F%2Fy%2F%2F%2F%2Fn9LyAAAAAAAA7s2Y%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FzfL%2FAABcAAAA4LFw%2F%2F%2F%2F%2F%2F%2F%2F4P%2F%2FAAB5AAAA7s2R%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F0vf%2FAABmXAAA%2F%2B7I%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FXJ%2FSAAAA8s2Y%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FAAAA"/> + <p/> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ +const knsIClipboard = Ci.nsIClipboard; + +function copyImageToClipboard() +{ + SpecialPowers.setCommandNode(window, document.getElementById("bitmapImage")); + + const kCmd = "cmd_copyImageContents"; + var controller = top.document.commandDispatcher + .getControllerForCommand(kCmd); + ok((controller && controller.isCommandEnabled(kCmd)), "have copy command"); + controller.doCommand(kCmd); + + SpecialPowers.setCommandNode(window, null); +} + +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +function runImageClipboardTests(aCBSvc, aImageType) +{ + // Verify that hasDataMatchingFlavors() is working correctly. + var typeArray = [ aImageType ]; + var hasImage = aCBSvc.hasDataMatchingFlavors(typeArray, + knsIClipboard.kGlobalClipboard); + ok(hasImage, aImageType + " - hasDataMatchingFlavors()"); + + // Verify that getData() is working correctly. + var xfer = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + xfer.init(getLoadContext()); + xfer.addDataFlavor(aImageType); + aCBSvc.getData(xfer, knsIClipboard.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext); + + var typeObj = {}, dataObj = {}; + xfer.getAnyTransferData(typeObj, dataObj); + var gotValue = (null != dataObj.value); + ok(gotValue, aImageType + " - getData() returned a value"); + if (gotValue) + { + const knsIInputStream = Ci.nsIInputStream; + var imgStream = dataObj.value.QueryInterface(knsIInputStream); + ok((null != imgStream), aImageType + " - got an nsIInputStream"); + var bytesAvailable = imgStream.available(); + ok((bytesAvailable > 10), aImageType + " - got some data"); + } +} + +function initAndRunTests() +{ + SimpleTest.waitForExplicitFinish(); + + copyImageToClipboard(); + + var cbSvc = Cc["@mozilla.org/widget/clipboard;1"] + .getService(knsIClipboard); + + // Work around a problem on Windows where clipboard is not ready after copy. + setTimeout(function() { runTests(cbSvc); }, 0); +} + +function runTests(aCBSvc) +{ + runImageClipboardTests(aCBSvc, "image/png"); + runImageClipboardTests(aCBSvc, "image/jpg"); + runImageClipboardTests(aCBSvc, "image/jpeg"); + + SimpleTest.finish(); +} + +]]> +</script> +</window> diff --git a/widget/tests/test_bug466599.xhtml b/widget/tests/test_bug466599.xhtml new file mode 100644 index 0000000000..95b3593437 --- /dev/null +++ b/widget/tests/test_bug466599.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=466599 +--> +<window title="Mozilla Bug 466599" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="initAndRunTests()"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.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> + + <!-- test code goes here --> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 466599 */ + +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +function copyToClipboard(txt) +{ + var clipid = Ci.nsIClipboard; + var clip = + Cc['@mozilla.org/widget/clipboard;1'].createInstance(clipid); + if (!clip) + return false; + var trans = + Cc['@mozilla.org/widget/transferable;1'].createInstance(Ci.nsITransferable); + if (!trans) + return false; + trans.init(getLoadContext()); + trans.addDataFlavor('text/html'); + var str = + Cc['@mozilla.org/supports-string;1'].createInstance(Ci.nsISupportsString); + var copytext = txt; + str.data = copytext; + trans.setTransferData("text/html",str); + if (!clip) + return false; + clip.setData(trans,null,clipid.kGlobalClipboard); + return true; +} + +function readFromClipboard() +{ + var clipid = Ci.nsIClipboard; + var clip = + Cc['@mozilla.org/widget/clipboard;1'].createInstance(clipid); + if (!clip) + return ""; + var trans = + Cc['@mozilla.org/widget/transferable;1'].createInstance(Ci.nsITransferable); + if (!trans) + return ""; + trans.init(getLoadContext()); + trans.addDataFlavor('text/html'); + clip.getData(trans, clipid.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext); + var str = {}; + trans.getTransferData("text/html",str); + if (str) + str = str.value.QueryInterface(Ci.nsISupportsString); + return str?.data; +} + +function encodeHtmlEntities(s) +{ + var result = ''; + for (var i = 0; i < s.length; i++) { + var c = s.charAt(i); + result += {'<':'<', '>':'>', '&':'&', '"':'"'}[c] || c; + } + return result; +} + +function initAndRunTests() +{ + var source = '<p>Lorem ipsum</p>'; + var expect = new RegExp('<html>.*charset=utf-8.*' + source + '.*</html>', 'im'); + + var result = copyToClipboard(source); + ok(result, "copied HTML data to system pasteboard"); + + result = readFromClipboard(); + ok(expect.test(result), "data on system pasteboard is wrapped with charset metadata"); + + $("display").innerHTML = + '<em>source:</em> <pre>' + encodeHtmlEntities(source) + '</pre><br/>' + + '<em>result:</em> <pre>' + encodeHtmlEntities(result) + '</pre>'; +} + + ]]> + </script> +</window> diff --git a/widget/tests/test_bug478536.xhtml b/widget/tests/test_bug478536.xhtml new file mode 100644 index 0000000000..383c0bb42f --- /dev/null +++ b/widget/tests/test_bug478536.xhtml @@ -0,0 +1,33 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=478536 +--> +<window title="Mozilla Bug 478536" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <title>Test for Bug 478536</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("window_bug478536.xhtml", "_blank", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug485118.xhtml b/widget/tests/test_bug485118.xhtml new file mode 100644 index 0000000000..5c635f2982 --- /dev/null +++ b/widget/tests/test_bug485118.xhtml @@ -0,0 +1,72 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=485118 +--> +<window title="Mozilla Bug 485118" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<hbox height="300"> + <vbox width="300"> + <scrollbar orient="horizontal" + maxpos="10000" + pageincrement="1" + id="horizontal"/> + <scrollbar orient="horizontal" + maxpos="10000" + pageincrement="1" + style="appearance: auto; -moz-default-appearance: scrollbar-small;" + id="horizontalSmall"/> + <hbox flex="1"> + <scrollbar orient="vertical" + maxpos="10000" + pageincrement="1" + id="vertical"/> + <scrollbar orient="vertical" + maxpos="10000" + pageincrement="1" + style="appearance: auto; -moz-default-appearance: scrollbar-small;" + id="verticalSmall"/> + <spacer flex="1"/> + </hbox> + </vbox> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTest() { + ["horizontal", "vertical"].forEach(function (orient) { + ["", "Small"].forEach(function (size) { + var elem = document.getElementById(orient + size); + var thumbRect = SpecialPowers.unwrap( + SpecialPowers.InspectorUtils.getChildrenForNode(elem, true, false)[0]) + .childNodes[0].getBoundingClientRect(); + var sizeToCheck = orient == "horizontal" ? "width" : "height"; + // var expectedSize = size == "Small" ? 19 : 26; + var expectedSize = 26; + is(thumbRect[sizeToCheck], expectedSize, size + " scrollbar has wrong minimum " + sizeToCheck); + }); + }); + SimpleTest.finish(); +} +window.addEventListener("load", runTest); + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug517396.xhtml b/widget/tests/test_bug517396.xhtml new file mode 100644 index 0000000000..c88baf49ab --- /dev/null +++ b/widget/tests/test_bug517396.xhtml @@ -0,0 +1,53 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=517396 +--> +<window title="Mozilla Bug 517396" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function () { + // this test fails on Linux, bug 526236 + if (navigator.platform.includes("Lin")) { + ok(true, "disabled on Linux"); + SimpleTest.finish(); + return; + } + + var win = Services.wm.getMostRecentWindow("navigator:browser"); + var oldWidth = win.outerWidth, oldHeight = win.outerHeight; + win.maximize(); + var newWidth = win.outerWidth, newHeight = win.outerHeight; + win.moveBy(10, 0); + var sizeShouldHaveChanged = !navigator.platform.match(/Mac/); + var compFunc = sizeShouldHaveChanged ? isnot : is; + var not = sizeShouldHaveChanged ? "" : "not "; + compFunc(win.outerWidth, newWidth, "moving a maximized window should " + not + "have changed its width"); + compFunc(win.outerHeight, newHeight, "moving a maximized window should " + not + "have changed its height"); + win.restore(); + is(win.outerWidth, oldWidth, "restored window has wrong width"); + is(win.outerHeight, oldHeight, "restored window has wrong height"); + SimpleTest.finish(); +}); + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug522217.xhtml b/widget/tests/test_bug522217.xhtml new file mode 100644 index 0000000000..0fa55a65e8 --- /dev/null +++ b/widget/tests/test_bug522217.xhtml @@ -0,0 +1,35 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=522217 +--> +<window title="Mozilla Bug 522217" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function () { + window.openDialog("window_bug522217.xhtml", "_blank", + "chrome,resizable,width=400,height=300,noopener", window); +}); + + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug538242.xhtml b/widget/tests/test_bug538242.xhtml new file mode 100644 index 0000000000..4608a74e35 --- /dev/null +++ b/widget/tests/test_bug538242.xhtml @@ -0,0 +1,55 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=538242 +--> +<window title="Mozilla Bug 538242" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +if (navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 1); +} + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function () { + if (navigator.platform.includes("Lin")) { + ok(true, "This test is disabled on Linux because it expects moving windows to be synchronous which is not guaranteed on Linux."); + SimpleTest.finish(); + return; + } + + var win = window.browsingContext.topChromeWindow.open( + "window_bug538242.xhtml", "_blank", + "chrome,width=400,height=300,left=100,top=100"); + SimpleTest.waitForFocus(function () { + is(win.screenX, 100, "window should open at 100, 100"); + is(win.screenY, 100, "window should open at 100, 100"); + var [oldX, oldY] = [win.screenX, win.screenY]; + win.moveTo(0, 0); + isnot(win.screenX, oldX, "window should have moved to a point near 0, 0"); + isnot(win.screenY, oldY, "window should have moved to a point near 0, 0"); + win.close(); + SimpleTest.finish(); + }, win); +}); + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug565392.html b/widget/tests/test_bug565392.html new file mode 100644 index 0000000000..b50053f23a --- /dev/null +++ b/widget/tests/test_bug565392.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=565392 +--> +<head> + <title>Test for Bug 565392</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=565392">Mozilla Bug 565392</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 565392 */ + +var dir1 = Services.dirsvc.get("ProfD", Ci.nsIFile); +var clipboard = Services.clipboard; + + function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); + } + + function getTransferableFile(file) { + var transferable = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + transferable.init(getLoadContext()); + transferable.setTransferData("application/x-moz-file", file); + return transferable; + } + + function setClipboardData(transferable) { + clipboard.setData(transferable, null, 1); + } + + function getClipboardData(mime) { + var transferable = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + transferable.init(getLoadContext()); + transferable.addDataFlavor(mime); + clipboard.getData(transferable, 1, SpecialPowers.wrap(window).browsingContext.currentWindowContext); + var data = {}; + transferable.getTransferData(mime, data); + return data; + } + +setClipboardData(getTransferableFile(dir1)); +is(clipboard.hasDataMatchingFlavors(["application/x-moz-file"], 1), true); +var data = getClipboardData("application/x-moz-file"); +var file = data.value.QueryInterface(Ci.nsIFile); +ok(file.isDirectory(), true); +is(file.target, dir1.target, true); + +</script> +</pre> +</body> +</html> diff --git a/widget/tests/test_bug586713.xhtml b/widget/tests/test_bug586713.xhtml new file mode 100644 index 0000000000..4733202264 --- /dev/null +++ b/widget/tests/test_bug586713.xhtml @@ -0,0 +1,29 @@ +<?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 title="Native menu system tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<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 class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug586713_window.xhtml", "bug586713_window", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug593307.xhtml b/widget/tests/test_bug593307.xhtml new file mode 100644 index 0000000000..770dd390cb --- /dev/null +++ b/widget/tests/test_bug593307.xhtml @@ -0,0 +1,40 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=593307 +--> +<window title="Mozilla Bug 593307" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function finish() { + offscreenWindow.close(); + SimpleTest.finish(); +} + +var mainWindow = window.browsingContext.topChromeWindow; + +var offscreenWindow = mainWindow.openDialog("window_bug593307_offscreen.xhtml", "", + "dialog=no,chrome,width=200,height=200,screenX=-3000,screenY=-3000", + SimpleTest, finish); + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug596600.xhtml b/widget/tests/test_bug596600.xhtml new file mode 100644 index 0000000000..4acdab79bc --- /dev/null +++ b/widget/tests/test_bug596600.xhtml @@ -0,0 +1,190 @@ +<?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 title="Native mouse event tests" + 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" /> + +<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 class="testbody" type="application/javascript"> +<![CDATA[ + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +var gLeftWindow, gRightWindow, gBrowserElement; + +function openWindows() { + gLeftWindow = window.browsingContext.topChromeWindow + .open('empty_window.xhtml', '_blank', 'chrome,screenX=50,screenY=50,width=200,height=200'); + SimpleTest.waitForFocus(function () { + gRightWindow = window.browsingContext.topChromeWindow + .open('empty_window.xhtml', '', 'chrome,screenX=300,screenY=50,width=200,height=200'); + SimpleTest.waitForFocus(attachBrowserToLeftWindow, gRightWindow); + }, gLeftWindow); +} + +function attachBrowserToLeftWindow() { + gBrowserElement = gLeftWindow.document.createXULElement("browser"); + gBrowserElement.setAttribute("type", "content"); + gBrowserElement.setAttribute("src", "file_bug596600.html"); + gBrowserElement.style.width = "100px"; + gBrowserElement.style.height = "100px"; + gBrowserElement.style.margin = "50px"; + gLeftWindow.document.documentElement.appendChild(gBrowserElement); + gBrowserElement.addEventListener("load", async () => { + await test1(); + await test2(); + gRightWindow.close(); + gLeftWindow.close(); + SimpleTest.finish(); + }, { capture: true, once: true }); +} + +async function test1() { + // gRightWindow is active, gLeftWindow is inactive. + info(`Synthesizing native "mousemove" event at top-left of the screen...`); + await promiseNativeMouseEvent({ + type: "mousemove", + screenX: 0, + screenY: 0, + scale: "inScreenPixels", + }); + await new Promise(resolve => SimpleTest.executeSoon(resolve)); + + // Move into the left window + info(`Synthesizing native "mousemove" event in the left window (but outside the content)...`); + await promiseNativeMouseEventAndWaitForEvent({ + type: "mousemove", + target: gBrowserElement, + offsetX: -20, + offsetY: -20, + win: gLeftWindow, + eventTypeToWait: "mouseover", + eventTargetToListen: gLeftWindow, + }); + ok(true, `"mouseover" event is fired on the left window when cursor is moved into it`); + + // Move over the browser + info(`Synthesizing native "mousemove" event on the content in the left window...`); + await promiseNativeMouseEventAndWaitForEvent({ + type: "mousemove", + target: gBrowserElement, + atCenter: true, + win: gLeftWindow, + eventTypeToWait: "mouseout", + eventTargetToListen: gLeftWindow, + }); + ok(true, `"mouseout" event is fired on the left window when cursor is moved into its child browser`); +} + +async function test2() { + // Make the browser cover the whole window. + gBrowserElement.style.margin = "0"; + gBrowserElement.style.width = gBrowserElement.style.height = "200px"; + + // Add a box to the browser at the left edge. + var doc = gBrowserElement.contentDocument; + var box = doc.createElement("div"); + box.setAttribute("id", "box"); + box.style.position = "absolute"; + box.style.left = "0"; + box.style.top = "50px"; + box.style.width = "100px"; + box.style.height = "100px"; + box.style.backgroundColor = "green"; + doc.body.appendChild(box); + + ok(!box.matches(":hover"), "Box shouldn't be hovered (since the mouse isn't over it and since it's in a non-clickthrough browser in a background window)"); + + // A function to waitForFocus and then wait for synthetic mouse + // events to happen. Note that those happen off the refresh driver, + // and happen after animation frame requests. + function changeFocusAndAwaitSyntheticMouse(winToFocus, + elementToWatchForMouseEventOn) { + return Promise.all([ + new Promise(resolve => { + function mouseWatcher() { + elementToWatchForMouseEventOn.removeEventListener("mouseover", + mouseWatcher); + elementToWatchForMouseEventOn.removeEventListener("mouseout", + mouseWatcher); + SimpleTest.executeSoon(resolve); + } + elementToWatchForMouseEventOn.addEventListener("mouseover", + mouseWatcher); + elementToWatchForMouseEventOn.addEventListener("mouseout", + mouseWatcher); + }), + new Promise(resolve => SimpleTest.waitForFocus(resolve, winToFocus)), + ]); + } + + // Move the mouse over the box. + info(`Synthesizing native "mousemove" event into the box...`); + await promiseNativeMouseEvent({ + type: "mousemove", + target: box, + atCenter: true, + win: gLeftWindow, + }); + await new Promise(resolve => + requestAnimationFrame(() => SimpleTest.executeSoon(resolve)) + ); + // XXX We cannot guarantee that the native mousemouse have already handled here. + ok(!box.matches(":hover"), "Box shouldn't be hovered (since it's in a non-clickthrough browser in a background window)"); + + // Activate the left window. + info("Waiting the left window activated..."); + await changeFocusAndAwaitSyntheticMouse(gLeftWindow, box); + ok(gBrowserElement.matches(":hover"), "browser should be hovered"); + ok(box.matches(":hover"), "Box should be hovered"); + + // De-activate the window (by activating the right window). + info("Waiting the right window activated..."); + await changeFocusAndAwaitSyntheticMouse(gRightWindow, box); + ok(!gBrowserElement.matches(":hover"), "browser shouldn't be hovered"); + ok(!box.matches(":hover"), "Box shouldn't be hovered"); + + // Re-activate it. + info("Waiting the left window activated again..."); + await changeFocusAndAwaitSyntheticMouse(gLeftWindow, box); + ok(gBrowserElement.matches(":hover"), "browser should be hovered"); + ok(box.matches(":hover"), "Box should be hovered"); + + // Unhover the box and the left window. + info(`Synthesizing native "mousemove" event outside the box and the left window...`); + await promiseNativeMouseEventAndWaitForEvent({ + type: "mousemove", + screenX: 0, + screenY: 0, + scale: "inScreenPixels", + win: gLeftWindow, + eventTargetToListen: box, + eventTypeToWait: "mouseout", + }); + await new Promise(resolve => + requestAnimationFrame(() => SimpleTest.executeSoon(resolve)) + ); + + ok(!gBrowserElement.matches(":hover"), "browser shouldn't be hovered"); + ok(!box.matches(":hover"), "box shouldn't be hovered"); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(openWindows); + +]]> +</script> + +</window> diff --git a/widget/tests/test_bug673301.xhtml b/widget/tests/test_bug673301.xhtml new file mode 100644 index 0000000000..663f18397e --- /dev/null +++ b/widget/tests/test_bug673301.xhtml @@ -0,0 +1,33 @@ +<?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 xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"/> +</body> + +<script type="application/javascript"> +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +var transferable = Cc['@mozilla.org/widget/transferable;1'] + .createInstance(Ci.nsITransferable); +transferable.init(getLoadContext()); + +transferable.addDataFlavor("text/plain"); +transferable.setTransferData("text/plain", document); + +Services.clipboard.setData(transferable, null, Ci.nsIClipboard.kGlobalClipboard); + +transferable.setTransferData("text/plain", null); + +SimpleTest.ok(true, "Didn't crash setting non-text data for text/plain type"); +</script> +</window> diff --git a/widget/tests/test_bug760802.xhtml b/widget/tests/test_bug760802.xhtml new file mode 100644 index 0000000000..831b4dea93 --- /dev/null +++ b/widget/tests/test_bug760802.xhtml @@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=760802 +--> +<window title="Mozilla Bug 760802" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=760802" + target="_blank">Mozilla Bug 760802</a> + <p id="display"></p> + <div id="content" style="display: none"/> + <iframe id="iframe_not_editable" width="300" height="150" + src="data:text/html,<html><body></body></html>"/><br/> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ +SimpleTest.waitForExplicitFinish(); + +function getBaseWindowInterface(win) { + return win.docShell + .treeOwner + .nsIBaseWindow; +} + +function getBaseWindowInterfaceFromDocShell(win) { + return win.docShell.QueryInterface(Ci.nsIBaseWindow); +} + +function shouldThrowException(fun, exception) { + try { + fun.call(); + return false; + } catch (e) { + $("display").innerHTML += "<br/>OK thrown: "+e.message; + return (e instanceof Components.Exception && + e.result === exception) + } +} +function doesntThrowException(fun) { + return !shouldThrowException(fun); +} + +var baseWindow = getBaseWindowInterface(this); +var nativeHandle = baseWindow.nativeHandle; +$("display").innerHTML = "found nativeHandle for this window: "+nativeHandle; + +var win = Services.wm.getMostRecentWindow("navigator:browser"); +let docShell = getBaseWindowInterfaceFromDocShell(win); + +ok( + shouldThrowException(function(){docShell.nativeHandle;}, + Cr.NS_ERROR_NOT_IMPLEMENTED), + "nativeHandle should not be implemented for nsDocShell" +); + +ok(typeof(nativeHandle) === "string", "nativeHandle should be a string"); +ok(nativeHandle.match(/^0x[0-9a-f]+$/), "nativeHandle should have a memory address format"); + +var iWin = document.getElementById("iframe_not_editable").contentWindow; +is(getBaseWindowInterface(iWin).nativeHandle, nativeHandle, + "the nativeHandle of an iframe should be its parent's nativeHandle"); + +var dialog = win.openDialog("data:text/plain,this is an active window.", "_blank", + "chrome,dialog=yes,width=100,height=100"); + +isnot(getBaseWindowInterface(dialog).nativeHandle, "", + "the nativeHandle of a dialog should not be empty"); + +dialog.close(); + +todo(false, "the nativeHandle of a window without a mainWidget should be empty"); // how to build a window without a mainWidget ? + +SimpleTest.finish(); + ]]></script> +</window> diff --git a/widget/tests/test_clipboard.html b/widget/tests/test_clipboard.html new file mode 100644 index 0000000000..35ad84ad73 --- /dev/null +++ b/widget/tests/test_clipboard.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=948065 +--> +<head> +<title>Test for Bug 948065</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="clipboard_helper.js"></script> +</head> +<body> +<!-- Tests are in file_test_clipboard.js --> +<script src="file_test_clipboard.js"></script> +</body> +</html> diff --git a/widget/tests/test_clipboard_asyncGetData.html b/widget/tests/test_clipboard_asyncGetData.html new file mode 100644 index 0000000000..0cb3dc2aa1 --- /dev/null +++ b/widget/tests/test_clipboard_asyncGetData.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1852947 +--> +<head> +<title>Test for Bug 1852947</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="clipboard_helper.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<!-- Tests are in file_clipboard_asyncGetData.js --> +<script src="file_test_clipboard_asyncGetData.js"></script> +</body> +</html> diff --git a/widget/tests/test_clipboard_asyncGetData_chrome.html b/widget/tests/test_clipboard_asyncGetData_chrome.html new file mode 100644 index 0000000000..d4e44185a5 --- /dev/null +++ b/widget/tests/test_clipboard_asyncGetData_chrome.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1852947 +--> +<head> +<title>Test for Bug 1852947</title> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="clipboard_helper.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<!-- Tests are in file_clipboard_asyncGetData.js --> +<script src="file_test_clipboard_asyncGetData.js"></script> +</body> +</html> diff --git a/widget/tests/test_clipboard_asyncSetData.html b/widget/tests/test_clipboard_asyncSetData.html new file mode 100644 index 0000000000..05e2ca714b --- /dev/null +++ b/widget/tests/test_clipboard_asyncSetData.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1712122 +--> +<head> +<title>Test for Bug 1712122</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="clipboard_helper.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<!-- Tests are in file_test_clipboard_asyncSetData.js --> +<script src="file_test_clipboard_asyncSetData.js"></script> +</body> +</html> diff --git a/widget/tests/test_clipboard_asyncSetData_chrome.html b/widget/tests/test_clipboard_asyncSetData_chrome.html new file mode 100644 index 0000000000..61afb9067b --- /dev/null +++ b/widget/tests/test_clipboard_asyncSetData_chrome.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1712122 +--> +<head> +<title>Test for Bug 1712122</title> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="clipboard_helper.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<!-- Tests are in file_test_clipboard_asyncSetData.js --> +<script src="file_test_clipboard_asyncSetData.js"></script> +</body> +</html> diff --git a/widget/tests/test_clipboard_cache_chrome.html b/widget/tests/test_clipboard_cache_chrome.html new file mode 100644 index 0000000000..55b6d41589 --- /dev/null +++ b/widget/tests/test_clipboard_cache_chrome.html @@ -0,0 +1,231 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1812543 +--> +<head> +<title>Test for Bug 1812543</title> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="clipboard_helper.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="application/javascript"> + +function testClipboardCache(aClipboardType, aAsync, aIsSupportGetFromCachedTransferable) { + add_task(function test_clipboard_get() { + info(`test_clipboard_get ${aAsync ? "async " : ""}` + + `with pref ${aIsSupportGetFromCachedTransferable ? "enabled" : "disabled"}`); + + const string = generateRandomString(); + const trans = generateNewTransferable("text/plain", string); + + info(`Write text/plain data to clipboard ${aClipboardType}`); + if (aAsync) { + let request = clipboard.asyncSetData(aClipboardType); + request.setData(trans, null); + } else { + clipboard.setData(trans, null, aClipboardType); + } + is(getClipboardData("text/plain", aClipboardType), string, + `Check text/plain data on clipboard ${aClipboardType}`); + + info(`Add text/foo data to transferable`); + addStringToTransferable("text/foo", string, trans); + // XXX macOS caches the transferable to implement kSelectionCache type, too, + // so it behaves differently than other types. + if (aClipboardType == clipboard.kSelectionCache && !aIsSupportGetFromCachedTransferable) { + todo_is(getClipboardData("text/foo", aClipboardType), + aIsSupportGetFromCachedTransferable ? string : null, + `Check text/foo data on clipboard ${aClipboardType}`); + } else { + is(getClipboardData("text/foo", aClipboardType), + aIsSupportGetFromCachedTransferable ? string : null, + `Check text/foo data on clipboard ${aClipboardType}`); + } + + info(`Should not get the data from other clipboard type`); + clipboardTypes.forEach(function(otherType) { + if (otherType != aClipboardType && + clipboard.isClipboardTypeSupported(otherType)) { + is(getClipboardData("text/plain", otherType), null, + `Check text/plain data on clipboard ${otherType}`); + is(getClipboardData("text/foo", otherType), null, + `Check text/foo data on clipboard ${otherType}`); + + info(`Write text/plain data to clipboard ${otherType}`); + writeRandomStringToClipboard("text/plain", otherType); + } + }); + + info(`Check data on clipboard ${aClipboardType} again`); + is(getClipboardData("text/plain", aClipboardType), string, + `Check text/plain data on clipboard ${aClipboardType} again`); + // XXX macOS caches the transferable to implement kSelectionCache type, too, + // so it behaves differently than other types. + if (aClipboardType == clipboard.kSelectionCache && !aIsSupportGetFromCachedTransferable) { + todo_is(getClipboardData("text/foo", aClipboardType), + aIsSupportGetFromCachedTransferable ? string : null, + `Check text/foo data on clipboard ${aClipboardType} again`); + } else { + is(getClipboardData("text/foo", aClipboardType), + aIsSupportGetFromCachedTransferable ? string : null, + `Check text/foo data on clipboard ${aClipboardType} again`); + } + + info(`Clean all clipboard data`); + cleanupAllClipboard(); + }); +} + +function runClipboardCacheTests(aIsSupportGetFromCachedTransferable) { + add_task(async function setup() { + cleanupAllClipboard(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "widget.clipboard.use-cached-data.enabled", + aIsSupportGetFromCachedTransferable, + ], + ], + }); + }); + + clipboardTypes.forEach(function (type) { + if (!clipboard.isClipboardTypeSupported(type)) { + return; + } + + add_task(function test_clipboard_hasDataMatchingFlavors() { + info(`test_clipboard_hasDataMatchingFlavors with pref ` + + `${aIsSupportGetFromCachedTransferable ? "enabled" : "disabled"}`); + + const trans = generateNewTransferable("text/plain", generateRandomString()); + + info(`Write text/plain data to clipboard ${type}`); + clipboard.setData(trans, null, type); + ok(clipboard.hasDataMatchingFlavors(["text/plain"], type), + `Check if there is text/plain flavor on clipboard ${type}`); + ok(!clipboard.hasDataMatchingFlavors(["text/foo"], type), + `Check if there is text/foo flavor on clipboard ${type}`); + + info(`Add text/foo data to transferable`); + addStringToTransferable("text/foo", generateRandomString(), trans); + ok(clipboard.hasDataMatchingFlavors(["text/plain"], type), + `Check if there is text/plain flavor on clipboard ${type}`); + // XXX macOS caches the transferable to implement kSelectionCache type, too, + // so it behaves differently than other types. + if (type == clipboard.kSelectionCache && !aIsSupportGetFromCachedTransferable) { + todo_is(clipboard.hasDataMatchingFlavors(["text/foo"], type), + aIsSupportGetFromCachedTransferable, + `Check if there is text/foo flavor on clipboard ${type}`); + } else { + is(clipboard.hasDataMatchingFlavors(["text/foo"], type), + aIsSupportGetFromCachedTransferable, + `Check if there is text/foo flavor on clipboard ${type}`); + } + + // Check other clipboard types. + clipboardTypes.forEach(function(otherType) { + if (otherType != type && + clipboard.isClipboardTypeSupported(otherType)) { + ok(!clipboard.hasDataMatchingFlavors(["text/plain"], otherType), + `Check if there is text/plain flavor on clipboard ${otherType}`); + ok(!clipboard.hasDataMatchingFlavors(["text/foo"], otherType), + `Check if there is text/foo flavor on clipboard ${otherType}`); + + info(`Write text/plain data to clipboard ${otherType}`); + writeRandomStringToClipboard("text/plain", otherType); + } + }); + + // Check again. + ok(clipboard.hasDataMatchingFlavors(["text/plain"], type), + `Check if there is text/plain flavor on clipboard ${type}`); + // XXX macOS caches the transferable to implement kSelectionCache type, too, + // so it behaves differently than other types. + if (type == clipboard.kSelectionCache && !aIsSupportGetFromCachedTransferable) { + todo_is(clipboard.hasDataMatchingFlavors(["text/foo"], type), + aIsSupportGetFromCachedTransferable, + `Check if there is text/foo flavor on clipboard ${type}`); + } else { + is(clipboard.hasDataMatchingFlavors(["text/foo"], type), + aIsSupportGetFromCachedTransferable, + `Check if there is text/foo flavor on clipboard ${type}`); + } + + info(`Write text/plain data to clipboard ${type} again`); + writeRandomStringToClipboard("text/plain", type); + ok(clipboard.hasDataMatchingFlavors(["text/plain"], type), + `Check if there is text/plain flavor on clipboard ${type}`); + ok(!clipboard.hasDataMatchingFlavors(["text/foo"], type), + `Check if there is text/foo flavor on clipboard ${type}`); + + // Clean clipboard data. + cleanupAllClipboard(); + }); + + add_task(async function test_clipboard_asyncGetData() { + const testClipboardData = async function(aRequest, aExpectedData) { + is(aRequest.flavorList.length, Object.keys(aExpectedData).length, "Check flavorList length"); + for (const [key, value] of Object.entries(aExpectedData)) { + ok(aRequest.flavorList.includes(key), `${key} should be available`); + is(await asyncClipboardRequestGetData(aRequest, key), value, + `Check ${key} data`); + } + }; + + info(`test_clipboard_hasDataMatchingFlavors with pref ` + + `${aIsSupportGetFromCachedTransferable ? "enabled" : "disabled"}`); + + const clipboardData = { "text/plain": generateRandomString() }; + const trans = generateNewTransferable("text/plain", clipboardData["text/plain"]); + + info(`Write text/plain data to clipboard ${type}`); + clipboard.setData(trans, null, type); + await testClipboardData(await asyncGetClipboardData(type), clipboardData); + + info(`Add text/html data to transferable`); + const htmlString = `<div>${generateRandomString()}</div>`; + addStringToTransferable("text/html", htmlString, trans); + // XXX macOS uses cached transferable to implement kSelectionCache type, too, + // so it behaves differently than other types. + if (aIsSupportGetFromCachedTransferable || + (type == clipboard.kSelectionCache && !SpecialPowers.isHeadless)) { + clipboardData["text/html"] = htmlString; + } + await testClipboardData(await asyncGetClipboardData(type), clipboardData); + + info(`Should not get the data from other clipboard type`); + clipboardTypes.forEach(async function(otherType) { + if (otherType != type && + clipboard.isClipboardTypeSupported(otherType)) { + info(`Check clipboard type ${otherType}`); + await testClipboardData(await asyncGetClipboardData(otherType), {}); + } + }); + + info(`Check data on clipboard ${type} again`); + await testClipboardData(await asyncGetClipboardData(type), clipboardData); + }); + + // Test sync set clipboard data. + testClipboardCache(type, false, aIsSupportGetFromCachedTransferable); + + // Test async set clipboard data. + testClipboardCache(type, true, aIsSupportGetFromCachedTransferable); +}); +} + +// Test not get data from clipboard cache. +runClipboardCacheTests(false); + +// Test get data from clipboard cache. +runClipboardCacheTests(true); + +</script> +</body> +</html> diff --git a/widget/tests/test_clipboard_chrome.html b/widget/tests/test_clipboard_chrome.html new file mode 100644 index 0000000000..387e3975b3 --- /dev/null +++ b/widget/tests/test_clipboard_chrome.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=948065 +--> +<head> +<title>Test for Bug 948065</title> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="clipboard_helper.js"></script> +</head> +<body> +<!-- Tests are in file_test_clipboard.js --> +<script src="file_test_clipboard.js"></script> +</body> +</html> diff --git a/widget/tests/test_clipboard_owner_chrome.html b/widget/tests/test_clipboard_owner_chrome.html new file mode 100644 index 0000000000..4759b2b14c --- /dev/null +++ b/widget/tests/test_clipboard_owner_chrome.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1812078 +--> +<head> +<title>Test for Bug 1812078</title> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="clipboard_helper.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="application/javascript"> + +function testClipboardOwner(aClipboardType, aAsync) { + let losingOwnership = false; + const clipboardOwner = { + QueryInterface: ChromeUtils.generateQI(["nsIClipboardOwner"]), + // nsIClipboardOwner + LosingOwnership(aTransferable) { + losingOwnership = true; + }, + }; + + add_task(function test_clipboard_owner() { + info(`Test clipboard owner for type ${aClipboardType} ${aAsync ? "async" : ""}`); + + // Setup clipboard owner. + writeRandomStringToClipboard("text/plain", aClipboardType, clipboardOwner, aAsync); + + // Test should not lose ownership. + clipboardTypes.forEach(function(otherType) { + losingOwnership = false; + if (aClipboardType != otherType && clipboard.isClipboardTypeSupported(otherType)) { + // Test setting clipboard data. + writeRandomStringToClipboard("text/plain", otherType); + ok(!losingOwnership, `Should not lose ownership while setting data to type ${otherType}`); + + // Test async setting clipboard data. + writeRandomStringToClipboard("text/plain", otherType, null, true); + ok(!losingOwnership, `Should not lose ownership while async setting data to type ${otherType}`); + } + }); + + // Test whether should lose ownership. + losingOwnership = false; + writeRandomStringToClipboard("text/plain", aClipboardType, clipboardOwner); + ok(losingOwnership, `Should lose ownership while setting data to type ${aClipboardType}`); + + losingOwnership = false; + writeRandomStringToClipboard("text/plain", aClipboardType, null, true); + ok(losingOwnership, `Should lose ownership while async setting data to type ${aClipboardType}`); + + // Clean clipboard data. + cleanupAllClipboard(); + }); +} + +/** Test for Bug 1812078 */ +clipboardTypes.forEach(function(testType) { + if (clipboard.isClipboardTypeSupported(testType)) { + // Test sync set clipboard data. + testClipboardOwner(testType, false); + + // Test async set clipboard data. + testClipboardOwner(testType, true); + } +}); + +</script> +</body> +</html> diff --git a/widget/tests/test_composition_text_querycontent.xhtml b/widget/tests/test_composition_text_querycontent.xhtml new file mode 100644 index 0000000000..48b7af8100 --- /dev/null +++ b/widget/tests/test_composition_text_querycontent.xhtml @@ -0,0 +1,34 @@ +<?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 title="Testing composition, text and query content events" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +// 3 assertions are: If setting selection with eSetSelection event whose range +// is larger than the actual range, hits "Can only call this on frames that have +// been reflowed: +// '!(GetStateBits() & NS_FRAME_FIRST_REFLOW) || (GetParent()->GetStateBits() & +// NS_FRAME_TOO_DEEP_IN_FRAME_TREE)'" in nsTextFrame.cpp. +// Strangely, this doesn't occur with RDP on Windows. +SimpleTest.expectAssertions(0, 3); +SimpleTest.waitForExplicitFinish(); +window.openDialog("window_composition_text_querycontent.xhtml", "_blank", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> +</window> diff --git a/widget/tests/test_contextmenu_by_mouse_on_unix.html b/widget/tests/test_contextmenu_by_mouse_on_unix.html new file mode 100644 index 0000000000..2b1f643dfc --- /dev/null +++ b/widget/tests/test_contextmenu_by_mouse_on_unix.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<html> +<head> +<title>Test to fire contextmenu event by widget level</title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/SpecialPowers.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +<style> +#target { + width: 100px; + height: 100px; + background-color: blue; +} +</style> +<script> +"use strict"; +add_task(async function test_fire_contextmenu_by_mousedown() { + await SpecialPowers.pushPrefEnv({ + set: [["ui.context_menus.after_mouseup", false]], + }); + await SimpleTest.promiseFocus(); + + // contextmenu event is fired by mouse down. + await process_contextmenu_event({ isMousedown: true, preventEvent: false }); + // contextmenu event is fired by mouse down even if mouse handler calls preventDefault. + await process_contextmenu_event({ isMousedown: true, preventEvent: true }); +}); + +add_task(async function test_fire_contextmenu_by_mouseup() { + await SpecialPowers.pushPrefEnv({ + set: [["ui.context_menus.after_mouseup", true]], + }); + await SimpleTest.promiseFocus(); + + // contextmenu event is fired by mouse up. + await process_contextmenu_event({ isMousedown: false, preventEvent: false }); + // contextmenu event is fired by mouse up even if mouse handler calls preventDefault. + await process_contextmenu_event({ isMousedown: false, preventEvent: true }); +}); + +async function process_contextmenu_event({ isMousedown, preventEvent }) { + await SpecialPowers.contentTransformsReceived(window); + + const target = document.getElementById("target"); + + let count = 0; + + const promise = new Promise(resolve => { + target.addEventListener("mousedown", e => { + is(e.buttons, 2, "The right button down should be fired"); + is(count++, 0, "The first event is mousedown"); + if (isMousedown && preventEvent) { + e.preventDefault(); + } + }, { once: true }); + + if (isMousedown) { + target.addEventListener("contextmenu", e => { + is(count++, 1, "The second event is contextmenu"); + e.preventDefault(); + }, { once: true }); + target.addEventListener("mouseup", e => { + is(count++, 2, "The third event is mouseup"); + resolve(); + }, { once: true} ); + } else { + target.addEventListener("mouseup", e => { + is(count++, 1, "The second event is mouseup"); + if (preventEvent) { + e.preventDefault(); + } + }, { once: true }); + target.addEventListener("contextmenu", e => { + is(count++, 2, "The third event is contextmenu"); + e.preventDefault(); + resolve(); + }, { once: true }); + } + }); + + synthesizeNativeMouseEvent({ + type: "mousedown", + target, + offsetX: 10, + offsetY: 10, + button: 2, + }); + + synthesizeNativeMouseEvent({ + type: "mouseup", + target, + offsetX: 10, + offsetY: 10, + button: 2, + }); + + await promise; +} +</script> +</head> +<body> +<div id="target"></div> +</body> +</html> diff --git a/widget/tests/test_ime_state_in_contenteditable_on_readonly_change_in_parent.html b/widget/tests/test_ime_state_in_contenteditable_on_readonly_change_in_parent.html new file mode 100644 index 0000000000..8d8662a8d8 --- /dev/null +++ b/widget/tests/test_ime_state_in_contenteditable_on_readonly_change_in_parent.html @@ -0,0 +1,72 @@ +<html> +<head> + <title>Test for IME state of contenteditable on readonly state change</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="file_ime_state_test_helper.js"></script> + <script src="file_test_ime_state_in_contenteditable_on_readonly_change.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +<script> +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ +/* import-globals-from file_test_ime_state_in_contenteditable_on_readonly_change.js */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const tipWrapper = new TIPWrapper(window); + const editingHost = document.querySelector("div[contenteditable]"); + await (async function test_ime_state_in_contenteditable_on_readonly_change() { + const tester = new IMEStateInContentEditableOnReadonlyChangeTester(); + tester.checkResultOfPreparation(await tester.prepareToRun(editingHost, editingHost), window, tipWrapper); + tester.checkResultOfMakingHTMLEditorReadonly(await tester.runToMakeHTMLEditorReadonly()); + tester.checkResultOfMakingHTMLEditorEditable(await tester.runToMakeHTMLEditorEditable()); + tester.checkResultOfRemovingContentEditableAttribute(await tester.runToRemoveContentEditableAttribute()); + tester.clear(); + })(); + + await (async function test_ime_state_in_button_in_contenteditable_on_readonly_change() { + const tester = new IMEStateInContentEditableOnReadonlyChangeTester(); + const button = editingHost.querySelector("button"); + tester.checkResultOfPreparation(await tester.prepareToRun(editingHost, button), window, tipWrapper); + tester.checkResultOfMakingHTMLEditorReadonly(await tester.runToMakeHTMLEditorReadonly()); + tester.checkResultOfMakingHTMLEditorEditable(await tester.runToMakeHTMLEditorEditable()); + tester.checkResultOfRemovingContentEditableAttribute(await tester.runToRemoveContentEditableAttribute()); + tester.clear(); + })(); + + await (async function test_ime_state_of_text_controls_in_contenteditable_on_readonly_change() { + const tester = new IMEStateOfTextControlInContentEditableOnReadonlyChangeTester(); + for (let index = 0; + index < IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.numberOfTextControlTypes; + index++) { + tester.checkResultOfPreparation(await tester.prepareToRun(index, editingHost), window, tipWrapper); + tester.checkResultOfMakingParentEditingHost(await tester.runToMakeParentEditingHost()); + tester.checkResultOfMakingHTMLEditorReadonly(await tester.runToMakeHTMLEditorReadonly()); + tester.checkResultOfMakingHTMLEditorEditable(await tester.runToMakeHTMLEditorEditable()); + tester.checkResultOfMakingParentNonEditable(await tester.runToMakeParentNonEditingHost()); + tester.clear(); + } + editingHost.setAttribute("contenteditable", ""); + })(); + + await (async function test_ime_state_outside_contenteditable_on_readonly_change() { + const tester = new IMEStateOutsideContentEditableOnReadonlyChangeTester(); + for (let index = 0; + index < IMEStateOutsideContentEditableOnReadonlyChangeTester.numberOfFocusTargets; + index++) { + tester.checkResultOfPreparation(await tester.prepareToRun(index, editingHost), window, tipWrapper); + tester.checkResultOfMakingParentEditingHost(await tester.runToMakeParentEditingHost()); + tester.checkResultOfMakingHTMLEditorReadonly(await tester.runToMakeHTMLEditorReadonly()); + tester.checkResultOfMakingHTMLEditorEditable(await tester.runToMakeHTMLEditorEditable()); + tester.checkResultOfMakingParentNonEditable(await tester.runToMakeParentNonEditingHost()); + tester.clear(); + } + editingHost.setAttribute("contenteditable", ""); + })(); + + SimpleTest.finish(); +}); +</script> +</head> +<body><div contenteditable><br><button>button</button></div></body> +</html> diff --git a/widget/tests/test_ime_state_in_plugin_in_parent.html b/widget/tests/test_ime_state_in_plugin_in_parent.html new file mode 100644 index 0000000000..9f3892ab88 --- /dev/null +++ b/widget/tests/test_ime_state_in_plugin_in_parent.html @@ -0,0 +1,92 @@ +<html> +<head> + <title>Test for IME state on plugin</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="file_ime_state_test_helper.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<input> +<object type="application/x-test"></object> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + const tipWrapper = new TIPWrapper(window); + const plugin = document.querySelector("object"); + + // Plugins are not supported and their elements should not accept focus; + // therefore, IME should not enable when we play with it. + + document.activeElement?.blur(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when no element has focus" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME should not have focus when no element has focus" + ); + + plugin.focus(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when an <object> for plugin has focus" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME should not have focus when an <object> for plugin has focus" + ); + + plugin.blur(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when an <object> for plugin gets blurred" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME should not have focus when an <object> for plugin gets blurred" + ); + + plugin.focus(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when an <object> for plugin gets focused again" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME should not have focus when an <object> for plugin gets focused again" + ); + + plugin.remove(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME enabled state should be disabled when focused <object> for plugin is removed from the document" + ); + ok( + !tipWrapper.IMEHasFocus, + "IME should not have focus when focused <object> for plugin is removed from the document" + ); + + document.querySelector("input").focus(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + "IME enabled state should be enabled after <input> gets focus" + ); + ok( + tipWrapper.IMEHasFocus, + "IME should have focus after <input> gets focus" + ); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/widget/tests/test_ime_state_in_text_control_on_reframe_in_parent.html b/widget/tests/test_ime_state_in_text_control_on_reframe_in_parent.html new file mode 100644 index 0000000000..ab38806261 --- /dev/null +++ b/widget/tests/test_ime_state_in_text_control_on_reframe_in_parent.html @@ -0,0 +1,42 @@ +<html> +<head> + <title>Test for IME state of contenteditable on readonly state change</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="file_ime_state_test_helper.js"></script> + <script src="file_test_ime_state_in_text_control_on_reframe.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<script> +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ +/* import-globals-from file_test_ime_state_in_text_control_on_reframe.js */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const tipWrapper = new TIPWrapper(window); + const tester = new IMEStateInTextControlOnReframeTester(); + for (let index = 0; + index < IMEStateInTextControlOnReframeTester.numberOfTextControlTypes; + index++) { + tipWrapper.clearFocusBlurNotifications(); + const expectedData1 = await tester.prepareToRun(index, document); + tipWrapper.typeA(); + await new Promise(resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve) + )); // Flush IME content observer notifications. + tester.checkResultAfterTypingA(expectedData1, window, tipWrapper); + + const expectedData2 = await tester.prepareToRun2(index, document); + tipWrapper.typeA(); + await new Promise(resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve) + )); // Flush IME content observer notifications. + tester.checkResultAfterTypingA2(expectedData2); + } + + SimpleTest.finish(); +}); +</script> +<body></body> +</html> diff --git a/widget/tests/test_ime_state_on_editable_state_change_in_parent.html b/widget/tests/test_ime_state_on_editable_state_change_in_parent.html new file mode 100644 index 0000000000..a1b307a51f --- /dev/null +++ b/widget/tests/test_ime_state_on_editable_state_change_in_parent.html @@ -0,0 +1,263 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for IME state management at changing editable state</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="file_ime_state_test_helper.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<div></div> +<script> +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const tipWrapper = new TIPWrapper(window); + + function waitForIMEContentObserverSendingNotifications() { + return new Promise( + resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve) + ) + ); + } + + function resetIMEStateWithFocusMove() { + const input = document.createElement("input"); + document.body.appendChild(input); + input.focus(); + input.remove(); + return waitForIMEContentObserverSendingNotifications(); + } + + await (async function test_setting_contenteditable_of_focused_div() { + const div = document.querySelector("div"); + div.setAttribute("tabindex", "0"); + div.focus(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "test_setting_contenteditable_of_focused_div: IME should be disabled when non-editable <div> has focus" + ); + div.setAttribute("contenteditable", ""); + await waitForIMEContentObserverSendingNotifications(); + // Sometimes, it's not enough waiting only 2 animation frames here to wait + // for IME focus, perhaps, it may be related to HTMLEditor initialization. + // Let's wait one more animation frame here. + if (!tipWrapper.IMEHasFocus) { + await new Promise(resolve => requestAnimationFrame(resolve)); + } + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + "test_setting_contenteditable_of_focused_div: IME should be enabled when contenteditable of focused <div> is set" + ); + ok( + tipWrapper.IMEHasFocus, + "test_setting_contenteditable_of_focused_div: IME should have focus when contenteditable of focused <div> is set" + ) + div.removeAttribute("contenteditable"); + await waitForIMEContentObserverSendingNotifications(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "test_setting_contenteditable_of_focused_div: IME should be disabled when contenteditable of focused <div> is removed" + ); + ok( + !tipWrapper.IMEHasFocus, + "test_setting_contenteditable_of_focused_div: IME should not have focus when contenteditable of focused <div> is removed" + ); + div.removeAttribute("tabindex"); + })(); + + await resetIMEStateWithFocusMove(); + + await (async function test_removing_contenteditable_of_non_last_editable_div() { + const div = document.querySelector("div"); + div.setAttribute("tabindex", "0"); + div.setAttribute("contenteditable", ""); + const anotherEditableDiv = document.createElement("div"); + anotherEditableDiv.setAttribute("contenteditable", ""); + div.parentElement.appendChild(anotherEditableDiv); + div.focus(); + await waitForIMEContentObserverSendingNotifications(); + div.removeAttribute("contenteditable"); + await waitForIMEContentObserverSendingNotifications(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "test_removing_contenteditable_of_non_last_editable_div: IME should be disabled when contenteditable of focused <div> is removed" + ); + ok( + !tipWrapper.IMEHasFocus, + "test_removing_contenteditable_of_non_last_editable_div: IME should not have focus when contenteditable of focused <div> is removed" + ); + anotherEditableDiv.remove(); + div.removeAttribute("tabindex"); + })(); + + await resetIMEStateWithFocusMove(); + + await (async function test_setting_designMode() { + window.focus(); + document.designMode = "on"; + await waitForIMEContentObserverSendingNotifications(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + 'test_setting_designMode: IME should be enabled when designMode is set to "on"' + ); + ok( + tipWrapper.IMEHasFocus, + 'test_setting_designMode: IME should have focus when designMode is set to "on"' + ); + document.designMode = "off"; + await waitForIMEContentObserverSendingNotifications(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + 'test_setting_designMode: IME should be disabled when designMode is set to "off"' + ); + ok( + !tipWrapper.IMEHasFocus, + 'test_setting_designMode: IME should not have focus when designMode is set to "off"' + ); + })(); + + await resetIMEStateWithFocusMove(); + + async function test_setting_content_editable_of_body_when_shadow_DOM_has_focus(aMode) { + const div = document.querySelector("div"); + const shadow = div.attachShadow({mode: aMode}); + const divInShadow = document.createElement("div"); + divInShadow.setAttribute("tabindex", "0"); + shadow.appendChild(divInShadow); + divInShadow.focus(); + await waitForIMEContentObserverSendingNotifications(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${ + aMode + }): IME should be disabled when non-editable <div> in a shadow DOM has focus` + ); + document.body.setAttribute("contenteditable", ""); + await waitForIMEContentObserverSendingNotifications(); + // todo_is because of bug 1807597. Gecko does not update focus when focused + // element becomes an editable child. Therefore, cannot initialize + // HTMLEditor with the new editing host. + todo_is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${ + aMode + }): IME should be enabled when the <body> becomes editable` + ); + todo( + tipWrapper.IMEHasFocus, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus(${ + aMode + }): IME should have focus when the <body> becomes editable` + ); + document.body.removeAttribute("contenteditable"); + await waitForIMEContentObserverSendingNotifications(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus)${ + aMode + }): IME should be disabled when the <body> becomes not editable` + ); + ok( + !tipWrapper.IMEHasFocus, + `test_setting_content_editable_of_body_when_shadow_DOM_has_focus)${ + aMode + }): IME should not have focus when the <body> becomes not editable` + ); + div.remove(); + document.body.appendChild(document.createElement("div")); + }; + + async function test_setting_designMode_when_shadow_DOM_has_focus(aMode) { + const div = document.querySelector("div"); + const shadow = div.attachShadow({mode: aMode}); + const divInShadow = document.createElement("div"); + divInShadow.setAttribute("tabindex", "0"); + shadow.appendChild(divInShadow); + divInShadow.focus(); + await waitForIMEContentObserverSendingNotifications(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_designMode_when_shadow_DOM_has_focus(${ + aMode + }): IME should be disabled when non-editable <div> in a shadow DOM has focus` + ); + document.designMode = "on"; + await waitForIMEContentObserverSendingNotifications(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_designMode_when_shadow_DOM_has_focus(${ + aMode + }): IME should stay disabled when designMode is set` + ); + ok( + !tipWrapper.IMEHasFocus, + `test_setting_designMode_when_shadow_DOM_has_focus(${ + aMode + }): IME should not have focus when designMode is set` + ); + divInShadow.setAttribute("contenteditable", ""); + await waitForIMEContentObserverSendingNotifications(); + // todo_is because of bug 1807597. Gecko does not update focus when focused + // document is into the design mode. Therefore, cannot initialize + // HTMLEditor with the document node properly. + todo_is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + `test_setting_designMode_when_shadow_DOM_has_focus(${ + aMode + }): IME should be enabled when focused <div> in a shadow DOM becomes editable` + ); + todo( + tipWrapper.IMEHasFocus, + `test_setting_designMode_when_shadow_DOM_has_focus(${ + aMode + }): IME should have focus when focused <div> in a shadow DOM becomes editable` + ); + document.designMode = "off"; + await waitForIMEContentObserverSendingNotifications(); + is( + window.windowUtils.IMEStatus, + Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + `test_setting_designMode_when_shadow_DOM_has_focus(${ + aMode + }): IME should be disabled when designMode is unset` + ); + ok( + !tipWrapper.IMEHasFocus, + `test_setting_designMode_when_shadow_DOM_has_focus(${ + aMode + }): IME should not have focus when designMode is unset` + ); + div.remove(); + document.body.appendChild(document.createElement("div")); + } + + for (const mode of ["open", "closed"]) { + await test_setting_content_editable_of_body_when_shadow_DOM_has_focus(mode); + await resetIMEStateWithFocusMove(); + await test_setting_designMode_when_shadow_DOM_has_focus(mode); + await resetIMEStateWithFocusMove(); + } + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/widget/tests/test_ime_state_on_focus_move_in_parent.html b/widget/tests/test_ime_state_on_focus_move_in_parent.html new file mode 100644 index 0000000000..fd74d61c7e --- /dev/null +++ b/widget/tests/test_ime_state_on_focus_move_in_parent.html @@ -0,0 +1,88 @@ +<!doctype html> +<html style="ime-mode: disabled;"> +<head> + <meta charset="utf-8"> + <title>Test for IME state management on focus move in parent process</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="file_ime_state_test_helper.js"></script> + <script src="file_test_ime_state_on_focus_move.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body style="ime-mode: disabled;"> +<div style="ime-mode: disabled;"></div> +<script> +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ +/* import-globals-from file_test_ime_state_on_focus_move.js */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const tipWrapper = new TIPWrapper(window); + ok(tipWrapper.isAvailable(), "TextInputProcessor should've been initialized"); + + const container = document.querySelector("div"); + async function runIMEStateOnFocusMoveTests(aDescription) { + { + const runnerAndChecker = new IMEStateWhenNoActiveElementTester(aDescription); + const expectedData = await runnerAndChecker.run(document); + runnerAndChecker.check(expectedData); + } + for (let index = 0; index < IMEStateOnFocusMoveTester.numberOfTests; ++index) { + const runnerAndChecker = new IMEStateOnFocusMoveTester(aDescription, index); + const expectedData = await runnerAndChecker.prepareToRun(container); + runnerAndChecker.prepareToCheck(expectedData, tipWrapper); + await runnerAndChecker.run(); + runnerAndChecker.check(expectedData); + if (runnerAndChecker.canTestOpenCloseState(expectedData)) { + for (const defaultOpenState of [false, true]) { + const expectedOpenStateData = + await runnerAndChecker.prepareToRunOpenCloseTest(container); + runnerAndChecker.prepareToCheckOpenCloseTest( + defaultOpenState, + expectedOpenStateData + ); + await runnerAndChecker.runOpenCloseTest(); + runnerAndChecker.checkOpenCloseTest(expectedOpenStateData); + } + } + runnerAndChecker.destroy(); + } + } + + // test for normal contents. + await runIMEStateOnFocusMoveTests("in non-editable container"); + + // test for contentEditable="true" + container.setAttribute("contenteditable", "true"); + await runIMEStateOnFocusMoveTests("in div[contenteditable]"); + + // test for contentEditable="false" + container.setAttribute("contenteditable", "false"); + await runIMEStateOnFocusMoveTests('in div[contenteditable="false"]'); + + // test for removing contentEditable + container.setAttribute("contenteditable", "true"); + container.focus(); + await new Promise(resolve => + requestAnimationFrame( + () => requestAnimationFrame(resolve) + ) + ); + container.removeAttribute("contenteditable"); + await runIMEStateOnFocusMoveTests("after removing contenteditable from the container"); + + // test designMode + document.designMode = "on"; + await runIMEStateOnFocusMoveTests('in designMode="on"'); + document.designMode = "off"; + await runIMEStateOnFocusMoveTests('in designMode="off"'); + + tipWrapper.destroy(); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/widget/tests/test_ime_state_on_input_type_change_in_parent.html b/widget/tests/test_ime_state_on_input_type_change_in_parent.html new file mode 100644 index 0000000000..2644c31e3f --- /dev/null +++ b/widget/tests/test_ime_state_on_input_type_change_in_parent.html @@ -0,0 +1,39 @@ +<html> +<head> + <title>Test for IME state on input type change</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="file_ime_state_test_helper.js"></script> + <script src="file_test_ime_state_on_input_type_change.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +<script> +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ +/* import-globals-from file_test_ime_state_on_input_type_change.js */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectAssertions(6); // Hit in IMEStateManager::UpdateIMEState +SimpleTest.waitForFocus(async () => { + const tipWrapper = new TIPWrapper(window); + for (let srcIndex = 0; srcIndex < IMEStateOnInputTypeChangeTester.numberOfTests; srcIndex++) { + const tester = new IMEStateOnInputTypeChangeTester(srcIndex); + for (let destIndex = 0; destIndex < IMEStateOnInputTypeChangeTester.numberOfTests; destIndex++) { + const expectedResultBefore = await tester.prepareToRun(destIndex, window, document.body); + if (expectedResultBefore === false) { + continue; + } + tester.checkBeforeRun(expectedResultBefore, tipWrapper); + const expectedResult = await tester.run(); + tester.checkResult(expectedResultBefore, expectedResult); + tipWrapper.clearFocusBlurNotifications(); + tester.clear(); + } + } + + SimpleTest.finish(); +}); +</script> +</head> +<body> +</body> +</html> diff --git a/widget/tests/test_ime_state_on_readonly_change_in_parent.html b/widget/tests/test_ime_state_on_readonly_change_in_parent.html new file mode 100644 index 0000000000..0557856542 --- /dev/null +++ b/widget/tests/test_ime_state_on_readonly_change_in_parent.html @@ -0,0 +1,31 @@ +<html> +<head> + <title>Test for IME state on readonly state change</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="file_ime_state_test_helper.js"></script> + <script src="file_test_ime_state_on_readonly_change.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +<script> +"use strict"; + +/* import-globals-from file_ime_state_test_helper.js */ +/* import-globals-from file_test_ime_state_on_readonly_change.js */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const tipWrapper = new TIPWrapper(window); + const tester = new IMEStateOnReadonlyChangeTester(); + for (let i = 0; i < IMEStateOnReadonlyChangeTester.numberOfTextControlTypes; i++) { + tester.checkBeforeRun(await tester.prepareToRun(i, window, document.body), tipWrapper); + tester.checkResultOfMakingTextControlReadonly(await tester.runToMakeTextControlReadonly()); + tester.checkResultOfMakingTextControlEditable(await tester.runToMakeTextControlEditable()); + tipWrapper.clearFocusBlurNotifications(); + tester.clear(); + } + SimpleTest.finish(); +}); +</script> +</head> +<body> +</body> +</html> diff --git a/widget/tests/test_ime_state_others_in_parent.html b/widget/tests/test_ime_state_others_in_parent.html new file mode 100644 index 0000000000..e6ae0ab272 --- /dev/null +++ b/widget/tests/test_ime_state_others_in_parent.html @@ -0,0 +1,153 @@ +<html> +<head> + <title>Test for IME state controlling in some special cases</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="file_ime_state_test_helper.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"></div> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script> +SimpleTest.waitForExplicitFinish(); + +var gUtils = window.windowUtils; +var gFM = Services.focus; + +function runEditorFlagChangeTests() { + var description = "runEditorFlagChangeTests: "; + + var container = document.getElementById("display"); + + // Reset selection from previous tests. + window.getSelection().collapse(container, 0); + + // the editor has focus directly. + container.setAttribute("contenteditable", "true"); + container.focus(); + + is(gFM.focusedElement, container, + description + "The editor doesn't get focus"); + is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED, + description + "IME isn't enabled on HTML editor"); + const kIMEStateChangeFlags = Ci.nsIEditor.eEditorReadonlyMask; + const kFlagsNotAllowedWithHTMLEditor = + Ci.nsIEditor.eEditorPasswordMask | + Ci.nsIEditor.eEditorSingleLineMask; + var editor = window.docShell.editor; + var flags = editor.flags; + + // input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3078\u3093\u3057\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": 4, "length": 0 }, + }); + + editor.flags &= ~kIMEStateChangeFlags; + ok(editor.composing, + description + "#1 IME composition was committed unexpectedly"); + is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED, + description + "#1 IME isn't enabled on HTML editor"); + + editor.flags |= + ~(kIMEStateChangeFlags | kFlagsNotAllowedWithHTMLEditor); + ok(editor.composing, + description + "#2 IME composition was committed unexpectedly"); + is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED, + description + "#2 IME isn't enabled on HTML editor"); + + editor.flags = flags; + ok(editor.composing, + description + "#3 IME composition was committed unexpectedly"); + is(gUtils.IMEStatus, gUtils.IME_STATUS_ENABLED, + description + "#3 IME isn't enabled on HTML editor"); + + // cancel the composition + synthesizeComposition({ type: "compositioncommit", data: "" }); + + container.removeAttribute("contenteditable"); +} + +function runEditableSubframeTests() { + window.open("window_imestate_iframes.html", "_blank", + "width=600,height=600"); +} + +function runTestPasswordFieldOnDialog() { + if (document.activeElement) { + document.activeElement.blur(); + } + + var dialog; + + function WindowObserver() { + Services.obs.addObserver(this, "domwindowopened"); + } + + WindowObserver.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + if (topic === "domwindowopened") { + ok(true, "dialog window is created"); + dialog = subject; + dialog.addEventListener("load", onPasswordDialogLoad); + } + }, + }; + + var observer = new WindowObserver(); + var arg1 = {}, arg2 = {}; + Services.prompt.promptPassword(window, "title", "text", arg1, "msg", arg2); + + ok(true, "password dialog was closed"); + + Services.obs.removeObserver(observer, "domwindowopened"); + + var passwordField; + + function onPasswordDialogLoad() { + ok(true, "onPasswordDialogLoad is called"); + dialog.removeEventListener("load", onPasswordDialogLoad); + passwordField = dialog.document.getElementById("password1Textbox"); + passwordField.addEventListener("focus", onPasswordFieldFocus); + } + + function onPasswordFieldFocus() { + ok(true, "onPasswordFieldFocus is called"); + passwordField.removeEventListener("focus", onPasswordFieldFocus); + var utils = dialog.windowUtils; + is(utils.IMEStatus, utils.IME_STATUS_PASSWORD, + "IME isn't disabled on a password field of password dialog"); + synthesizeKey("VK_ESCAPE", { }, dialog); + } +} + +SimpleTest.waitForFocus(async () => { + // test whether the IME state and composition are not changed unexpectedly + runEditorFlagChangeTests(); + + // test password field on dialog + // XXX temporary disable against failure + // runTestPasswordFieldOnDialog(); + + // This will call onFinish(), so, this test must be the last. + // TODO: Make this test run with remote content too. + runEditableSubframeTests(); +}); + +function onFinish() { + SimpleTest.finish(); +} +</script> +</body> +</html> diff --git a/widget/tests/test_input_events_on_deactive_window.xhtml b/widget/tests/test_input_events_on_deactive_window.xhtml new file mode 100644 index 0000000000..d54699f76c --- /dev/null +++ b/widget/tests/test_input_events_on_deactive_window.xhtml @@ -0,0 +1,233 @@ +<?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 title="Testing composition, text and query content events" + 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" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> +</div> +<p id="display"> + <textarea id="textarea"></textarea> +</p> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +var textarea = document.getElementById("textarea"); +var otherWindow; +var timer; + +function runTests() +{ + textarea.focus(); + is(Services.focus.focusedElement, textarea, "we're deactive"); + if (Services.focus.focusedElement != textarea) { + SimpleTest.finish(); + return; + } + + otherWindow = + window.browsingContext.topChromeWindow.open( + "./file_input_events_on_deactive_window.html", "_blank", + "chrome,width=100,height=100"); + ok(otherWindow, "failed to open other window"); + if (!otherWindow) { + SimpleTest.finish(); + return; + } + + SimpleTest.waitForFocus(startTests, otherWindow); + otherWindow.focus(); +} + +function startTests() +{ + clearTimeout(timer); + isnot(Services.focus.focusedWindow, window, "we're not deactive"); + if (Services.focus.focusedWindow == window) { + otherWindow.close(); + SimpleTest.finish(); + return; + } + + var keydownHandled, keypressHandled, keyupHandled, compositionstartHandled, + compositionendHandled, compositionupdateHandled, inputHandled; + + function clear() + { + keydownHandled = false; + keypressHandled = false; + keyupHandled = false; + compositionstartHandled = false; + compositionendHandled = false; + compositionupdateHandled = false; + inputHandled = false; + } + + function onEvent(aEvent) + { + if (aEvent.type == "keydown") { + keydownHandled = true; + } else if (aEvent.type == "keypress") { + keypressHandled = true; + } else if (aEvent.type == "keyup") { + keyupHandled = true; + } else if (aEvent.type == "compositionstart") { + compositionstartHandled = true; + } else if (aEvent.type == "compositionend") { + compositionendHandled = true; + } else if (aEvent.type == "compositionupdate") { + compositionupdateHandled = true; + } else if (aEvent.type == "input") { + inputHandled = true; + } else { + ok(false, "handled unknown event: " + aEvent.type); + } + } + + textarea.addEventListener("keydown", onEvent); + textarea.addEventListener("keypress", onEvent); + textarea.addEventListener("keyup", onEvent); + textarea.addEventListener("compositionstart", onEvent); + textarea.addEventListener("compositionend", onEvent); + textarea.addEventListener("compositionupdate", onEvent); + textarea.addEventListener("input", onEvent); + + startTestsInternal(); + + function startTestsInternal() + { + // key events + function checkKeyEvents(aKeydown, aKeypress, aKeyup, aInput, aDescription) + { + is(keydownHandled, aKeydown, + "keydown event is (not) handled: " + aDescription); + is(keypressHandled, aKeypress, + "keypress event is (not) handled: " + aDescription); + is(keyupHandled, aKeyup, + "keyup event is (not) handled: " + aDescription); + is(inputHandled, aInput, + "input event is (not) handled: " + aDescription); + } + + function checkCompositionEvents(aStart, aEnd, aUpdate, aInput, aDescription) + { + is(compositionstartHandled, aStart, + "compositionstart event is (not) handled: " + aDescription); + is(compositionendHandled, aEnd, + "compositionend event is (not) handled: " + aDescription); + is(compositionupdateHandled, aUpdate, + "compositionupdate event is (not) handled: " + aDescription); + is(inputHandled, aInput, + "input event is (not) handled: " + aDescription); + } + + clear(); + synthesizeKey("a", {type: "keydown"}); + checkKeyEvents(true, true, false, true, "a keydown and a keypress"); + is(textarea.value, "a", "textarea value isn't 'a'"); + clear(); + synthesizeKey("a", {type: "keyup"}); + checkKeyEvents(false, false, true, false, "a keyup"); + clear(); + synthesizeKey("KEY_Backspace"); + checkKeyEvents(true, true, true, true, "KEY_Backspace key events"); + is(textarea.value, "", "textarea value isn't empty"); + + // IME events + clear(); + // input first character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + checkCompositionEvents(true, false, true, true, "starting to compose"); + var queryText = synthesizeQueryTextContent(0, 100); + ok(queryText, "query text event result is null"); + if (!queryText) { + return; + } + ok(queryText.succeeded, "query text event failed"); + if (!queryText.succeeded) { + return; + } + is(queryText.text, "\u3089", "composing text is incorrect"); + var querySelectedText = synthesizeQuerySelectedText(); + ok(querySelectedText, "query selected text event result is null"); + if (!querySelectedText) { + return; + } + ok(querySelectedText.succeeded, "query selected text event failed"); + if (!querySelectedText.succeeded) { + return; + } + is(querySelectedText.offset, 1, + "query selected text event returns wrong offset"); + is(querySelectedText.text, "", + "query selected text event returns wrong selected text"); + clear(); + // commit composition + synthesizeComposition({ type: "compositioncommitasis" }); + checkCompositionEvents(false, true, false, true, "commit composition as is"); + queryText = synthesizeQueryTextContent(0, 100); + ok(queryText, "query text event result is null after commit"); + if (!queryText) { + return; + } + ok(queryText.succeeded, "query text event failed after commit"); + if (!queryText.succeeded) { + return; + } + is(queryText.text, "\u3089", "composing text is incorrect after commit"); + querySelectedText = synthesizeQuerySelectedText(); + ok(querySelectedText, + "query selected text event result is null after commit"); + if (!querySelectedText) { + return; + } + ok(querySelectedText.succeeded, + "query selected text event failed after commit"); + if (!querySelectedText.succeeded) { + return; + } + is(querySelectedText.offset, 1, + "query selected text event returns wrong offset after commit"); + is(querySelectedText.text, "", + "query selected text event returns wrong selected text after commit"); + clear(); + } + + textarea.removeEventListener("keydown", onEvent); + textarea.removeEventListener("keypress", onEvent); + textarea.removeEventListener("keyup", onEvent); + textarea.removeEventListener("compositionstart", onEvent); + textarea.removeEventListener("compositionupdate", onEvent); + textarea.removeEventListener("compositionend", onEvent); + textarea.removeEventListener("input", onEvent); + + otherWindow.close(); + + SimpleTest.finish(); +} + + +]]> +</script> +</window> diff --git a/widget/tests/test_key_event_counts.xhtml b/widget/tests/test_key_event_counts.xhtml new file mode 100644 index 0000000000..6eda6a52fb --- /dev/null +++ b/widget/tests/test_key_event_counts.xhtml @@ -0,0 +1,90 @@ +<?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"?> + +<!-- We've had issues on Mac OS X where native key events either don't get processed + or they get processed twice. This test tests some of those scenarios. --> + +<window id="window1" title="Test Key Event Counts" onload="runTest()" + 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[ + var gKeyPressEventCount = 0; + var gKeyDownEventCount = 0; + + function onKeyDown(e) + { + gKeyDownEventCount++; + } + + function onKeyPress(e) + { + gKeyPressEventCount++; + e.preventDefault(); + } + + function* testBody() + { + window.addEventListener("keydown", onKeyDown); + window.addEventListener("keypress", onKeyPress); + + // Test ctrl-tab + gKeyDownEventCount = 0; + gKeyPressEventCount = 0; + yield synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_Tab, {ctrlKey:1}, "\t", "\t", continueTest); + is(gKeyDownEventCount, 1); + is(gKeyPressEventCount, 0, "ctrl-tab should be consumed by tabbox of tabbrowser at keydown"); + + // Test cmd+shift+a + gKeyDownEventCount = 0; + gKeyPressEventCount = 0; + yield synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {metaKey:1, shiftKey:1}, "a", "A", continueTest); + is(gKeyDownEventCount, 1); + is(gKeyPressEventCount, 1); + + // Test cmd-; + gKeyDownEventCount = 0; + gKeyPressEventCount = 0; + yield synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_Semicolon, {metaKey:1}, ";", ";", continueTest); + is(gKeyDownEventCount, 1); + is(gKeyPressEventCount, 1); + + window.removeEventListener("keydown", onKeyDown); + window.removeEventListener("keypress", onKeyPress); + } + + var gTestContinuation = null; + + function continueTest() + { + if (!gTestContinuation) { + gTestContinuation = testBody(); + } + var ret = gTestContinuation.next(); + if (ret.done) { + SimpleTest.finish(); + } else { + is(ret.value, true, "Key synthesized successfully"); + } + } + + function runTest() + { + SimpleTest.waitForExplicitFinish(); + continueTest(); + } + ]]></script> + +</window> diff --git a/widget/tests/test_keycodes.xhtml b/widget/tests/test_keycodes.xhtml new file mode 100644 index 0000000000..aea6aaae98 --- /dev/null +++ b/widget/tests/test_keycodes.xhtml @@ -0,0 +1,5647 @@ +<?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 title="Key event tests" + onload="runTest()" + 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/NativeKeyCodes.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + +<commandset> + <command id="expectedCommand" oncommand="this.activeCount++" disabled="true"/> + <command id="unexpectedCommand" oncommand="this.activeCount++" disabled="true"/> + <command id="expectedReservedCommand" oncommand="this.activeCount++" reserved="true" disabled="true"/> +</commandset> +<keyset> + <key id="unshiftedKey" key=";" modifiers="accel" command="unexpectedCommand"/> + <key id="shiftedKey" key=":" modifiers="accel" command="unexpectedCommand"/> + <key id="commandOptionF" key='f' modifiers="accel,alt" command="unexpectedCommand"/> + <key id="question" key='?' modifiers="accel" command="unexpectedCommand"/> + <key id="unshiftedX" key="x" modifiers="accel" command="unexpectedCommand"/> + <key id="shiftedX" key="X" modifiers="accel,shift" command="unexpectedCommand"/> + <key id="ctrlAltA" key="a" modifiers="accel,alt" command="unexpectedCommand"/> + <key id="ctrlAltShiftA" key="A" modifiers="accel,alt,shift" command="unexpectedCommand"/> + <key id="unshiftedPlus" key="+" modifiers="accel" command="unexpectedCommand"/> + <key id="reservedUnshiftedKey" key="'" modifiers="accel" command="unexpectedCommand"/> + <key id="reservedShiftedKey" key='"' modifiers="accel" command="unexpectedCommand"/> +</keyset> + +<browser id="browser" type="content" src="data:text/html;charset=utf-8,<button id='content_button'>button</button>" width="200" height="32"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> + <!-- for some reason, if we don't have 'accesskey' here, adding it dynamically later + doesn't work! --> + <button id="button" accesskey="z">Hello</button> + <input type="text" id="textbox" value=""/> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ +const IS_MAC = navigator.platform.indexOf("Mac") == 0; +const IS_WIN = navigator.platform.indexOf("Win") == 0; +const OS_VERSION = + IS_WIN ? parseFloat(Services.sysinfo.getProperty("version")) : 0; +const WIN8 = 6.2; // Can remove once bug 1594270 is closed + +function isModifierKeyEvent(aEvent) +{ + switch (aEvent.key) { + case "Alt": + case "AltGraph": + case "CapsLock": + case "Control": + case "Fn": + case "FnLock": + case "Hyper": + case "Meta": + case "NumLock": + case "ScrollLock": + case "Shift": + case "Super": + case "Symbol": + case "SymbolLock": + return true; + default: + return false; + } +} + +/** + * Firefox infobar UI can have access keys which conflict with this test. Really + * stupid workaround until we can move this test into its own chrome window. + */ +function clearInfobars() +{ + var browser = window.top.docShell.chromeEventHandler; + var chromeWin = browser.ownerGlobal; + var nb = chromeWin.gBrowser.getNotificationBox(browser); + for (let n of nb.allNotifications) { + nb.removeNotification(n, true); + } +} + +function eventToString(aEvent) +{ + var name = aEvent.layout.name + " keyCode=" + + aEvent.keyCode + " (0x" + aEvent.keyCode.toString(16).toUpperCase() + + ") chars='" + aEvent.chars + "'"; + if (typeof aEvent.unmodifiedChars === "string") { + name += " unmodifiedChars='" + aEvent.unmodifiedChars + "'"; + } + if (aEvent.modifiers.capsLockKey) { + name += " [CapsLock]"; + } + if (aEvent.modifiers.shiftKey) { + name += " [Shift]"; + } + if (aEvent.modifiers.shiftRightKey) { + name += " [Right Shift]"; + } + if (aEvent.modifiers.ctrlKey) { + name += " [Ctrl]"; + } + if (aEvent.modifiers.ctrlRightKey) { + name += " [Right Ctrl]"; + } + if (aEvent.modifiers.altKey) { + name += " [Alt]"; + } + if (aEvent.modifiers.altGrKey) { + name += " [AltGr]"; + } + if (aEvent.modifiers.altRightKey) { + name += " [Right Alt]"; + } + if (aEvent.modifiers.metaKey) { + name += ` [${IS_MAC ? "Command" : "Win"}]`; + } + if (aEvent.modifiers.metaRightKey) { + name += ` [Right ${IS_MAC ? "Command" : "Win"}]`; + } + + return name; +} + +function getPhase(aDOMEvent) +{ + switch (aDOMEvent.eventPhase) { + case aDOMEvent.None: + return "none"; + case aDOMEvent.CAPTURING_PHASE: + return "capture"; + case aDOMEvent.AT_TARGET: + return "target"; + case aDOMEvent.BUBBLING_PHASE: + return "bubble"; + default: + return ""; + } +} + +function eventTargetToString(aEventTarget) +{ + if (aEventTarget.navigator) { + return "window"; + } + switch (aEventTarget.nodeType) { + case Node.ELEMENT_NODE: + return "element (" + aEventTarget.tagName + ")"; + case Node.DOCUMENT_NODE: + return "document"; + default: + return ""; + } +} + +function synthesizeKey(aEvent, aFocusElementId, aCallback) +{ + if (aFocusElementId.startsWith("content_")) { + var browser = document.getElementById("browser"); + browser.contentDocument.getElementById(aFocusElementId).focus(); + } else { + document.getElementById(aFocusElementId).focus(); + } + + return synthesizeNativeKey(aEvent.layout, aEvent.keyCode, + aEvent.modifiers, + aEvent.chars, aEvent.unmodifiedChars, + aCallback); +} + +// Test the charcodes and modifiers being delivered to keypress handlers and +// also keydown/keyup events too. +function* runKeyEventTests() +{ + var currentTestName; + var eventList, keyDownFlags, keyUpFlags, testingEvent, expectedDOMKeyCode; + const kShiftFlag = 0x1; + const kCtrlFlag = 0x2; + const kAltFlag = 0x4; + const kMetaFlag = 0x8; + const kNumLockFlag = 0x10; + const kCapsLockFlag = 0x20; + const kAltGraphFlag = 0x40; + + function onKeyEvent(e) + { + /* eslint-disable-next-line no-shadow */ + function removeFlag(e, aFlag) + { + if (e.type == "keydown") { + let oldValue = keyDownFlags; + keyDownFlags &= ~aFlag; + return oldValue != keyDownFlags; + } else if (e.type == "keyup") { + let oldValue = keyUpFlags; + keyUpFlags &= ~aFlag; + return oldValue != keyUpFlags; + } + return false; + } + + /* eslint-disable-next-line no-shadow, complexity */ + function isStateChangingModifierKeyEvent(e) + { + var flags = 0; + if (e.type == "keydown") { + flags = keyDownFlags ^ keyUpFlags; + } else if (e.type == "keyup") { + flags = keyUpFlags; + } + switch (e.key) { + case "Shift": + is(e.ctrlKey, (flags & kCtrlFlag) != 0, + currentTestName + ", Ctrl of Shift " + e.type + " event mismatch"); + is(e.metaKey, (flags & kMetaFlag) != 0, + currentTestName + ", Command of Shift " + e.type + " event mismatch"); + is(e.altKey, (flags & kAltFlag) != 0, + currentTestName + ", Alt of Shift " + e.type + " event mismatch"); + is(e.shiftKey, e.type == "keydown", + currentTestName + ", Shift of Shift " + e.type + " event mismatch"); + // AltGr on Windows is always pressed after and released before Shift key operation. + is(e.getModifierState("AltGraph"), (IS_MAC && e.altKey), + currentTestName + ", AltGraph of Shift " + e.type + " event mismatch"); + return (testingEvent.modifiers.shiftKey || testingEvent.modifiers.shiftRightKey) && + removeFlag(e, kShiftFlag) && expectedDOMKeyCode != e.keyCode; + case "Control": + is(e.ctrlKey, e.type == "keydown", + currentTestName + ", Ctrl of Ctrl " + e.type + " event mismatch"); + is(e.metaKey, (flags & kMetaFlag) != 0, + currentTestName + ", Command of Ctrl " + e.type + " event mismatch"); + // When AltGr key is released on Windows, ControlLeft keyup event + // is followed by AltRight keyup event. However, altKey should be + // false in such case. + is(e.altKey, (flags & kAltFlag) != 0 && !(IS_WIN && !!testingEvent.modifiers.altGrKey), + currentTestName + ", Alt of Ctrl " + e.type + " event mismatch"); + is(e.shiftKey, (flags & kShiftFlag) != 0, + currentTestName + ", Shift of Ctrl " + e.type + " event mismatch"); + is(e.getModifierState("AltGraph"), + (IS_WIN && !!testingEvent.modifiers.altGrKey && e.type == "keyup") || (IS_MAC && e.altKey), + currentTestName + ", AltGraph of Ctrl " + e.type + " event mismatch"); + return (testingEvent.modifiers.ctrlKey || testingEvent.modifiers.ctrlRightKey || + (IS_WIN && !!testingEvent.modifiers.altGrKey)) && + removeFlag(e, kCtrlFlag) && expectedDOMKeyCode != e.keyCode; + case "Alt": + is(e.ctrlKey, (flags & kCtrlFlag) != 0 && !(IS_WIN && !!testingEvent.modifiers.altGrKey), + currentTestName + ", Ctrl of Alt " + e.type + " event mismatch"); + is(e.metaKey, (flags & kMetaFlag) != 0, + currentTestName + ", Command of Alt " + e.type + " event mismatch"); + is(e.altKey, e.type == "keydown" && !(IS_WIN && !!testingEvent.modifiers.altGrKey), + currentTestName + ", Alt of Alt " + e.type + " event mismatch"); + is(e.shiftKey, (flags & kShiftFlag) != 0, + currentTestName + ", Shift of Alt " + e.type + " event mismatch"); + is(e.getModifierState("AltGraph"), + e.type == "keydown" && ((IS_WIN && !!testingEvent.modifiers.altGrKey) || (IS_MAC && e.altKey)), + currentTestName + ", AltGraph of Alt " + e.type + " event mismatch"); + return (testingEvent.modifiers.altKey || testingEvent.modifiers.altRightKey || + (IS_WIN && !!testingEvent.modifiers.altGrKey)) && + removeFlag(e, kAltFlag) && expectedDOMKeyCode != e.keyCode; + case "AltGraph": + // On Windows, AltGraph events are fired only when AltRight key is + // pressed when active keyboard layout maps AltGraph to AltRight. + // Note that AltGraph is represented with pressing both Control key + // and Alt key. Therefore, when AltGraph keyboard event is fired, + // both ctrlKey and altKey are always false on Windows. + is(e.ctrlKey, (flags & kCtrlFlag) != 0 && !IS_WIN, + currentTestName + ", Ctrl of AltGraph " + e.type + " event mismatch"); + is(e.metaKey, (flags & kMetaFlag) != 0, + currentTestName + ", Command of AltGraph " + e.type + " event mismatch"); + is(e.altKey, (flags & kAltFlag) != 0 && !IS_WIN, + currentTestName + ", Alt of AltGraph " + e.type + " event mismatch"); + is(e.shiftKey, (flags & kShiftFlag) != 0, + currentTestName + ", Shift of Ctrl " + e.type + " event mismatch"); + is(e.getModifierState("AltGraph"), e.type === "keydown", + currentTestName + ", AltGraph of AltGraph " + e.type + " event mismatch"); + return IS_WIN && testingEvent.modifiers.altGrKey && + removeFlag(e, kAltGraphFlag) && expectedDOMKeyCode != e.keyCode; + case "Meta": + is(e.ctrlKey, (flags & kCtrlFlag) != 0, + currentTestName + ", Ctrl of Command " + e.type + " event mismatch"); + is(e.metaKey, e.type == "keydown", + currentTestName + ", Command of Command " + e.type + " event mismatch"); + is(e.altKey, (flags & kAltFlag) != 0, + currentTestName + ", Alt of Command " + e.type + " event mismatch"); + is(e.shiftKey, (flags & kShiftFlag) != 0, + currentTestName + ", Shift of Command " + e.type + " event mismatch"); + is(e.getModifierState("AltGraph"), + (IS_WIN && (flags & kAltGraphFlag) != 0) || (IS_MAC && e.altKey), + currentTestName + ", AltGraph of Meta " + e.type + " event mismatch"); + return (testingEvent.modifiers.metaKey || testingEvent.modifiers.metaRightKey) && + removeFlag(e, kMetaFlag) && expectedDOMKeyCode != e.keyCode; + case "NumLock": + is(e.ctrlKey, (flags & kCtrlFlag) != 0, + currentTestName + ", Ctrl of NumLock " + e.type + " event mismatch"); + is(e.metaKey, (flags & kMetaFlag) != 0, + currentTestName + ", Command of NumLock " + e.type + " event mismatch"); + is(e.altKey, (flags & kAltFlag) != 0, + currentTestName + ", Alt of NumLock " + e.type + " event mismatch"); + is(e.shiftKey, (flags & kShiftFlag) != 0, + currentTestName + ", Shift of NumLock " + e.type + " event mismatch"); + is(e.getModifierState("AltGraph"), false, + currentTestName + ", AltGraph of NumLock " + e.type + " event mismatch"); + // AltGr on Windows is always pressed after and released before NumLock key operation. + return (testingEvent.modifiers.numLockKey || testingEvent.modifiers.numericKeyPadKey) && + removeFlag(e, kNumLockFlag) && expectedDOMKeyCode != e.keyCode; + case "CapsLock": + is(e.ctrlKey, (flags & kCtrlFlag) != 0, + currentTestName + ", Ctrl of CapsLock " + e.type + " event mismatch"); + is(e.metaKey, (flags & kMetaFlag) != 0, + currentTestName + ", Command of CapsLock " + e.type + " event mismatch"); + is(e.altKey, (flags & kAltFlag) != 0, + currentTestName + ", Alt of CapsLock " + e.type + " event mismatch"); + is(e.shiftKey, (flags & kShiftFlag) != 0, + currentTestName + ", Shift of CapsLock " + e.type + " event mismatch"); + // AltGr on Windows is always pressed after and released before CapsLock key operation. + is(e.getModifierState("AltGraph"), false, + currentTestName + ", AltGraph of CapsLock " + e.type + " event mismatch"); + return testingEvent.modifiers.capsLockKey && + removeFlag(e, kCapsLockFlag) && expectedDOMKeyCode != e.keyCode; + } + return false; + } + + // Ignore the state changing key events which is fired by the testing event. + if (!isStateChangingModifierKeyEvent(e)) + eventList.push(e); + } + + function consumer(aEvent) + { + aEvent.preventDefault(); + } + + const SHOULD_DELIVER_KEYDOWN = 0x1; + const SHOULD_DELIVER_KEYPRESS = 0x2; + const SHOULD_DELIVER_KEYUP = 0x4; + const SHOULD_DELIVER_ALL = SHOULD_DELIVER_KEYDOWN | + SHOULD_DELIVER_KEYPRESS | + SHOULD_DELIVER_KEYUP; + const SHOULD_DELIVER_KEYDOWN_KEYUP = SHOULD_DELIVER_KEYDOWN | + SHOULD_DELIVER_KEYUP; + const SHOULD_DELIVER_KEYDOWN_KEYPRESS = SHOULD_DELIVER_KEYDOWN | + SHOULD_DELIVER_KEYPRESS; + + // The first parameter is the complete input event. The second parameter is + // what to test against. The third parameter is which key events should be + // delived for the event. + // @param aExpectedKeyValues Can be string or array of string. + // If all keyboard events have same key value, + // specify it as string. Otherwise, specify + // each key value in array. + function testKey(aEvent, aExpectedKeyValues, aExpectedCodeValue, + aExpectedGeckoKeyCode, aExpectGeckoChar, + aShouldDelivedEvent, aExpectLocation) + { + ok(aExpectedGeckoKeyCode != undefined, "keycode is undefined"); + eventList = []; + + // The modifier key events which are fired for state changing are har to + // test. We should ignore them for now. + keyDownFlags = keyUpFlags = 0; + if (!IS_MAC) { + // On Mac, nsChildView doesn't generate modifier keydown/keyup events for + // state changing for synthesizeNativeKeyEvent. + if (aEvent.modifiers.shiftKey || aEvent.modifiers.shiftRightKey) { + keyDownFlags |= kShiftFlag; + } + if (aEvent.modifiers.ctrlKey || aEvent.modifiers.ctrlRightKey || + (IS_WIN && aEvent.modifiers.altGrKey)) { + keyDownFlags |= kCtrlFlag; + } + if (aEvent.modifiers.altKey || aEvent.modifiers.altRightKey) { + keyDownFlags |= kAltFlag; + } + if (aEvent.modifiers.altGrKey) { + keyDownFlags |= kAltGraphFlag; + } + if (aEvent.modifiers.metaKey || aEvent.modifiers.metaRightKey) { + keyDownFlags |= kMetaFlag; + } + if (aEvent.modifiers.numLockKey || aEvent.modifiers.numericKeyPadKey) { + keyDownFlags |= kNumLockFlag; + } + if (aEvent.modifiers.capsLockKey) { + keyDownFlags |= kCapsLockFlag; + } + keyUpFlags = keyDownFlags; + } + + testingEvent = aEvent; + expectedDOMKeyCode = aExpectedGeckoKeyCode; + + currentTestName = eventToString(aEvent); + ok(true, "Starting: " + currentTestName); + + // eslint-disable-next-line complexity + return synthesizeKey(aEvent, "button", function() { + + var expectEventTypeList = []; + if (aShouldDelivedEvent & SHOULD_DELIVER_KEYDOWN) + expectEventTypeList.push("keydown"); + if (aShouldDelivedEvent & SHOULD_DELIVER_KEYPRESS) { + expectEventTypeList.push("keypress"); + for (let i = 1; i < aExpectGeckoChar.length; i++) { + expectEventTypeList.push("keypress"); + } + } + if (aShouldDelivedEvent & SHOULD_DELIVER_KEYUP) + expectEventTypeList.push("keyup"); + is(eventList.length, expectEventTypeList.length, + currentTestName + ", wrong number of key events"); + + var longerLength = Math.max(eventList.length, expectEventTypeList.length); + var keypressCount = 0; + for (let i = 0; i < longerLength; i++) { + var firedEventType = i < eventList.length ? eventList[i].type : ""; + var expectEventType = i < expectEventTypeList.length ? expectEventTypeList[i] : ""; + if (firedEventType != "") { + is(firedEventType, expectEventType, + currentTestName + ", " + expectEventType + " should be fired"); + } else { + is(firedEventType, expectEventType, + currentTestName + ", a needed event is not fired"); + } + + if (firedEventType != "") { + var expectedKeyValue = + // eslint-disable-next-line no-nested-ternary + typeof aExpectedKeyValues === "string" ? aExpectedKeyValues : + i < aExpectedKeyValues.length ? aExpectedKeyValues[i] : + undefined; + + var e = eventList[i]; + switch (e.key) { + case "Shift": + case "Control": + case "Alt": + case "AltGraph": + case "Meta": + case "CapsLock": + case "NumLock": + // XXX To check modifier state of modifiers, we need to check + // e.type since modifier key may change modifier state. + // However, doing it makes the following check more + // complicated. So, we ignore the modifier state of + // modifier keydown/keyup events for now. + break; + default: + is(e.shiftKey, !!(aEvent.modifiers.shiftKey || aEvent.modifiers.shiftRightKey), + currentTestName + ", Shift of " + e.type + " of " + e.code + " mismatch"); + is(e.metaKey, !!(aEvent.modifiers.metaKey || aEvent.modifiers.metaRightKey), + currentTestName + ", Meta of " + e.type + " of " + e.code + " mismatch"); + var isControlPressed = !!(aEvent.modifiers.ctrlKey || aEvent.modifiers.ctrlRightKey); + var isAltPressed = !!(aEvent.modifiers.altKey || aEvent.modifiers.altRightKey); + var isAltGraphExpected = + !!aEvent.modifiers.altGrKey || + (IS_WIN && aEvent.layout.hasAltGrOnWin && + isControlPressed && isAltPressed && + (aEvent.isInputtingCharacters || expectedKeyValue == "Dead")) || + (IS_MAC && isAltPressed); + var isControlExpected = !(IS_WIN && isAltGraphExpected) && isControlPressed; + var isAltExpected = !(IS_WIN && isAltGraphExpected) && isAltPressed; + if (e.type == "keypress" && aEvent.isInputtingCharacters) { + isControlExpected = false; + isAltExpected = false; + } + is(e.ctrlKey, isControlExpected, + currentTestName + ", Ctrl of " + e.type + " of " + e.code + " mismatch"); + is(e.altKey, isAltExpected, + currentTestName + ", Alt of " + e.type + " of " + e.code + " mismatch"); + is(e.getModifierState("AltGraph"), isAltGraphExpected, + currentTestName + ", AltGraph of " + e.type + " of " + e.code + " mismatch"); + break; + } + + is(e.key, expectedKeyValue, currentTestName + ", wrong key value"); + is(e.code, aExpectedCodeValue, currentTestName + ", wrong code value"); + + if (aExpectGeckoChar.length && e.type == "keypress") { + is(e.charCode, aExpectGeckoChar.charCodeAt(keypressCount++), + currentTestName + ", charcode"); + if (aExpectedGeckoKeyCode >= 0) { + if (aExpectGeckoChar) { + is(e.keyCode, 0, + currentTestName + ", wrong keycode"); + } else { + is(e.keyCode, aExpectedGeckoKeyCode, + currentTestName + ", wrong keycode"); + } + } + } else { + is(e.charCode, 0, + currentTestName + ", no charcode"); + if (aExpectedGeckoKeyCode >= 0) { + is(e.keyCode, aExpectedGeckoKeyCode, + currentTestName + ", wrong keycode"); + } + } + is(e.location, aExpectLocation, + currentTestName + ", wrong location"); + } + } + + continueTest(); + }); + } + + // These tests have to be per-plaform. + document.addEventListener("keydown", onKeyEvent); + document.addEventListener("keypress", onKeyEvent); + document.addEventListener("keyup", onKeyEvent); + // Prevent almost all shortcut key handlers. + SpecialPowers.addSystemEventListener(document, "keypress", consumer, true); + + function cleanup() + { + document.removeEventListener("keydown", onKeyEvent); + document.removeEventListener("keypress", onKeyEvent); + document.removeEventListener("keyup", onKeyEvent); + SpecialPowers.removeSystemEventListener(document, "keypress", consumer, true); + } + + function* testKeysOnMac() + { + // On Mac, you can produce event records for any desired keyboard input + // by running with NSPR_LOG_MODULES=TextInputHandlerWidgets:5 and typing + // into the browser. We will dump the key event fields to the console + // (Find TextInputHandler::HandleKeyDownEvent or + // TextInputHandler::HandleKeyUpEvent in the log). Use the International system + // preferences widget to enable other keyboard layouts and select them from the + // input menu to see what keyboard events they generate. + // Note that it's possible to send bogus key events here, e.g. + // {keyCode:0, chars:"z", unmodifiedChars:"P"} --- sendNativeKeyEvent + // makes no attempt to verify that the keyCode matches the characters. So only + // test key event records that you saw Cocoa send. + + // Command keys + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1}, chars:"a", unmodifiedChars:"a"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Shift-cmd gives us the shifted character + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1, shiftKey:1}, chars:"a", unmodifiedChars:"A"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Ctrl-cmd gives us the unshifted character + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1, ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Alt-cmd gives us the shifted character + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1, altKey:1}, chars:"\u00e5", unmodifiedChars:"a"}, + "\u00e5", "KeyA", KeyboardEvent.DOM_VK_A, "\u00e5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1, altKey:1, shiftKey:1}, chars:"\u00c5", unmodifiedChars:"a"}, + "\u00c5", "KeyA", KeyboardEvent.DOM_VK_A, "\u00c5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Greek ctrl keys produce Latin charcodes + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"}, + "\u03b1", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"\u0391"}, + "\u0391", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Greek command keys + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1}, chars:"a", unmodifiedChars:"\u03b1"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Shift-cmd gives us the shifted character + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1, shiftKey:1}, chars:"a", unmodifiedChars:"\u0391"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Ctrl-cmd gives us the unshifted character + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1, ctrlKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"}, + "\u03b1", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Alt-cmd gives us the shifted character + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1, altKey:1}, chars:"\u00a8", unmodifiedChars:"\u03b1"}, + "\u00a8", "KeyA", KeyboardEvent.DOM_VK_A, "\u00a8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1, altKey:1, shiftKey:1}, chars:"\u00b9", unmodifiedChars:"\u0391"}, + "\u00b9", "KeyA", KeyboardEvent.DOM_VK_A, "\u00b9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // German + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_A, + modifiers: {}, chars:"a", unmodifiedChars:"a"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers: {}, chars:"\u00fc", unmodifiedChars:"\u00fc"}, + "\u00fc", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u00fc", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_Minus, + modifiers: {}, chars:"\u00df", unmodifiedChars:"\u00df"}, + "\u00df", "Minus", KeyboardEvent.DOM_VK_QUESTION_MARK, "\u00df", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_Minus, + modifiers:{shiftKey:1}, chars:"?", unmodifiedChars:"?"}, + "?", "Minus", KeyboardEvent.DOM_VK_QUESTION_MARK, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Note that Shift+SS is '?' but Cmd+Shift+SS is '/' on German layout. + // Therefore, when Cmd key is pressed, the SS key's keycode is changed. + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_Minus, + modifiers:{metaKey:1}, chars:"\u00df", unmodifiedChars:"\u00df"}, + "\u00df", "Minus", KeyboardEvent.DOM_VK_SLASH, "\u00df", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_Minus, + modifiers:{metaKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"?"}, + "/", "Minus", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Caps Lock key event + // XXX keyup event of Caps Lock key is not fired. + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_CapsLock, + modifiers:{capsLockKey:1}, chars:"", unmodifiedChars:""}, + "CapsLock", "CapsLock", KeyboardEvent.DOM_VK_CAPS_LOCK, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_CapsLock, + modifiers:{capsLockKey:0}, chars:"", unmodifiedChars:""}, + "CapsLock", "CapsLock", KeyboardEvent.DOM_VK_CAPS_LOCK, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Shift/RightShift key event + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Shift, + modifiers:{shiftKey:1}, chars:"", unmodifiedChars:""}, + "Shift", "ShiftLeft", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Shift, + modifiers:{shiftKey:0}, chars:"", unmodifiedChars:""}, + "Shift", "ShiftLeft", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightShift, + modifiers:{shiftRightKey:1}, chars:"", unmodifiedChars:""}, + "Shift", "ShiftRight", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightShift, + modifiers:{shiftRightKey:0}, chars:"", unmodifiedChars:""}, + "Shift", "ShiftRight", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + + // Control/RightControl key event + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Control, + modifiers:{ctrlKey:1}, chars:"", unmodifiedChars:""}, + "Control", "ControlLeft", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Control, + modifiers:{ctrlKey:0}, chars:"", unmodifiedChars:""}, + "Control", "ControlLeft", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightControl, + modifiers:{ctrlRightKey:1}, chars:"", unmodifiedChars:""}, + "Control", "ControlRight", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightControl, + modifiers:{ctrlRightKey:0}, chars:"", unmodifiedChars:""}, + "Control", "ControlRight", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + + // Option/RightOption key event + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Option, + modifiers:{altKey:1}, chars:"", unmodifiedChars:""}, + "Alt", "AltLeft", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Option, + modifiers:{altKey:0}, chars:"", unmodifiedChars:""}, + "Alt", "AltLeft", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightOption, + modifiers:{altRightKey:1}, chars:"", unmodifiedChars:""}, + "Alt", "AltRight", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightOption, + modifiers:{altRightKey:0}, chars:"", unmodifiedChars:""}, + "Alt", "AltRight", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + + // Command/RightCommand key event + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Command, + modifiers:{metaKey:1}, chars:"", unmodifiedChars:""}, + "Meta", "MetaLeft", KeyboardEvent.DOM_VK_META, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Command, + modifiers:{metaKey:0}, chars:"", unmodifiedChars:""}, + "Meta", "MetaLeft", KeyboardEvent.DOM_VK_META, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightCommand, + modifiers:{metaRightKey:1}, chars:"", unmodifiedChars:""}, + "Meta", "MetaRight", KeyboardEvent.DOM_VK_META, "", SHOULD_DELIVER_KEYDOWN, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightCommand, + modifiers:{metaRightKey:0}, chars:"", unmodifiedChars:""}, + "Meta", "MetaRight", KeyboardEvent.DOM_VK_META, "", SHOULD_DELIVER_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + + // all keys on keyboard (keyCode test) + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Tab, + modifiers: {}, chars:"\t", unmodifiedChars:"\t"}, + "Tab", "Tab", KeyboardEvent.DOM_VK_TAB, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadClear, + modifiers: {}, chars:"\uF739", unmodifiedChars:"\uF739"}, + "Clear", "NumLock", KeyboardEvent.DOM_VK_CLEAR, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Return, + modifiers: {}, chars:"\u000D", unmodifiedChars:"\u000D"}, + "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_Pause, + modifiers: {}, chars:"\uF712", unmodifiedChars:"\uF712"}, + "F15", "F15", KeyboardEvent.DOM_VK_PAUSE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Escape, + modifiers: {}, chars:"\u001B", unmodifiedChars:"\u001B"}, + "Escape", "Escape", KeyboardEvent.DOM_VK_ESCAPE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Space, + modifiers: {}, chars:" ", unmodifiedChars:" "}, + " ", "Space", KeyboardEvent.DOM_VK_SPACE, " ", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PageUp, + modifiers: {}, chars:"\uF72C", unmodifiedChars:"\uF72C"}, + "PageUp", "PageUp", KeyboardEvent.DOM_VK_PAGE_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PageDown, + modifiers: {}, chars:"\uF72D", unmodifiedChars:"\uF72D"}, + "PageDown", "PageDown", KeyboardEvent.DOM_VK_PAGE_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_End, + modifiers: {}, chars:"\uF72B", unmodifiedChars:"\uF72B"}, + "End", "End", KeyboardEvent.DOM_VK_END, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_Home, + modifiers: {}, chars:"\uF729", unmodifiedChars:"\uF729"}, + "Home", "Home", KeyboardEvent.DOM_VK_HOME, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_LeftArrow, + modifiers: {}, chars:"\uF702", unmodifiedChars:"\uF702"}, + "ArrowLeft", "ArrowLeft", KeyboardEvent.DOM_VK_LEFT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_UpArrow, + modifiers: {}, chars:"\uF700", unmodifiedChars:"\uF700"}, + "ArrowUp", "ArrowUp", KeyboardEvent.DOM_VK_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_RightArrow, + modifiers: {}, chars:"\uF703", unmodifiedChars:"\uF703"}, + "ArrowRight", "ArrowRight", KeyboardEvent.DOM_VK_RIGHT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_DownArrow, + modifiers: {}, chars:"\uF701", unmodifiedChars:"\uF701"}, + "ArrowDown", "ArrowDown", KeyboardEvent.DOM_VK_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_PrintScreen, + modifiers: {}, chars:"\uF710", unmodifiedChars:"\uF710"}, + "F13", "F13", KeyboardEvent.DOM_VK_PRINTSCREEN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_Delete, + modifiers: {}, chars:"\uF728", unmodifiedChars:"\uF728"}, + "Delete", "Delete", KeyboardEvent.DOM_VK_DELETE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_ScrollLock, + modifiers: {}, chars:"\uF711", unmodifiedChars:"\uF711"}, + "F14", "F14", KeyboardEvent.DOM_VK_SCROLL_LOCK, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_PC_ContextMenu, + modifiers: {}, chars:"\u0010", unmodifiedChars:"\u0010"}, + "ContextMenu", "ContextMenu", KeyboardEvent.DOM_VK_CONTEXT_MENU, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F1, + modifiers:{fnKey:1}, chars:"\uF704", unmodifiedChars:"\uF704"}, + "F1", "F1", KeyboardEvent.DOM_VK_F1, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F2, + modifiers:{fnKey:1}, chars:"\uF705", unmodifiedChars:"\uF705"}, + "F2", "F2", KeyboardEvent.DOM_VK_F2, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F3, + modifiers:{fnKey:1}, chars:"\uF706", unmodifiedChars:"\uF706"}, + "F3", "F3", KeyboardEvent.DOM_VK_F3, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F4, + modifiers:{fnKey:1}, chars:"\uF707", unmodifiedChars:"\uF707"}, + "F4", "F4", KeyboardEvent.DOM_VK_F4, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F5, + modifiers:{fnKey:1}, chars:"\uF708", unmodifiedChars:"\uF708"}, + "F5", "F5", KeyboardEvent.DOM_VK_F5, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F6, + modifiers:{fnKey:1}, chars:"\uF709", unmodifiedChars:"\uF709"}, + "F6", "F6", KeyboardEvent.DOM_VK_F6, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F7, + modifiers:{fnKey:1}, chars:"\uF70A", unmodifiedChars:"\uF70A"}, + "F7", "F7", KeyboardEvent.DOM_VK_F7, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F8, + modifiers:{fnKey:1}, chars:"\uF70B", unmodifiedChars:"\uF70B"}, + "F8", "F8", KeyboardEvent.DOM_VK_F8, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F9, + modifiers:{fnKey:1}, chars:"\uF70C", unmodifiedChars:"\uF70C"}, + "F9", "F9", KeyboardEvent.DOM_VK_F9, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F10, + modifiers:{fnKey:1}, chars:"\uF70D", unmodifiedChars:"\uF70D"}, + "F10", "F10", KeyboardEvent.DOM_VK_F10, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F11, + modifiers:{fnKey:1}, chars:"\uF70E", unmodifiedChars:"\uF70E"}, + "F11", "F11", KeyboardEvent.DOM_VK_F11, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F12, + modifiers:{fnKey:1}, chars:"\uF70F", unmodifiedChars:"\uF70F"}, + "F12", "F12", KeyboardEvent.DOM_VK_F12, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F16, + modifiers:{fnKey:1}, chars:"\uF713", unmodifiedChars:"\uF713"}, + "F16", "F16", KeyboardEvent.DOM_VK_F16, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F17, + modifiers:{fnKey:1}, chars:"\uF714", unmodifiedChars:"\uF714"}, + "F17", "F17", KeyboardEvent.DOM_VK_F17, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F18, + modifiers:{fnKey:1}, chars:"\uF715", unmodifiedChars:"\uF715"}, + "F18", "F18", KeyboardEvent.DOM_VK_F18, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_F19, + modifiers:{fnKey:1}, chars:"\uF716", unmodifiedChars:"\uF716"}, + "F19", "F19", KeyboardEvent.DOM_VK_F19, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // US + // Alphabet + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers: {}, chars:"a", unmodifiedChars:"a"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{shiftKey:1}, chars:"A", unmodifiedChars:"A"}, + "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"A"}, + "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, capsLockKey:1}, chars:"\u0001", unmodifiedChars:"a"}, + "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, shiftKey:1, capsLockKey:1}, chars:"\u0001", unmodifiedChars:"A"}, + "A", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{altKey:1}, chars:"\u00E5", unmodifiedChars:"a"}, + "\u00E5", "KeyA", KeyboardEvent.DOM_VK_A, "\u00E5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00C5", unmodifiedChars:"A"}, + "\u00C5", "KeyA", KeyboardEvent.DOM_VK_A, "\u00C5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"}, + "\u00E5", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"A"}, + "\u00C5", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{metaKey:1}, chars:"a", unmodifiedChars:"a"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B, + modifiers:{}, chars:"b", unmodifiedChars:"b"}, + "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B, + modifiers:{shiftKey:1}, chars:"B", unmodifiedChars:"B"}, + "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B, + modifiers:{ctrlKey:1}, chars:"\u0002", unmodifiedChars:"b"}, + "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0002", unmodifiedChars:"B"}, + "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B, + modifiers:{altKey:1}, chars:"\u222B", unmodifiedChars:"b"}, + "\u222B", "KeyB", KeyboardEvent.DOM_VK_B, "\u222B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B, + modifiers:{altKey:1, shiftKey:1}, chars:"\u0131", unmodifiedChars:"B"}, + "\u0131", "KeyB", KeyboardEvent.DOM_VK_B, "\u0131", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0002", unmodifiedChars:"b"}, + "\u222B", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0002", unmodifiedChars:"B"}, + "\u0131", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_B, + modifiers:{metaKey:1}, chars:"b", unmodifiedChars:"b"}, + "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C, + modifiers:{}, chars:"c", unmodifiedChars:"c"}, + "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C, + modifiers:{shiftKey:1}, chars:"C", unmodifiedChars:"C"}, + "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C, + modifiers:{ctrlKey:1}, chars:"\u0003", unmodifiedChars:"c"}, + "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"C"}, + "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C, + modifiers:{altKey:1}, chars:"\u00E7", unmodifiedChars:"c"}, + "\u00E7", "KeyC", KeyboardEvent.DOM_VK_C, "\u00E7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00C7", unmodifiedChars:"C"}, + "\u00C7", "KeyC", KeyboardEvent.DOM_VK_C, "\u00C7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0003", unmodifiedChars:"c"}, + "\u00E7", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"C"}, + "\u00C7", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_C, + modifiers:{metaKey:1}, chars:"c", unmodifiedChars:"c"}, + "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D, + modifiers:{}, chars:"d", unmodifiedChars:"d"}, + "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D, + modifiers:{shiftKey:1}, chars:"D", unmodifiedChars:"D"}, + "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D, + modifiers:{ctrlKey:1}, chars:"\u0004", unmodifiedChars:"d"}, + "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0004", unmodifiedChars:"D"}, + "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D, + modifiers:{altKey:1}, chars:"\u2202", unmodifiedChars:"d"}, + "\u2202", "KeyD", KeyboardEvent.DOM_VK_D, "\u2202", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00CE", unmodifiedChars:"D"}, + "\u00CE", "KeyD", KeyboardEvent.DOM_VK_D, "\u00CE", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0004", unmodifiedChars:"d"}, + "\u2202", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0004", unmodifiedChars:"D"}, + "\u00CE", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_D, + modifiers:{metaKey:1}, chars:"d", unmodifiedChars:"d"}, + "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E, + modifiers:{}, chars:"e", unmodifiedChars:"e"}, + "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E, + modifiers:{shiftKey:1}, chars:"E", unmodifiedChars:"E"}, + "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E, + modifiers:{ctrlKey:1}, chars:"\u0005", unmodifiedChars:"e"}, + "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0005", unmodifiedChars:"E"}, + "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E, + modifiers:{altKey:1}, chars:"", unmodifiedChars:"e"}, + "Dead", "KeyE", KeyboardEvent.DOM_VK_E, "\u00B4", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00B4", unmodifiedChars:"E"}, + "\u00B4", "KeyE", KeyboardEvent.DOM_VK_E, "\u00B4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0005", unmodifiedChars:"e"}, + "Dead", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0005", unmodifiedChars:"E"}, + "\u00B4", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_E, + modifiers:{metaKey:1}, chars:"e", unmodifiedChars:"e"}, + "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + modifiers:{}, chars:"f", unmodifiedChars:"f"}, + "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + modifiers:{shiftKey:1}, chars:"F", unmodifiedChars:"F"}, + "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + modifiers:{ctrlKey:1}, chars:"\u0006", unmodifiedChars:"f"}, + "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0006", unmodifiedChars:"F"}, + "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + modifiers:{altKey:1}, chars:"\u0192", unmodifiedChars:"f"}, + "\u0192", "KeyF", KeyboardEvent.DOM_VK_F, "\u0192", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00CF", unmodifiedChars:"F"}, + "\u00CF", "KeyF", KeyboardEvent.DOM_VK_F, "\u00CF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0006", unmodifiedChars:"f"}, + "\u0192", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0006", unmodifiedChars:"F"}, + "\u00CF", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // XXX This test starts fullscreen mode. + // yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + // modifiers:{metaKey:1}, chars:"f", unmodifiedChars:"f"}, + // "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G, + modifiers:{}, chars:"g", unmodifiedChars:"g"}, + "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G, + modifiers:{shiftKey:1}, chars:"G", unmodifiedChars:"G"}, + "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G, + modifiers:{ctrlKey:1}, chars:"\u0007", unmodifiedChars:"g"}, + "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0007", unmodifiedChars:"G"}, + "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G, + modifiers:{altKey:1}, chars:"\u00A9", unmodifiedChars:"g"}, + "\u00A9", "KeyG", KeyboardEvent.DOM_VK_G, "\u00A9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G, + modifiers:{altKey:1, shiftKey:1}, chars:"\u02DD", unmodifiedChars:"G"}, + "\u02DD", "KeyG", KeyboardEvent.DOM_VK_G, "\u02DD", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0007", unmodifiedChars:"g"}, + "\u00A9", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0007", unmodifiedChars:"G"}, + "\u02DD", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_G, + modifiers:{metaKey:1}, chars:"g", unmodifiedChars:"g"}, + "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H, + modifiers:{}, chars:"h", unmodifiedChars:"h"}, + "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H, + modifiers:{shiftKey:1}, chars:"H", unmodifiedChars:"H"}, + "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H, + modifiers:{ctrlKey:1}, chars:"\u0008", unmodifiedChars:"h"}, + "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0008", unmodifiedChars:"H"}, + "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H, + modifiers:{altKey:1}, chars:"\u02D9", unmodifiedChars:"h"}, + "\u02D9", "KeyH", KeyboardEvent.DOM_VK_H, "\u02D9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00D3", unmodifiedChars:"H"}, + "\u00D3", "KeyH", KeyboardEvent.DOM_VK_H, "\u00D3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0008", unmodifiedChars:"h"}, + "\u02D9", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0008", unmodifiedChars:"H"}, + "\u00D3", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_H, + modifiers:{metaKey:1}, chars:"h", unmodifiedChars:"h"}, + "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I, + modifiers:{}, chars:"i", unmodifiedChars:"i"}, + "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I, + modifiers:{shiftKey:1}, chars:"I", unmodifiedChars:"I"}, + "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I, + modifiers:{ctrlKey:1}, chars:"\u0009", unmodifiedChars:"i"}, + "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0009", unmodifiedChars:"I"}, + "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I, + modifiers:{altKey:1}, chars:"", unmodifiedChars:"i"}, + "Dead", "KeyI", KeyboardEvent.DOM_VK_I, "\u02C6", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I, + modifiers:{altKey:1, shiftKey:1}, chars:"\u02C6", unmodifiedChars:"I"}, + "\u02C6", "KeyI", KeyboardEvent.DOM_VK_I, "\u02C6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0009", unmodifiedChars:"i"}, + "Dead", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0009", unmodifiedChars:"I"}, + "\u02C6", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // XXX This test causes memory leak. + // yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_I, + // modifiers:{metaKey:1}, chars:"i", unmodifiedChars:"i"}, + // "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J, + modifiers:{}, chars:"j", unmodifiedChars:"j"}, + "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J, + modifiers:{shiftKey:1}, chars:"J", unmodifiedChars:"J"}, + "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J, + modifiers:{ctrlKey:1}, chars:"\u000A", unmodifiedChars:"j"}, + "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000A", unmodifiedChars:"J"}, + "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J, + modifiers:{altKey:1}, chars:"\u2206", unmodifiedChars:"j"}, + "\u2206", "KeyJ", KeyboardEvent.DOM_VK_J, "\u2206", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00D4", unmodifiedChars:"J"}, + "\u00D4", "KeyJ", KeyboardEvent.DOM_VK_J, "\u00D4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u000A", unmodifiedChars:"j"}, + "\u2206", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000A", unmodifiedChars:"J"}, + "\u00D4", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_J, + modifiers:{metaKey:1}, chars:"j", unmodifiedChars:"j"}, + "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K, + modifiers:{}, chars:"k", unmodifiedChars:"k"}, + "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K, + modifiers:{shiftKey:1}, chars:"K", unmodifiedChars:"K"}, + "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K, + modifiers:{ctrlKey:1}, chars:"\u000B", unmodifiedChars:"k"}, + "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000B", unmodifiedChars:"K"}, + "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K, + modifiers:{altKey:1}, chars:"\u02DA", unmodifiedChars:"k"}, + "\u02DA", "KeyK", KeyboardEvent.DOM_VK_K, "\u02DA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K, + modifiers:{altKey:1, shiftKey:1}, chars:"\uF8FF", unmodifiedChars:"K"}, + "\uF8FF", "KeyK", KeyboardEvent.DOM_VK_K, "\uF8FF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u000B", unmodifiedChars:"k"}, + "\u02DA", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000B", unmodifiedChars:"K"}, + "\uF8FF", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_K, + modifiers:{metaKey:1}, chars:"k", unmodifiedChars:"k"}, + "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L, + modifiers:{}, chars:"l", unmodifiedChars:"l"}, + "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L, + modifiers:{shiftKey:1}, chars:"L", unmodifiedChars:"L"}, + "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L, + modifiers:{ctrlKey:1}, chars:"\u000C", unmodifiedChars:"l"}, + "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000C", unmodifiedChars:"L"}, + "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L, + modifiers:{altKey:1}, chars:"\u00AC", unmodifiedChars:"l"}, + "\u00AC", "KeyL", KeyboardEvent.DOM_VK_L, "\u00AC", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00D2", unmodifiedChars:"L"}, + "\u00D2", "KeyL", KeyboardEvent.DOM_VK_L, "\u00D2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u000C", unmodifiedChars:"l"}, + "\u00AC", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000C", unmodifiedChars:"L"}, + "\u00D2", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_L, + modifiers:{metaKey:1}, chars:"l", unmodifiedChars:"l"}, + "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M, + modifiers:{}, chars:"m", unmodifiedChars:"m"}, + "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M, + modifiers:{shiftKey:1}, chars:"M", unmodifiedChars:"M"}, + "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M, + modifiers:{ctrlKey:1}, chars:"\u000D", unmodifiedChars:"m"}, + "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000D", unmodifiedChars:"M"}, + "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M, + modifiers:{altKey:1}, chars:"\u00B5", unmodifiedChars:"m"}, + "\u00B5", "KeyM", KeyboardEvent.DOM_VK_M, "\u00B5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00C2", unmodifiedChars:"M"}, + "\u00C2", "KeyM", KeyboardEvent.DOM_VK_M, "\u00C2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u000D", unmodifiedChars:"m"}, + "\u00B5", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000D", unmodifiedChars:"M"}, + "\u00C2", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_M, + modifiers:{metaKey:1}, chars:"m", unmodifiedChars:"m"}, + "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N, + modifiers:{}, chars:"n", unmodifiedChars:"n"}, + "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N, + modifiers:{shiftKey:1}, chars:"N", unmodifiedChars:"N"}, + "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N, + modifiers:{ctrlKey:1}, chars:"\u000E", unmodifiedChars:"n"}, + "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000E", unmodifiedChars:"N"}, + "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N, + modifiers:{altKey:1}, chars:"", unmodifiedChars:"n"}, + "Dead", "KeyN", KeyboardEvent.DOM_VK_N, "\u02DC", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N, + modifiers:{altKey:1, shiftKey:1}, chars:"\u02DC", unmodifiedChars:"N"}, + "\u02DC", "KeyN", KeyboardEvent.DOM_VK_N, "\u02DC", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u000E", unmodifiedChars:"n"}, + "Dead", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000E", unmodifiedChars:"N"}, + "\u02DC", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_N, + modifiers:{metaKey:1}, chars:"n", unmodifiedChars:"n"}, + "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O, + modifiers:{}, chars:"o", unmodifiedChars:"o"}, + "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O, + modifiers:{shiftKey:1}, chars:"O", unmodifiedChars:"O"}, + "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O, + modifiers:{ctrlKey:1}, chars:"\u000F", unmodifiedChars:"o"}, + "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000F", unmodifiedChars:"O"}, + "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O, + modifiers:{altKey:1}, chars:"\u00F8", unmodifiedChars:"o"}, + "\u00F8", "KeyO", KeyboardEvent.DOM_VK_O, "\u00F8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00D8", unmodifiedChars:"O"}, + "\u00D8", "KeyO", KeyboardEvent.DOM_VK_O, "\u00D8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u000F", unmodifiedChars:"o"}, + "\u00F8", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u000F", unmodifiedChars:"O"}, + "\u00D8", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_O, + modifiers:{metaKey:1}, chars:"o", unmodifiedChars:"o"}, + "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P, + modifiers:{}, chars:"p", unmodifiedChars:"p"}, + "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P, + modifiers:{shiftKey:1}, chars:"P", unmodifiedChars:"P"}, + "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P, + modifiers:{ctrlKey:1}, chars:"\u0010", unmodifiedChars:"p"}, + "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0010", unmodifiedChars:"P"}, + "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P, + modifiers:{altKey:1}, chars:"\u03C0", unmodifiedChars:"p"}, + "\u03C0", "KeyP", KeyboardEvent.DOM_VK_P, "\u03C0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P, + modifiers:{altKey:1, shiftKey:1}, chars:"\u220F", unmodifiedChars:"P"}, + "\u220F", "KeyP", KeyboardEvent.DOM_VK_P, "\u220F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0010", unmodifiedChars:"p"}, + "\u03C0", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0010", unmodifiedChars:"P"}, + "\u220F", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // XXX This test starts private browsing mode (stopped at the confirmation dialog) + // yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_P, + // modifiers:{metaKey:1}, chars:"p", unmodifiedChars:"p"}, + // "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q, + modifiers:{}, chars:"q", unmodifiedChars:"q"}, + "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q, + modifiers:{shiftKey:1}, chars:"Q", unmodifiedChars:"Q"}, + "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q, + modifiers:{ctrlKey:1}, chars:"\u0011", unmodifiedChars:"q"}, + "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0011", unmodifiedChars:"Q"}, + "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q, + modifiers:{altKey:1}, chars:"\u0153", unmodifiedChars:"q"}, + "\u0153", "KeyQ", KeyboardEvent.DOM_VK_Q, "\u0153", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q, + modifiers:{altKey:1, shiftKey:1}, chars:"\u0152", unmodifiedChars:"Q"}, + "\u0152", "KeyQ", KeyboardEvent.DOM_VK_Q, "\u0152", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0011", unmodifiedChars:"q"}, + "\u0153", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0011", unmodifiedChars:"Q"}, + "\u0152", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Q, + modifiers:{metaKey:1}, chars:"q", unmodifiedChars:"q"}, + "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R, + modifiers:{}, chars:"r", unmodifiedChars:"r"}, + "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R, + modifiers:{shiftKey:1}, chars:"R", unmodifiedChars:"R"}, + "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R, + modifiers:{ctrlKey:1}, chars:"\u0012", unmodifiedChars:"r"}, + "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0012", unmodifiedChars:"R"}, + "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R, + modifiers:{altKey:1}, chars:"\u00AE", unmodifiedChars:"r"}, + "\u00AE", "KeyR", KeyboardEvent.DOM_VK_R, "\u00AE", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R, + modifiers:{altKey:1, shiftKey:1}, chars:"\u2030", unmodifiedChars:"R"}, + "\u2030", "KeyR", KeyboardEvent.DOM_VK_R, "\u2030", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0012", unmodifiedChars:"r"}, + "\u00AE", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0012", unmodifiedChars:"R"}, + "\u2030", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // XXX This test makes some tabs and dialogs. + // yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_R, + // modifiers:{metaKey:1}, chars:"r", unmodifiedChars:"r"}, + // "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S, + modifiers:{}, chars:"s", unmodifiedChars:"s"}, + "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S, + modifiers:{shiftKey:1}, chars:"S", unmodifiedChars:"S"}, + "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S, + modifiers:{ctrlKey:1}, chars:"\u0013", unmodifiedChars:"s"}, + "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0013", unmodifiedChars:"S"}, + "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S, + modifiers:{altKey:1}, chars:"\u00DF", unmodifiedChars:"s"}, + "\u00DF", "KeyS", KeyboardEvent.DOM_VK_S, "\u00DF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00CD", unmodifiedChars:"S"}, + "\u00CD", "KeyS", KeyboardEvent.DOM_VK_S, "\u00CD", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0013", unmodifiedChars:"s"}, + "\u00DF", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0013", unmodifiedChars:"S"}, + "\u00CD", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_S, + modifiers:{metaKey:1}, chars:"s", unmodifiedChars:"s"}, + "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{}, chars:"t", unmodifiedChars:"t"}, + "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{shiftKey:1}, chars:"T", unmodifiedChars:"T"}, + "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{ctrlKey:1}, chars:"\u0014", unmodifiedChars:"t"}, + "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0014", unmodifiedChars:"T"}, + "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{altKey:1}, chars:"\u2020", unmodifiedChars:"t"}, + "\u2020", "KeyT", KeyboardEvent.DOM_VK_T, "\u2020", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{altKey:1, shiftKey:1}, chars:"\u02C7", unmodifiedChars:"T"}, + "\u02C7", "KeyT", KeyboardEvent.DOM_VK_T, "\u02C7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0014", unmodifiedChars:"t"}, + "\u2020", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0014", unmodifiedChars:"T"}, + "\u02C7", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{metaKey:1}, chars:"t", unmodifiedChars:"t"}, + "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U, + modifiers:{}, chars:"u", unmodifiedChars:"u"}, + "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U, + modifiers:{shiftKey:1}, chars:"U", unmodifiedChars:"U"}, + "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U, + modifiers:{ctrlKey:1}, chars:"\u0015", unmodifiedChars:"u"}, + "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0015", unmodifiedChars:"U"}, + "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U, + modifiers:{altKey:1}, chars:"", unmodifiedChars:"u"}, + "Dead", "KeyU", KeyboardEvent.DOM_VK_U, "\u00A8", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00A8", unmodifiedChars:"U"}, + "\u00A8", "KeyU", KeyboardEvent.DOM_VK_U, "\u00A8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0015", unmodifiedChars:"u"}, + "Dead", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0015", unmodifiedChars:"U"}, + "\u00A8", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_U, + modifiers:{metaKey:1}, chars:"u", unmodifiedChars:"u"}, + "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V, + modifiers:{}, chars:"v", unmodifiedChars:"v"}, + "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V, + modifiers:{shiftKey:1}, chars:"V", unmodifiedChars:"V"}, + "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V, + modifiers:{ctrlKey:1}, chars:"\u0016", unmodifiedChars:"v"}, + "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0016", unmodifiedChars:"V"}, + "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V, + modifiers:{altKey:1}, chars:"\u221A", unmodifiedChars:"v"}, + "\u221A", "KeyV", KeyboardEvent.DOM_VK_V, "\u221A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V, + modifiers:{altKey:1, shiftKey:1}, chars:"\u25CA", unmodifiedChars:"V"}, + "\u25CA", "KeyV", KeyboardEvent.DOM_VK_V, "\u25CA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0016", unmodifiedChars:"v"}, + "\u221A", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0016", unmodifiedChars:"V"}, + "\u25CA", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_V, + modifiers:{metaKey:1}, chars:"v", unmodifiedChars:"v"}, + "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W, + modifiers:{}, chars:"w", unmodifiedChars:"w"}, + "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W, + modifiers:{shiftKey:1}, chars:"W", unmodifiedChars:"W"}, + "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W, + modifiers:{ctrlKey:1}, chars:"\u0017", unmodifiedChars:"w"}, + "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0017", unmodifiedChars:"W"}, + "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W, + modifiers:{altKey:1}, chars:"\u2211", unmodifiedChars:"w"}, + "\u2211", "KeyW", KeyboardEvent.DOM_VK_W, "\u2211", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W, + modifiers:{altKey:1, shiftKey:1}, chars:"\u201E", unmodifiedChars:"W"}, + "\u201E", "KeyW", KeyboardEvent.DOM_VK_W, "\u201E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0017", unmodifiedChars:"w"}, + "\u2211", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0017", unmodifiedChars:"W"}, + "\u201E", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_W, + modifiers:{metaKey:1}, chars:"w", unmodifiedChars:"w"}, + "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X, + modifiers:{}, chars:"x", unmodifiedChars:"x"}, + "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X, + modifiers:{shiftKey:1}, chars:"X", unmodifiedChars:"X"}, + "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X, + modifiers:{ctrlKey:1}, chars:"\u0018", unmodifiedChars:"x"}, + "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018", unmodifiedChars:"X"}, + "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X, + modifiers:{altKey:1}, chars:"\u2248", unmodifiedChars:"x"}, + "\u2248", "KeyX", KeyboardEvent.DOM_VK_X, "\u2248", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X, + modifiers:{altKey:1, shiftKey:1}, chars:"\u02DB", unmodifiedChars:"X"}, + "\u02DB", "KeyX", KeyboardEvent.DOM_VK_X, "\u02DB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0018", unmodifiedChars:"x"}, + "\u2248", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0018", unmodifiedChars:"X"}, + "\u02DB", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_X, + modifiers:{metaKey:1}, chars:"x", unmodifiedChars:"x"}, + "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y, + modifiers:{}, chars:"y", unmodifiedChars:"y"}, + "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y, + modifiers:{shiftKey:1}, chars:"Y", unmodifiedChars:"Y"}, + "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y, + modifiers:{ctrlKey:1}, chars:"\u0019", unmodifiedChars:"y"}, + "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0019", unmodifiedChars:"Y"}, + "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y, + modifiers:{altKey:1}, chars:"\u00A5", unmodifiedChars:"y"}, + "\u00A5", "KeyY", KeyboardEvent.DOM_VK_Y, "\u00A5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00C1", unmodifiedChars:"Y"}, + "\u00C1", "KeyY", KeyboardEvent.DOM_VK_Y, "\u00C1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u0019", unmodifiedChars:"y"}, + "\u00A5", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0019", unmodifiedChars:"Y"}, + "\u00C1", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Y, + modifiers:{metaKey:1}, chars:"y", unmodifiedChars:"y"}, + "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z, + modifiers:{}, chars:"z", unmodifiedChars:"z"}, + "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z, + modifiers:{shiftKey:1}, chars:"Z", unmodifiedChars:"Z"}, + "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z, + modifiers:{ctrlKey:1}, chars:"\u001A", unmodifiedChars:"z"}, + "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001A", unmodifiedChars:"Z"}, + "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z, + modifiers:{altKey:1}, chars:"\u03A9", unmodifiedChars:"z"}, + "\u03A9", "KeyZ", KeyboardEvent.DOM_VK_Z, "\u03A9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00B8", unmodifiedChars:"Z"}, + "\u00B8", "KeyZ", KeyboardEvent.DOM_VK_Z, "\u00B8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u001A", unmodifiedChars:"z"}, + "\u03A9", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001A", unmodifiedChars:"Z"}, + "\u00B8", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Z, + modifiers:{metaKey:1}, chars:"z", unmodifiedChars:"z"}, + "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // numeric + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1, + modifiers:{}, chars:"1", unmodifiedChars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1, + modifiers:{shiftKey:1}, chars:"!", unmodifiedChars:"!"}, + "!", "Digit1", KeyboardEvent.DOM_VK_1, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1, + modifiers:{ctrlKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"!"}, + "!", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1, + modifiers:{altKey:1}, chars:"\u00A1", unmodifiedChars:"1"}, + "\u00A1", "Digit1", KeyboardEvent.DOM_VK_1, "\u00A1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1, + modifiers:{altKey:1, shiftKey:1}, chars:"\u2044", unmodifiedChars:"!"}, + "\u2044", "Digit1", KeyboardEvent.DOM_VK_1, "\u2044", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1, + modifiers:{altKey:1, ctrlKey:1}, chars:"1", unmodifiedChars:"1"}, + "\u00A1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"!"}, + "\u2044", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_1, + modifiers:{metaKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2, + modifiers:{}, chars:"2", unmodifiedChars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2, + modifiers:{shiftKey:1}, chars:"@", unmodifiedChars:"@"}, + "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2, + modifiers:{ctrlKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0000", unmodifiedChars:"@"}, + "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2, + modifiers:{altKey:1}, chars:"\u2122", unmodifiedChars:"2"}, + "\u2122", "Digit2", KeyboardEvent.DOM_VK_2, "\u2122", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2, + modifiers:{altKey:1, shiftKey:1}, chars:"\u20AC", unmodifiedChars:"@"}, + "\u20AC", "Digit2", KeyboardEvent.DOM_VK_2, "\u20AC", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2, + modifiers:{altKey:1, ctrlKey:1}, chars:"2", unmodifiedChars:"2"}, + "\u2122", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0000", unmodifiedChars:"@"}, + "\u20AC", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_2, + modifiers:{metaKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3, + modifiers:{}, chars:"3", unmodifiedChars:"3"}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3, + modifiers:{shiftKey:1}, chars:"#", unmodifiedChars:"#"}, + "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3, + modifiers:{ctrlKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"#"}, + "#", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3, + modifiers:{altKey:1}, chars:"\u00A3", unmodifiedChars:"3"}, + "\u00A3", "Digit3", KeyboardEvent.DOM_VK_3, "\u00A3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3, + modifiers:{altKey:1, shiftKey:1}, chars:"\u2039", unmodifiedChars:"#"}, + "\u2039", "Digit3", KeyboardEvent.DOM_VK_3, "\u2039", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3, + modifiers:{altKey:1, ctrlKey:1}, chars:"3", unmodifiedChars:"3"}, + "\u00A3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"#"}, + "\u2039", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_3, + modifiers:{metaKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4, + modifiers:{}, chars:"4", unmodifiedChars:"4"}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4, + modifiers:{shiftKey:1}, chars:"$", unmodifiedChars:"$"}, + "$", "Digit4", KeyboardEvent.DOM_VK_4, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4, + modifiers:{ctrlKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"$"}, + "$", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4, + modifiers:{altKey:1}, chars:"\u00A2", unmodifiedChars:"4"}, + "\u00A2", "Digit4", KeyboardEvent.DOM_VK_4, "\u00A2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4, + modifiers:{altKey:1, shiftKey:1}, chars:"\u203A", unmodifiedChars:"$"}, + "\u203A", "Digit4", KeyboardEvent.DOM_VK_4, "\u203A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4, + modifiers:{altKey:1, ctrlKey:1}, chars:"4", unmodifiedChars:"4"}, + "\u00A2", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"$"}, + "\u203A", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_4, + modifiers:{metaKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5, + modifiers:{}, chars:"5", unmodifiedChars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5, + modifiers:{shiftKey:1}, chars:"%", unmodifiedChars:"%"}, + "%", "Digit5", KeyboardEvent.DOM_VK_5, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5, + modifiers:{ctrlKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"%"}, + "%", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5, + modifiers:{altKey:1}, chars:"\u221E", unmodifiedChars:"5"}, + "\u221E", "Digit5", KeyboardEvent.DOM_VK_5, "\u221E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5, + modifiers:{altKey:1, shiftKey:1}, chars:"\uFB01", unmodifiedChars:"%"}, + "\uFB01", "Digit5", KeyboardEvent.DOM_VK_5, "\uFB01", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5, + modifiers:{altKey:1, ctrlKey:1}, chars:"5", unmodifiedChars:"5"}, + "\u221E", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"%"}, + "\uFB01", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_5, + modifiers:{metaKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6, + modifiers:{}, chars:"6", unmodifiedChars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6, + modifiers:{shiftKey:1}, chars:"^", unmodifiedChars:"^"}, + "^", "Digit6", KeyboardEvent.DOM_VK_6, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6, + modifiers:{ctrlKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001E", unmodifiedChars:"^"}, + "^", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6, + modifiers:{altKey:1}, chars:"\u00A7", unmodifiedChars:"6"}, + "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6, + modifiers:{altKey:1, shiftKey:1}, chars:"\uFB02", unmodifiedChars:"^"}, + "\uFB02", "Digit6", KeyboardEvent.DOM_VK_6, "\uFB02", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6, + modifiers:{altKey:1, ctrlKey:1}, chars:"6", unmodifiedChars:"6"}, + "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001E", unmodifiedChars:"^"}, + "\uFB02", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_6, + modifiers:{metaKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7, + modifiers:{}, chars:"7", unmodifiedChars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7, + modifiers:{shiftKey:1}, chars:"&", unmodifiedChars:"&"}, + "&", "Digit7", KeyboardEvent.DOM_VK_7, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7, + modifiers:{ctrlKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"&"}, + "&", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7, + modifiers:{altKey:1}, chars:"\u00B6", unmodifiedChars:"7"}, + "\u00B6", "Digit7", KeyboardEvent.DOM_VK_7, "\u00B6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7, + modifiers:{altKey:1, shiftKey:1}, chars:"\u2021", unmodifiedChars:"&"}, + "\u2021", "Digit7", KeyboardEvent.DOM_VK_7, "\u2021", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7, + modifiers:{altKey:1, ctrlKey:1}, chars:"7", unmodifiedChars:"7"}, + "\u00B6", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"&"}, + "\u2021", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_7, + modifiers:{metaKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8, + modifiers:{}, chars:"8", unmodifiedChars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8, + modifiers:{shiftKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "Digit8", KeyboardEvent.DOM_VK_8, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8, + modifiers:{ctrlKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"*"}, + "*", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8, + modifiers:{altKey:1}, chars:"\u2022", unmodifiedChars:"8"}, + "\u2022", "Digit8", KeyboardEvent.DOM_VK_8, "\u2022", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00B0", unmodifiedChars:"*"}, + "\u00B0", "Digit8", KeyboardEvent.DOM_VK_8, "\u00B0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8, + modifiers:{altKey:1, ctrlKey:1}, chars:"8", unmodifiedChars:"8"}, + "\u2022", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"*"}, + "\u00B0", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_8, + modifiers:{metaKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9, + modifiers:{}, chars:"9", unmodifiedChars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9, + modifiers:{shiftKey:1}, chars:"(", unmodifiedChars:"("}, + "(", "Digit9", KeyboardEvent.DOM_VK_9, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9, + modifiers:{ctrlKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"("}, + "(", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9, + modifiers:{altKey:1}, chars:"\u00AA", unmodifiedChars:"9"}, + "\u00AA", "Digit9", KeyboardEvent.DOM_VK_9, "\u00AA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00B7", unmodifiedChars:"("}, + "\u00B7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00B7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9, + modifiers:{altKey:1, ctrlKey:1}, chars:"9", unmodifiedChars:"9"}, + "\u00AA", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"("}, + "\u00B7", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_9, + modifiers:{metaKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0, + modifiers:{}, chars:"0", unmodifiedChars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0, + modifiers:{shiftKey:1}, chars:")", unmodifiedChars:")"}, + ")", "Digit0", KeyboardEvent.DOM_VK_0, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0, + modifiers:{ctrlKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"0", unmodifiedChars:")"}, + ")", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0, + modifiers:{altKey:1}, chars:"\u00BA", unmodifiedChars:"0"}, + "\u00BA", "Digit0", KeyboardEvent.DOM_VK_0, "\u00BA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0, + modifiers:{altKey:1, shiftKey:1}, chars:"\u201A", unmodifiedChars:")"}, + "\u201A", "Digit0", KeyboardEvent.DOM_VK_0, "\u201A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0, + modifiers:{altKey:1, ctrlKey:1}, chars:"0", unmodifiedChars:"0"}, + "\u00BA", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"0", unmodifiedChars:")"}, + "\u201A", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_0, + modifiers:{metaKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // other chracters + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave, + modifiers:{}, chars:"`", unmodifiedChars:"`"}, + "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave, + modifiers:{shiftKey:1}, chars:"~", unmodifiedChars:"~"}, + "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "~", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave, + modifiers:{ctrlKey:1}, chars:"`", unmodifiedChars:"`"}, + "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"`", unmodifiedChars:"~"}, + "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave, + modifiers:{altKey:1}, chars:"", unmodifiedChars:"`"}, + "Dead", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave, + modifiers:{altKey:1, shiftKey:1}, chars:"`", unmodifiedChars:"~"}, + "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave, + modifiers:{altKey:1, ctrlKey:1}, chars:"`", unmodifiedChars:"`"}, + "Dead", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"`", unmodifiedChars:"~"}, + "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Grave, + modifiers:{metaKey:1}, chars:"`", unmodifiedChars:"`"}, + "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus, + modifiers:{}, chars:"-", unmodifiedChars:"-"}, + "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus, + modifiers:{shiftKey:1}, chars:"_", unmodifiedChars:"_"}, + "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus, + modifiers:{ctrlKey:1}, chars:"\u001F", unmodifiedChars:"-"}, + "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001F", unmodifiedChars:"_"}, + "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus, + modifiers:{altKey:1}, chars:"\u2013", unmodifiedChars:"-"}, + "\u2013", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u2013", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus, + modifiers:{altKey:1, shiftKey:1}, chars:"\u2014", unmodifiedChars:"_"}, + "\u2014", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u2014", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u001F", unmodifiedChars:"-"}, + "\u2013", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001F", unmodifiedChars:"_"}, + "\u2014", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Minus, + modifiers:{metaKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal, + modifiers:{}, chars:"=", unmodifiedChars:"="}, + "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal, + modifiers:{shiftKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal, + modifiers:{ctrlKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"+"}, + "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal, + modifiers:{altKey:1}, chars:"\u2260", unmodifiedChars:"="}, + "\u2260", "Equal", KeyboardEvent.DOM_VK_EQUALS, "\u2260", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00B1", unmodifiedChars:"+"}, + "\u00B1", "Equal", KeyboardEvent.DOM_VK_EQUALS, "\u00B1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal, + modifiers:{altKey:1, ctrlKey:1}, chars:"=", unmodifiedChars:"="}, + "\u2260", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"+"}, + "\u00B1", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Equal, + modifiers:{metaKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{}, chars:"[", unmodifiedChars:"["}, + "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{shiftKey:1}, chars:"{", unmodifiedChars:"{"}, + "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{ctrlKey:1}, chars:"\u001B", unmodifiedChars:"["}, + "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001B", unmodifiedChars:"{"}, + "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{altKey:1}, chars:"\u201C", unmodifiedChars:"["}, + "\u201C", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u201C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{altKey:1, shiftKey:1}, chars:"\u201D", unmodifiedChars:"{"}, + "\u201D", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u201D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u001B", unmodifiedChars:"["}, + "\u201C", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001B", unmodifiedChars:"{"}, + "\u201D", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{metaKey:1}, chars:"[", unmodifiedChars:"["}, + "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket, + modifiers:{}, chars:"]", unmodifiedChars:"]"}, + "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket, + modifiers:{shiftKey:1}, chars:"}", unmodifiedChars:"}"}, + "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket, + modifiers:{ctrlKey:1}, chars:"\u001D", unmodifiedChars:"]"}, + "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001D", unmodifiedChars:"}"}, + "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket, + modifiers:{altKey:1}, chars:"\u2018", unmodifiedChars:"]"}, + "\u2018", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u2018", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket, + modifiers:{altKey:1, shiftKey:1}, chars:"\u2019", unmodifiedChars:"}"}, + "\u2019", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u2019", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u001D", unmodifiedChars:"]"}, + "\u2018", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001D", unmodifiedChars:"}"}, + "\u2019", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_RightBracket, + modifiers:{metaKey:1}, chars:"]", unmodifiedChars:"]"}, + "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash, + modifiers:{}, chars:"\\", unmodifiedChars:"\\"}, + "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash, + modifiers:{shiftKey:1}, chars:"|", unmodifiedChars:"|"}, + "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash, + modifiers:{ctrlKey:1}, chars:"\u001C", unmodifiedChars:"\\"}, + "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001C", unmodifiedChars:"|"}, + "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash, + modifiers:{altKey:1}, chars:"\u00AB", unmodifiedChars:"\\"}, + "\u00AB", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u00AB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00BB", unmodifiedChars:"|"}, + "\u00BB", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u00BB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u001C", unmodifiedChars:"\\"}, + "\u00AB", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001C", unmodifiedChars:"|"}, + "\u00BB", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Backslash, + modifiers:{metaKey:1}, chars:"\\", unmodifiedChars:"\\"}, + "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{}, chars:";", unmodifiedChars:";"}, + ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{shiftKey:1}, chars:":", unmodifiedChars:":"}, + ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{ctrlKey:1}, chars:";", unmodifiedChars:";"}, + ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{ctrlKey:1, shiftKey:1}, chars:";", unmodifiedChars:":"}, + ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{altKey:1}, chars:"\u2026", unmodifiedChars:";"}, + "\u2026", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, "\u2026", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00DA", unmodifiedChars:":"}, + "\u00DA", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, "\u00DA", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{altKey:1, ctrlKey:1}, chars:";", unmodifiedChars:";"}, + "\u2026", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:";", unmodifiedChars:":"}, + "\u00DA", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{metaKey:1}, chars:";", unmodifiedChars:";"}, + ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{}, chars:"'", unmodifiedChars:"'"}, + "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{shiftKey:1}, chars:"\"", unmodifiedChars:"\""}, + "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{ctrlKey:1}, chars:"'", unmodifiedChars:"'"}, + "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"'", unmodifiedChars:"\""}, + "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{altKey:1}, chars:"\u00E6", unmodifiedChars:"'"}, + "\u00E6", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\u00E6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00C6", unmodifiedChars:"\""}, + "\u00C6", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\u00C6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{altKey:1, ctrlKey:1}, chars:"'", unmodifiedChars:"'"}, + "\u00E6", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"'", unmodifiedChars:"\""}, + "\u00C6", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{metaKey:1}, chars:"'", unmodifiedChars:"'"}, + "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma, + modifiers:{}, chars:",", unmodifiedChars:","}, + ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma, + modifiers:{shiftKey:1}, chars:"<", unmodifiedChars:"<"}, + "<", "Comma", KeyboardEvent.DOM_VK_COMMA, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma, + modifiers:{ctrlKey:1}, chars:",", unmodifiedChars:","}, + ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma, + modifiers:{ctrlKey:1, shiftKey:1}, chars:",", unmodifiedChars:"<"}, + "<", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma, + modifiers:{altKey:1}, chars:"\u2264", unmodifiedChars:","}, + "\u2264", "Comma", KeyboardEvent.DOM_VK_COMMA, "\u2264", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00AF", unmodifiedChars:"<"}, + "\u00AF", "Comma", KeyboardEvent.DOM_VK_COMMA, "\u00AF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma, + modifiers:{altKey:1, ctrlKey:1}, chars:",", unmodifiedChars:","}, + "\u2264", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:",", unmodifiedChars:"<"}, + "\u00AF", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Comma, + modifiers:{metaKey:1}, chars:",", unmodifiedChars:","}, + ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{}, chars:".", unmodifiedChars:"."}, + ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{shiftKey:1}, chars:">", unmodifiedChars:">"}, + ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{ctrlKey:1}, chars:".", unmodifiedChars:"."}, + ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{ctrlKey:1, shiftKey:1}, chars:".", unmodifiedChars:">"}, + ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{altKey:1}, chars:"\u2265", unmodifiedChars:"."}, + "\u2265", "Period", KeyboardEvent.DOM_VK_PERIOD, "\u2265", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{altKey:1, shiftKey:1}, chars:"\u02D8", unmodifiedChars:">"}, + "\u02D8", "Period", KeyboardEvent.DOM_VK_PERIOD, "\u02D8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{altKey:1, ctrlKey:1}, chars:".", unmodifiedChars:"."}, + "\u2265", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:".", unmodifiedChars:">"}, + "\u02D8", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{metaKey:1}, chars:".", unmodifiedChars:"."}, + ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash, + modifiers:{}, chars:"/", unmodifiedChars:"/"}, + "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash, + modifiers:{shiftKey:1}, chars:"?", unmodifiedChars:"?"}, + "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash, + modifiers:{ctrlKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"?"}, + "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash, + modifiers:{altKey:1}, chars:"\u00F7", unmodifiedChars:"/"}, + "\u00F7", "Slash", KeyboardEvent.DOM_VK_SLASH, "\u00F7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00BF", unmodifiedChars:"?"}, + "\u00BF", "Slash", KeyboardEvent.DOM_VK_SLASH, "\u00BF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash, + modifiers:{altKey:1, ctrlKey:1}, chars:"/", unmodifiedChars:"/"}, + "\u00F7", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"?"}, + "\u00BF", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Slash, + modifiers:{metaKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // numpad + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1, + modifiers:{numericKeyPadKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad1, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2, + modifiers:{numericKeyPadKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad2, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3, + modifiers:{numericKeyPadKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad3, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4, + modifiers:{numericKeyPadKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad4, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5, + modifiers:{numericKeyPadKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad5, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6, + modifiers:{numericKeyPadKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad6, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7, + modifiers:{numericKeyPadKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad7, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8, + modifiers:{numericKeyPadKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad8, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9, + modifiers:{numericKeyPadKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad9, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0, + modifiers:{numericKeyPadKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Keypad0, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals, + modifiers:{numericKeyPadKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEquals, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"=", unmodifiedChars:"="}, + "=", "NumpadEqual", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide, + modifiers:{numericKeyPadKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadDivide, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"/", unmodifiedChars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply, + modifiers:{numericKeyPadKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMultiply, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"*", unmodifiedChars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus, + modifiers:{numericKeyPadKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadMinus, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"-", unmodifiedChars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus, + modifiers:{numericKeyPadKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadPlus, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"+", unmodifiedChars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter, + modifiers:{numericKeyPadKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_KeypadEnter, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:"\u0003", unmodifiedChars:"\u0003"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma, + modifiers:{numericKeyPadKey:1, shiftKey:1}, chars:",", unmodifiedChars:","}, + ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma, + modifiers:{numericKeyPadKey:1, ctrlKey:1}, chars:",", unmodifiedChars:","}, + ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma, + modifiers:{numericKeyPadKey:1, ctrlKey:1, shiftKey:1}, chars:",", unmodifiedChars:","}, + ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma, + modifiers:{numericKeyPadKey:1, altKey:1}, chars:",", unmodifiedChars:","}, + ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma, + modifiers:{numericKeyPadKey:1, altKey:1, shiftKey:1}, chars:",", unmodifiedChars:","}, + ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1}, chars:",", unmodifiedChars:","}, + ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma, + modifiers:{numericKeyPadKey:1, altKey:1, ctrlKey:1, shiftKey:1}, chars:",", unmodifiedChars:","}, + ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_JIS_KeypadComma, + modifiers:{numericKeyPadKey:1, metaKey:1}, chars:",", unmodifiedChars:","}, + ",", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ",", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + + // French, numeric + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{}, chars:"&", unmodifiedChars:"&"}, + "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{shiftKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{ctrlKey:1}, chars:"1", unmodifiedChars:"&"}, + "&", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{altKey:1}, chars:"\uF8FF", unmodifiedChars:"&"}, + "\uF8FF", "Digit1", KeyboardEvent.DOM_VK_1, "\uF8FF", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{altKey:1, shiftKey:1}, chars:"", unmodifiedChars:"1"}, + "Dead", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{altKey:1, ctrlKey:1}, chars:"1", unmodifiedChars:"&"}, + "\uF8FF", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"}, + "Dead", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{metaKey:1}, chars:"&", unmodifiedChars:"&"}, + "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_1, + modifiers:{metaKey:1, shiftKey:1}, chars:"1", unmodifiedChars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{}, chars:"\u00E9", unmodifiedChars:"\u00E9"}, + "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "\u00E9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{shiftKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{ctrlKey:1}, chars:"2", unmodifiedChars:"\u00E9"}, + "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{altKey:1}, chars:"\u00EB", unmodifiedChars:"\u00E9"}, + "\u00EB", "Digit2", KeyboardEvent.DOM_VK_2, "\u00EB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{altKey:1, shiftKey:1}, chars:"\u201E", unmodifiedChars:"2"}, + "\u201E", "Digit2", KeyboardEvent.DOM_VK_2, "\u201E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{altKey:1, ctrlKey:1}, chars:"2", unmodifiedChars:"\u00E9"}, + "\u00EB", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"}, + "\u201E", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{metaKey:1}, chars:"\u00E9", unmodifiedChars:"\u00E9"}, + "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "\u00E9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_2, + modifiers:{metaKey:1, shiftKey:1}, chars:"2", unmodifiedChars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + modifiers:{}, chars:"\"", unmodifiedChars:"\""}, + "\"", "Digit3", KeyboardEvent.DOM_VK_3, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + modifiers:{shiftKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + modifiers:{ctrlKey:1}, chars:"3", unmodifiedChars:"\""}, + "\"", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + modifiers:{altKey:1}, chars:"\u201C", unmodifiedChars:"\""}, + "\u201C", "Digit3", KeyboardEvent.DOM_VK_3, "\u201C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + modifiers:{altKey:1, shiftKey:1}, chars:"\u201D", unmodifiedChars:"3"}, + "\u201D", "Digit3", KeyboardEvent.DOM_VK_3, "\u201D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + modifiers:{altKey:1, ctrlKey:1}, chars:"3", unmodifiedChars:"\""}, + "\u201C", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"3", unmodifiedChars:"3"}, + "\u201D", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + modifiers:{metaKey:1}, chars:"\"", unmodifiedChars:"\""}, + "\"", "Digit3", KeyboardEvent.DOM_VK_3, "\"", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Cmd+Shift+3 is a shortcut key of taking a snapshot + // yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_3, + // modifiers:{metaKey:1, shiftKey:1}, chars:"\"", unmodifiedChars:"\""}, + // "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + modifiers:{}, chars:"'", unmodifiedChars:"'"}, + "'", "Digit4", KeyboardEvent.DOM_VK_4, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + modifiers:{shiftKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + modifiers:{ctrlKey:1}, chars:"4", unmodifiedChars:"'"}, + "'", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + modifiers:{altKey:1}, chars:"\u2018", unmodifiedChars:"'"}, + "\u2018", "Digit4", KeyboardEvent.DOM_VK_4, "\u2018", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + modifiers:{altKey:1, shiftKey:1}, chars:"\u2019", unmodifiedChars:"4"}, + "\u2019", "Digit4", KeyboardEvent.DOM_VK_4, "\u2019", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + modifiers:{altKey:1, ctrlKey:1}, chars:"4", unmodifiedChars:"'"}, + "\u2018", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"}, + "\u2019", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + modifiers:{metaKey:1}, chars:"'", unmodifiedChars:"'"}, + "'", "Digit4", KeyboardEvent.DOM_VK_4, "'", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Cmd+Shift+4 is a shortcut key of taking a snapshot in specific range + // yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_4, + // modifiers:{metaKey:1, shiftKey:1}, chars:"4", unmodifiedChars:"4"}, + // "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{}, chars:"(", unmodifiedChars:"("}, + "(", "Digit5", KeyboardEvent.DOM_VK_5, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{shiftKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{ctrlKey:1}, chars:"5", unmodifiedChars:"("}, + "(", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{altKey:1}, chars:"{", unmodifiedChars:"("}, + "{", "Digit5", KeyboardEvent.DOM_VK_5, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{altKey:1, shiftKey:1}, chars:"[", unmodifiedChars:"5"}, + "[", "Digit5", KeyboardEvent.DOM_VK_5, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{altKey:1, ctrlKey:1}, chars:"5", unmodifiedChars:"("}, + "{", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"}, + "[", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{metaKey:1}, chars:"(", unmodifiedChars:"("}, + "(", "Digit5", KeyboardEvent.DOM_VK_5, "(", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_5, + modifiers:{metaKey:1, shiftKey:1}, chars:"5", unmodifiedChars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{}, chars:"\u00A7", unmodifiedChars:"\u00A7"}, + "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{shiftKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{ctrlKey:1}, chars:"\u001D", unmodifiedChars:"\u00A7"}, + "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001D", unmodifiedChars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{altKey:1}, chars:"\u00B6", unmodifiedChars:"\u00A7"}, + "\u00B6", "Digit6", KeyboardEvent.DOM_VK_6, "\u00B6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00E5", unmodifiedChars:"6"}, + "\u00E5", "Digit6", KeyboardEvent.DOM_VK_6, "\u00E5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u001D", unmodifiedChars:"\u00A7"}, + "\u00B6", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001D", unmodifiedChars:"6"}, + "\u00E5", "Digit6", KeyboardEvent.DOM_VK_6, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{metaKey:1}, chars:"\u00A7", unmodifiedChars:"\u00A7"}, + "\u00A7", "Digit6", KeyboardEvent.DOM_VK_6, "\u00A7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_6, + modifiers:{metaKey:1, shiftKey:1}, chars:"6", unmodifiedChars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{}, chars:"\u00E8", unmodifiedChars:"\u00E8"}, + "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "\u00E8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{shiftKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{ctrlKey:1}, chars:"7", unmodifiedChars:"\u00E8"}, + "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{altKey:1}, chars:"\u00AB", unmodifiedChars:"\u00E8"}, + "\u00AB", "Digit7", KeyboardEvent.DOM_VK_7, "\u00AB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00BB", unmodifiedChars:"7"}, + "\u00BB", "Digit7", KeyboardEvent.DOM_VK_7, "\u00BB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{altKey:1, ctrlKey:1}, chars:"7", unmodifiedChars:"\u00E8"}, + "\u00AB", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"}, + "\u00BB", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{metaKey:1}, chars:"\u00E8", unmodifiedChars:"\u00E8"}, + "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "\u00E8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_7, + modifiers:{metaKey:1, shiftKey:1}, chars:"7", unmodifiedChars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{}, chars:"!", unmodifiedChars:"!"}, + "!", "Digit8", KeyboardEvent.DOM_VK_8, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{shiftKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{ctrlKey:1}, chars:"8", unmodifiedChars:"!"}, + "!", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{altKey:1}, chars:"\u00A1", unmodifiedChars:"!"}, + "\u00A1", "Digit8", KeyboardEvent.DOM_VK_8, "\u00A1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00DB", unmodifiedChars:"8"}, + "\u00DB", "Digit8", KeyboardEvent.DOM_VK_8, "\u00DB", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{altKey:1, ctrlKey:1}, chars:"8", unmodifiedChars:"!"}, + "\u00A1", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"}, + "\u00DB", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{metaKey:1}, chars:"!", unmodifiedChars:"!"}, + "!", "Digit8", KeyboardEvent.DOM_VK_8, "!", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_8, + modifiers:{metaKey:1, shiftKey:1}, chars:"8", unmodifiedChars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{}, chars:"\u00E7", unmodifiedChars:"\u00E7"}, + "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00E7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{shiftKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{ctrlKey:1}, chars:"\u001C", unmodifiedChars:"\u00E7"}, + "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001C", unmodifiedChars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{altKey:1}, chars:"\u00C7", unmodifiedChars:"\u00E7"}, + "\u00C7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00C7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00C1", unmodifiedChars:"9"}, + "\u00C1", "Digit9", KeyboardEvent.DOM_VK_9, "\u00C1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{altKey:1, ctrlKey:1}, chars:"\u001C", unmodifiedChars:"\u00E7"}, + "\u00C7", "Digit9", KeyboardEvent.DOM_VK_9, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"\u001C", unmodifiedChars:"9"}, + "\u00C1", "Digit9", KeyboardEvent.DOM_VK_9, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{metaKey:1}, chars:"\u00E7", unmodifiedChars:"\u00E7"}, + "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00E7", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_9, + modifiers:{metaKey:1, shiftKey:1}, chars:"9", unmodifiedChars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{}, chars:"\u00E0", unmodifiedChars:"\u00E0"}, + "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{shiftKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{ctrlKey:1}, chars:"", unmodifiedChars:"\u00E0"}, + "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"", unmodifiedChars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{altKey:1}, chars:"\u00F8", unmodifiedChars:"\u00E0"}, + "\u00F8", "Digit0", KeyboardEvent.DOM_VK_0, "\u00F8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00D8", unmodifiedChars:"0"}, + "\u00D8", "Digit0", KeyboardEvent.DOM_VK_0, "\u00D8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{altKey:1, ctrlKey:1}, chars:"", unmodifiedChars:"\u00E0"}, + "\u00F8", "Digit0", KeyboardEvent.DOM_VK_0, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:"", unmodifiedChars:"0"}, + "\u00D8", "Digit0", KeyboardEvent.DOM_VK_0, "\u0000" /* TODO */, SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{metaKey:1}, chars:"\u00E0", unmodifiedChars:"\u00E0"}, + "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u00E0", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:MAC_VK_ANSI_0, + modifiers:{metaKey:1, shiftKey:1}, chars:"0", unmodifiedChars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Thai + // keycode should be DOM_VK_[A-Z] of the key on the latest ASCII capable keyboard layout is for alphabet + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_A, + modifiers:{}, chars:"\u0E1F", unmodifiedChars:"\u0E1F"}, + "\u0E1F", "KeyA", KeyboardEvent.DOM_VK_A, "\u0E1F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // keycode should be shifted character if unshifted character isn't an ASCII character + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_Quote, + modifiers:{}, chars:"\u0E07", unmodifiedChars:"\u0E07"}, + "\u0E07", "Quote", KeyboardEvent.DOM_VK_PERIOD, "\u0E07", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // keycode should be same as ANSI keyboard layout's key which causes the native virtual keycode + // if the character of the key on the latest ASCII capable keyboard layout isn't for alphabet + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_Period, + modifiers:{}, chars:"\u0E43", unmodifiedChars:"\u0E43"}, + "\u0E43", "Period", KeyboardEvent.DOM_VK_PERIOD, "\u0E43", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // keycode should be DOM_VK_[0-9] if the key on the latest ASCII capable keyboard layout is for numeric + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_1, + modifiers:{}, chars:"\u0E45", unmodifiedChars:"\u0E45"}, + "\u0E45", "Digit1", KeyboardEvent.DOM_VK_1, "\u0E45", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_2, + modifiers:{}, chars:"/", unmodifiedChars:"/"}, + "/", "Digit2", KeyboardEvent.DOM_VK_2, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_3, + modifiers:{}, chars:"_", unmodifiedChars:"_"}, + "_", "Digit3", KeyboardEvent.DOM_VK_3, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_4, + modifiers:{}, chars:"\u0E20", unmodifiedChars:"\u0E20"}, + "\u0E20", "Digit4", KeyboardEvent.DOM_VK_4, "\u0E20", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_5, + modifiers:{}, chars:"\u0E16", unmodifiedChars:"\u0E16"}, + "\u0E16", "Digit5", KeyboardEvent.DOM_VK_5, "\u0E16", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_6, + modifiers:{}, chars:"\u0E38", unmodifiedChars:"\u0E38"}, + "\u0E38", "Digit6", KeyboardEvent.DOM_VK_6, "\u0E38", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_7, + modifiers:{}, chars:"\u0E36", unmodifiedChars:"\u0E36"}, + "\u0E36", "Digit7", KeyboardEvent.DOM_VK_7, "\u0E36", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_8, + modifiers:{}, chars:"\u0E04", unmodifiedChars:"\u0E04"}, + "\u0E04", "Digit8", KeyboardEvent.DOM_VK_8, "\u0E04", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_9, + modifiers:{}, chars:"\u0E15", unmodifiedChars:"\u0E15"}, + "\u0E15", "Digit9", KeyboardEvent.DOM_VK_9, "\u0E15", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_THAI, keyCode:MAC_VK_ANSI_0, + modifiers:{}, chars:"\u0E08", unmodifiedChars:"\u0E08"}, + "\u0E08", "Digit0", KeyboardEvent.DOM_VK_0, "\u0E08", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Dvorak-Qwerty, layout should be changed when Command key is pressed. + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S, + modifiers:{}, chars:"o", unmodifiedChars:"o"}, + "o", "KeyS", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S, + modifiers:{shiftKey:1}, chars:"O", unmodifiedChars:"O"}, + "O", "KeyS", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S, + modifiers:{ctrlKey:1}, chars:"\u000F", unmodifiedChars:"o"}, + "o", "KeyS", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S, + modifiers:{altKey:1}, chars:"\u00F8", unmodifiedChars:"o"}, + "\u00F8", "KeyS", KeyboardEvent.DOM_VK_O, "\u00F8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_S, + modifiers:{metaKey:1}, chars:"s", unmodifiedChars:"o"}, + "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D, + modifiers:{}, chars:"e", unmodifiedChars:"e"}, + "e", "KeyD", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D, + modifiers:{shiftKey:1}, chars:"E", unmodifiedChars:"E"}, + "E", "KeyD", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D, + modifiers:{ctrlKey:1}, chars:"\u0005", unmodifiedChars:"e"}, + "e", "KeyD", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D, + modifiers:{altKey:1}, chars:"", unmodifiedChars:"e"}, + "Dead", "KeyD", KeyboardEvent.DOM_VK_E, "\u00B4", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // dead key + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_D, + modifiers:{metaKey:1}, chars:"d", unmodifiedChars:"e"}, + "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_I, + modifiers:{metaKey:1, altKey:1}, chars:"^", unmodifiedChars:"c"}, + "^", "KeyI", KeyboardEvent.DOM_VK_I, "^", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_DVORAK_QWERTY, keyCode:MAC_VK_ANSI_I, + modifiers:{metaKey:1, altKey:1, shiftKey:1}, chars:"\u02C6", unmodifiedChars:"C"}, + "\u02C6", "KeyI", KeyboardEvent.DOM_VK_I, "\u02C6", SHOULD_DELIVER_KEYDOWN_KEYPRESS, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Arabic - PC keyboard layout inputs 2 or more characters with some key. + yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_G, + modifiers:{shiftKey:1}, chars:"\u0644\u0623", unmodifiedChars:"\u0644\u0623"}, + ["\u0644\u0623", "\u0644", "\u0623", "\u0644\u0623"], "KeyG", KeyboardEvent.DOM_VK_G, "\u0644\u0623", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_T, + modifiers:{shiftKey:1}, chars:"\u0644\u0625", unmodifiedChars:"\u0644\u0625"}, + ["\u0644\u0625", "\u0644", "\u0625", "\u0644\u0625"], "KeyT", KeyboardEvent.DOM_VK_T, "\u0644\u0625", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_B, + modifiers:{shiftKey:1}, chars:"\u0644\u0622", unmodifiedChars:"\u0644\u0622"}, + ["\u0644\u0622", "\u0644", "\u0622", "\u0644\u0622"], "KeyB", KeyboardEvent.DOM_VK_B, "\u0644\u0622", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_B, + modifiers:{}, chars:"\u0644\u0627", unmodifiedChars:"\u0644\u0627"}, + ["\u0644\u0627", "\u0644", "\u0627", "\u0644\u0627"], "KeyB", KeyboardEvent.DOM_VK_B, "\u0644\u0627", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + cleanup(); + } + + function* testKeysOnWindows() + { + // On Windows, you can use Spy++ or Winspector (free) to watch window messages. + // The keyCode is given by the wParam of the last WM_KEYDOWN message. The + // chars string is given by the wParam of the WM_CHAR message. unmodifiedChars + // is not needed on Windows. + + // Shift-ctrl-alt generates no WM_CHAR, but we still get a keypress + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:""}, + "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Greek plain text + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A, + modifiers:{}, chars:"\u03b1"}, + "\u03b1", "KeyA", KeyboardEvent.DOM_VK_A, "\u03b1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A, + modifiers:{shiftKey:1}, chars:"\u0391"}, + "\u0391", "KeyA", KeyboardEvent.DOM_VK_A, "\u0391", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Greek ctrl keys produce Latin charcodes + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1}, chars:"\u0001"}, + "\u03b1", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001"}, + "\u0391", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Caps Lock key event + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_CAPITAL, + modifiers:{capsLockKey:1}, chars:""}, + "CapsLock", "CapsLock", KeyboardEvent.DOM_VK_CAPS_LOCK, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_CAPITAL, + modifiers:{capsLockKey:0}, chars:""}, + "CapsLock", "CapsLock", KeyboardEvent.DOM_VK_CAPS_LOCK, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Shift keys + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LSHIFT, + modifiers:{shiftKey:1}, chars:""}, + "Shift", "ShiftLeft", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RSHIFT, + modifiers:{shiftRightKey:1}, chars:""}, + "Shift", "ShiftRight", KeyboardEvent.DOM_VK_SHIFT, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + + // Ctrl keys + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LCONTROL, + modifiers:{ctrlKey:1}, chars:""}, + "Control", "ControlLeft", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RCONTROL, + modifiers:{ctrlRightKey:1}, chars:""}, + "Control", "ControlRight", KeyboardEvent.DOM_VK_CONTROL, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + + // Alt keys + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LMENU, + modifiers:{altKey:1}, chars:""}, + "Alt", "AltLeft", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RMENU, + modifiers:{altRightKey:1}, chars:""}, + "Alt", "AltRight", KeyboardEvent.DOM_VK_ALT, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + + // Win keys + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LWIN, + modifiers:{metaKey:1}, chars:""}, + "Meta", "MetaLeft", KeyboardEvent.DOM_VK_WIN, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_LEFT); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RWIN, + modifiers:{metaRightKey:1}, chars:""}, + "Meta", "MetaRight", KeyboardEvent.DOM_VK_WIN, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_RIGHT); + + // all keys on keyboard (keyCode test) + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_BACK, + modifiers:{}, chars:"\u0008"}, + "Backspace", "Backspace", KeyboardEvent.DOM_VK_BACK_SPACE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_TAB, + modifiers:{}, chars:"\t"}, + "Tab", "Tab", KeyboardEvent.DOM_VK_TAB, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RETURN, + modifiers:{}, chars:"\r"}, + "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PAUSE, + modifiers:{}, chars:""}, + "Pause", "Pause", KeyboardEvent.DOM_VK_PAUSE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_KANA, + modifiers:{}, chars:""}, + "Unidentified", "", KeyboardEvent.DOM_VK_KANA, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_JUNJA, + modifiers:{}, chars:""}, + "JunjaMode", "", KeyboardEvent.DOM_VK_JUNJA, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_FINAL, + modifiers:{}, chars:""}, + "FinalMode", "", KeyboardEvent.DOM_VK_FINAL, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_KANJI, + modifiers:{}, chars:""}, + "Unidentified", "", KeyboardEvent.DOM_VK_KANJI, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_ESCAPE, + modifiers:{}, chars:""}, + "Escape", "Escape", KeyboardEvent.DOM_VK_ESCAPE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_CONVERT, + modifiers:{}, chars:""}, + "Convert", "", KeyboardEvent.DOM_VK_CONVERT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NONCONVERT, + modifiers:{}, chars:""}, + "NonConvert", "", KeyboardEvent.DOM_VK_NONCONVERT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_ACCEPT, + modifiers:{}, chars:""}, + "Accept", "", KeyboardEvent.DOM_VK_ACCEPT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_MODECHANGE, + modifiers:{}, chars:""}, + "ModeChange", "", KeyboardEvent.DOM_VK_MODECHANGE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SPACE, + modifiers:{}, chars:" "}, + " ", "Space", KeyboardEvent.DOM_VK_SPACE, " ", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Ctrl+Space causes WM_CHAR with ' '. However, its keypress event's ctrlKey state shouldn't be false. + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SPACE, + modifiers:{ctrlKey:1}, chars:" "}, + " ", "Space", KeyboardEvent.DOM_VK_SPACE, " ", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SELECT, + modifiers:{}, chars:""}, + "Select", "", KeyboardEvent.DOM_VK_SELECT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PRINT, + modifiers:{}, chars:""}, + "Unidentified", "", KeyboardEvent.DOM_VK_PRINT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_EXECUTE, + modifiers:{}, chars:""}, + "Execute", "", KeyboardEvent.DOM_VK_EXECUTE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SNAPSHOT, + modifiers:{}, chars:""}, + "PrintScreen", "PrintScreen", KeyboardEvent.DOM_VK_PRINTSCREEN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_HELP, + modifiers:{}, chars:""}, + "Help", "", KeyboardEvent.DOM_VK_HELP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SLEEP, + modifiers:{}, chars:""}, + "Standby", "", KeyboardEvent.DOM_VK_SLEEP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PRIOR, + modifiers:{}, chars:""}, + "PageUp", "PageUp", KeyboardEvent.DOM_VK_PAGE_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NEXT, + modifiers:{}, chars:""}, + "PageDown", "PageDown", KeyboardEvent.DOM_VK_PAGE_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_END, + modifiers:{}, chars:""}, + "End", "End", KeyboardEvent.DOM_VK_END, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_HOME, + modifiers:{}, chars:""}, + "Home", "Home", KeyboardEvent.DOM_VK_HOME, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_LEFT, + modifiers:{}, chars:""}, + "ArrowLeft", "ArrowLeft", KeyboardEvent.DOM_VK_LEFT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_UP, + modifiers:{}, chars:""}, + "ArrowUp", "ArrowUp", KeyboardEvent.DOM_VK_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RIGHT, + modifiers:{}, chars:""}, + "ArrowRight", "ArrowRight", KeyboardEvent.DOM_VK_RIGHT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DOWN, + modifiers:{}, chars:""}, + "ArrowDown", "ArrowDown", KeyboardEvent.DOM_VK_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_INSERT, + modifiers:{}, chars:""}, + "Insert", "Insert", KeyboardEvent.DOM_VK_INSERT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DELETE, + modifiers:{}, chars:""}, + "Delete", "Delete", KeyboardEvent.DOM_VK_DELETE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Backspace and Enter are handled with special path in mozilla::widget::NativeKey. So, let's test them with modifiers too. + // Note that when both Ctrl and Alt are pressed, they don't cause WM_(SYS)CHAR message. + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_BACK, + modifiers:{ctrlKey:1}, chars:"\u007F"}, + "Backspace", "Backspace", KeyboardEvent.DOM_VK_BACK_SPACE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_BACK, + modifiers:{altKey:1}, chars:"\u0008"}, + "Backspace", "Backspace", KeyboardEvent.DOM_VK_BACK_SPACE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_BACK, + modifiers:{ctrl:1, altKey:1}, chars:""}, + "Backspace", "Backspace", KeyboardEvent.DOM_VK_BACK_SPACE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RETURN, + modifiers:{ctrlKey:1}, chars:"\n"}, + "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RETURN, + modifiers:{altKey:1}, chars:"\r"}, + "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_RETURN, + modifiers:{ctrl:1, altKey:1}, chars:""}, + "Enter", "Enter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Even non-printable key could be mapped as a printable key. + // Only "keyup" event cannot know if it *did* cause inputting text. + // Therefore, only "keydown" and "keypress" event's key value should be the character inputted by the key. + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F4, + modifiers:{}, chars:"a"}, + ["a", "a", "F4"], "F4", KeyboardEvent.DOM_VK_F4, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Even if key message is processed by IME, when the key causes inputting text, + // keypress event(s) should be fired. + const WIN_VK_PROCESSKEY_WITH_SC_A = WIN_VK_PROCESSKEY | 0x001E0000; + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PROCESSKEY_WITH_SC_A, + modifiers:{}, chars:"a"}, + ["a", "a", "Process"], "KeyA", KeyboardEvent.DOM_VK_PROCESSKEY, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_PROCESSKEY_WITH_SC_A, + modifiers:{altKey:1}, chars:"a"}, + ["a", "a", "Process"], "KeyA", KeyboardEvent.DOM_VK_PROCESSKEY, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // US + // Alphabet + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{}, chars:"a"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{shiftKey:1}, chars:"A"}, + "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1}, chars:"\u0001"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001"}, + "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{altKey:1}, chars:"a"}, + "a", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{altKey:1, shiftKey:1}, chars:"A"}, + "A", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B, + modifiers:{}, chars:"b"}, + "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B, + modifiers:{shiftKey:1}, chars:"B"}, + "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B, + modifiers:{ctrlKey:1}, chars:"\u0002"}, + "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0002"}, + "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B, + modifiers:{altKey:1}, chars:"b"}, + "b", "KeyB", KeyboardEvent.DOM_VK_B, "b", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_B, + modifiers:{altKey:1, shiftKey:1}, chars:"B"}, + "B", "KeyB", KeyboardEvent.DOM_VK_B, "B", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C, + modifiers:{}, chars:"c"}, + "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C, + modifiers:{shiftKey:1}, chars:"C"}, + "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C, + modifiers:{ctrlKey:1}, chars:"\u0003"}, + "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0003"}, + "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C, + modifiers:{altKey:1}, chars:"c"}, + "c", "KeyC", KeyboardEvent.DOM_VK_C, "c", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_C, + modifiers:{altKey:1, shiftKey:1}, chars:"C"}, + "C", "KeyC", KeyboardEvent.DOM_VK_C, "C", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D, + modifiers:{}, chars:"d"}, + "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D, + modifiers:{shiftKey:1}, chars:"D"}, + "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D, + modifiers:{ctrlKey:1}, chars:"\u0004"}, + "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0004"}, + "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D, + modifiers:{altKey:1}, chars:"d"}, + "d", "KeyD", KeyboardEvent.DOM_VK_D, "d", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_D, + modifiers:{altKey:1, shiftKey:1}, chars:"D"}, + "D", "KeyD", KeyboardEvent.DOM_VK_D, "D", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E, + modifiers:{}, chars:"e"}, + "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E, + modifiers:{shiftKey:1}, chars:"E"}, + "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E, + modifiers:{ctrlKey:1}, chars:"\u0005"}, + "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0005"}, + "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E, + modifiers:{altKey:1}, chars:"e"}, + "e", "KeyE", KeyboardEvent.DOM_VK_E, "e", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_E, + modifiers:{altKey:1, shiftKey:1}, chars:"E"}, + "E", "KeyE", KeyboardEvent.DOM_VK_E, "E", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F, + modifiers:{}, chars:"f"}, + "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F, + modifiers:{shiftKey:1}, chars:"F"}, + "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F, + modifiers:{ctrlKey:1}, chars:"\u0006"}, + "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0006"}, + "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F, + modifiers:{altKey:1}, chars:"f"}, + "f", "KeyF", KeyboardEvent.DOM_VK_F, "f", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F, + modifiers:{altKey:1, shiftKey:1}, chars:"F"}, + "F", "KeyF", KeyboardEvent.DOM_VK_F, "F", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G, + modifiers:{}, chars:"g"}, + "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G, + modifiers:{shiftKey:1}, chars:"G"}, + "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G, + modifiers:{ctrlKey:1}, chars:"\u0007"}, + "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0007"}, + "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G, + modifiers:{altKey:1}, chars:"g"}, + "g", "KeyG", KeyboardEvent.DOM_VK_G, "g", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_G, + modifiers:{altKey:1, shiftKey:1}, chars:"G"}, + "G", "KeyG", KeyboardEvent.DOM_VK_G, "G", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H, + modifiers:{}, chars:"h"}, + "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H, + modifiers:{shiftKey:1}, chars:"H"}, + "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H, + modifiers:{ctrlKey:1}, chars:"\u0008"}, + "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0008"}, + "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H, + modifiers:{altKey:1}, chars:"h"}, + "h", "KeyH", KeyboardEvent.DOM_VK_H, "h", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_H, + modifiers:{altKey:1, shiftKey:1}, chars:"H"}, + "H", "KeyH", KeyboardEvent.DOM_VK_H, "H", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I, + modifiers:{}, chars:"i"}, + "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I, + modifiers:{shiftKey:1}, chars:"I"}, + "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I, + modifiers:{ctrlKey:1}, chars:"\u0009"}, + "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0009"}, + "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I, + modifiers:{altKey:1}, chars:"i"}, + "i", "KeyI", KeyboardEvent.DOM_VK_I, "i", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_I, + modifiers:{altKey:1, shiftKey:1}, chars:"I"}, + "I", "KeyI", KeyboardEvent.DOM_VK_I, "I", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J, + modifiers:{}, chars:"j"}, + "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J, + modifiers:{shiftKey:1}, chars:"J"}, + "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J, + modifiers:{ctrlKey:1}, chars:"\u000A"}, + "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000A"}, + "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J, + modifiers:{altKey:1}, chars:"j"}, + "j", "KeyJ", KeyboardEvent.DOM_VK_J, "j", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_J, + modifiers:{altKey:1, shiftKey:1}, chars:"J"}, + "J", "KeyJ", KeyboardEvent.DOM_VK_J, "J", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K, + modifiers:{}, chars:"k"}, + "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K, + modifiers:{shiftKey:1}, chars:"K"}, + "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K, + modifiers:{ctrlKey:1}, chars:"\u000B"}, + "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000B"}, + "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K, + modifiers:{altKey:1}, chars:"k"}, + "k", "KeyK", KeyboardEvent.DOM_VK_K, "k", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_K, + modifiers:{altKey:1, shiftKey:1}, chars:"K"}, + "K", "KeyK", KeyboardEvent.DOM_VK_K, "K", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L, + modifiers:{}, chars:"l"}, + "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L, + modifiers:{shiftKey:1}, chars:"L"}, + "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L, + modifiers:{ctrlKey:1}, chars:"\u000C"}, + "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000C"}, + "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L, + modifiers:{altKey:1}, chars:"l"}, + "l", "KeyL", KeyboardEvent.DOM_VK_L, "l", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_L, + modifiers:{altKey:1, shiftKey:1}, chars:"L"}, + "L", "KeyL", KeyboardEvent.DOM_VK_L, "L", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M, + modifiers:{}, chars:"m"}, + "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M, + modifiers:{shiftKey:1}, chars:"M"}, + "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M, + modifiers:{ctrlKey:1}, chars:"\u000D"}, + "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000D"}, + "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M, + modifiers:{altKey:1}, chars:"m"}, + "m", "KeyM", KeyboardEvent.DOM_VK_M, "m", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_M, + modifiers:{altKey:1, shiftKey:1}, chars:"M"}, + "M", "KeyM", KeyboardEvent.DOM_VK_M, "M", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N, + modifiers:{}, chars:"n"}, + "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N, + modifiers:{shiftKey:1}, chars:"N"}, + "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N, + modifiers:{ctrlKey:1}, chars:"\u000E"}, + "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000E"}, + "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N, + modifiers:{altKey:1}, chars:"n"}, + "n", "KeyN", KeyboardEvent.DOM_VK_N, "n", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_N, + modifiers:{altKey:1, shiftKey:1}, chars:"N"}, + "N", "KeyN", KeyboardEvent.DOM_VK_N, "N", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O, + modifiers:{}, chars:"o"}, + "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O, + modifiers:{shiftKey:1}, chars:"O"}, + "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O, + modifiers:{ctrlKey:1}, chars:"\u000F"}, + "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u000F"}, + "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O, + modifiers:{altKey:1}, chars:"o"}, + "o", "KeyO", KeyboardEvent.DOM_VK_O, "o", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_O, + modifiers:{altKey:1, shiftKey:1}, chars:"O"}, + "O", "KeyO", KeyboardEvent.DOM_VK_O, "O", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P, + modifiers:{}, chars:"p"}, + "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P, + modifiers:{shiftKey:1}, chars:"P"}, + "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P, + modifiers:{ctrlKey:1}, chars:"\u0010"}, + "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0010"}, + "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P, + modifiers:{altKey:1}, chars:"p"}, + "p", "KeyP", KeyboardEvent.DOM_VK_P, "p", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_P, + modifiers:{altKey:1, shiftKey:1}, chars:"P"}, + "P", "KeyP", KeyboardEvent.DOM_VK_P, "P", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q, + modifiers:{}, chars:"q"}, + "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q, + modifiers:{shiftKey:1}, chars:"Q"}, + "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q, + modifiers:{ctrlKey:1}, chars:"\u0011"}, + "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0011"}, + "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q, + modifiers:{altKey:1}, chars:"q"}, + "q", "KeyQ", KeyboardEvent.DOM_VK_Q, "q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Q, + modifiers:{altKey:1, shiftKey:1}, chars:"Q"}, + "Q", "KeyQ", KeyboardEvent.DOM_VK_Q, "Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R, + modifiers:{}, chars:"r"}, + "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R, + modifiers:{shiftKey:1}, chars:"R"}, + "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R, + modifiers:{ctrlKey:1}, chars:"\u0012"}, + "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0012"}, + "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R, + modifiers:{altKey:1}, chars:"r"}, + "r", "KeyR", KeyboardEvent.DOM_VK_R, "r", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_R, + modifiers:{altKey:1, shiftKey:1}, chars:"R"}, + "R", "KeyR", KeyboardEvent.DOM_VK_R, "R", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S, + modifiers:{}, chars:"s"}, + "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S, + modifiers:{shiftKey:1}, chars:"S"}, + "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S, + modifiers:{ctrlKey:1}, chars:"\u0013"}, + "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0013"}, + "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S, + modifiers:{altKey:1}, chars:"s"}, + "s", "KeyS", KeyboardEvent.DOM_VK_S, "s", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_S, + modifiers:{altKey:1, shiftKey:1}, chars:"S"}, + "S", "KeyS", KeyboardEvent.DOM_VK_S, "S", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T, + modifiers:{}, chars:"t"}, + "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T, + modifiers:{shiftKey:1}, chars:"T"}, + "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T, + modifiers:{ctrlKey:1}, chars:"\u0014"}, + "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0014"}, + "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T, + modifiers:{altKey:1}, chars:"t"}, + "t", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T, + modifiers:{altKey:1, shiftKey:1}, chars:"T"}, + "T", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U, + modifiers:{}, chars:"u"}, + "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U, + modifiers:{shiftKey:1}, chars:"U"}, + "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U, + modifiers:{ctrlKey:1}, chars:"\u0015"}, + "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0015"}, + "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U, + modifiers:{altKey:1}, chars:"u"}, + "u", "KeyU", KeyboardEvent.DOM_VK_U, "u", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_U, + modifiers:{altKey:1, shiftKey:1}, chars:"U"}, + "U", "KeyU", KeyboardEvent.DOM_VK_U, "U", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V, + modifiers:{}, chars:"v"}, + "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V, + modifiers:{shiftKey:1}, chars:"V"}, + "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V, + modifiers:{ctrlKey:1}, chars:"\u0016"}, + "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0016"}, + "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V, + modifiers:{altKey:1}, chars:"v"}, + "v", "KeyV", KeyboardEvent.DOM_VK_V, "v", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_V, + modifiers:{altKey:1, shiftKey:1}, chars:"V"}, + "V", "KeyV", KeyboardEvent.DOM_VK_V, "V", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W, + modifiers:{}, chars:"w"}, + "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W, + modifiers:{shiftKey:1}, chars:"W"}, + "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W, + modifiers:{ctrlKey:1}, chars:"\u0017"}, + "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0017"}, + "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W, + modifiers:{altKey:1}, chars:"w"}, + "w", "KeyW", KeyboardEvent.DOM_VK_W, "w", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_W, + modifiers:{altKey:1, shiftKey:1}, chars:"W"}, + "W", "KeyW", KeyboardEvent.DOM_VK_W, "W", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X, + modifiers:{}, chars:"x"}, + "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X, + modifiers:{shiftKey:1}, chars:"X"}, + "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X, + modifiers:{ctrlKey:1}, chars:"\u0018"}, + "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018"}, + "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X, + modifiers:{altKey:1}, chars:"x"}, + "x", "KeyX", KeyboardEvent.DOM_VK_X, "x", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X, + modifiers:{altKey:1, shiftKey:1}, chars:"X"}, + "X", "KeyX", KeyboardEvent.DOM_VK_X, "X", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y, + modifiers:{}, chars:"y"}, + "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y, + modifiers:{shiftKey:1}, chars:"Y"}, + "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y, + modifiers:{ctrlKey:1}, chars:"\u0019"}, + "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0019"}, + "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y, + modifiers:{altKey:1}, chars:"y"}, + "y", "KeyY", KeyboardEvent.DOM_VK_Y, "y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Y, + modifiers:{altKey:1, shiftKey:1}, chars:"Y"}, + "Y", "KeyY", KeyboardEvent.DOM_VK_Y, "Y", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z, + modifiers:{}, chars:"z"}, + "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z, + modifiers:{shiftKey:1}, chars:"Z"}, + "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z, + modifiers:{ctrlKey:1}, chars:"\u001A"}, + "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001A"}, + "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z, + modifiers:{altKey:1}, chars:"z"}, + "z", "KeyZ", KeyboardEvent.DOM_VK_Z, "z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_Z, + modifiers:{altKey:1, shiftKey:1}, chars:"Z"}, + "Z", "KeyZ", KeyboardEvent.DOM_VK_Z, "Z", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Numeric + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0, + modifiers:{}, chars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0, + modifiers:{shiftKey:1}, chars:")"}, + ")", "Digit0", KeyboardEvent.DOM_VK_0, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0, + modifiers:{ctrlKey:1}, chars:""}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + ")", "Digit0", KeyboardEvent.DOM_VK_0, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0, + modifiers:{altKey:1}, chars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_0, + modifiers:{altKey:1, shiftKey:1}, chars:")"}, + ")", "Digit0", KeyboardEvent.DOM_VK_0, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1, + modifiers:{}, chars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1, + modifiers:{shiftKey:1}, chars:"!"}, + "!", "Digit1", KeyboardEvent.DOM_VK_1, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1, + modifiers:{ctrlKey:1}, chars:""}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "!", "Digit1", KeyboardEvent.DOM_VK_1, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1, + modifiers:{altKey:1}, chars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_1, + modifiers:{altKey:1, shiftKey:1}, chars:"!"}, + "!", "Digit1", KeyboardEvent.DOM_VK_1, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2, + modifiers:{}, chars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2, + modifiers:{shiftKey:1}, chars:"@"}, + "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2, + modifiers:{ctrlKey:1}, chars:""}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2, + modifiers:{altKey:1}, chars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_2, + modifiers:{altKey:1, shiftKey:1}, chars:"@"}, + "@", "Digit2", KeyboardEvent.DOM_VK_2, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3, + modifiers:{}, chars:"3"}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3, + modifiers:{shiftKey:1}, chars:"#"}, + "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3, + modifiers:{ctrlKey:1}, chars:""}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3, + modifiers:{altKey:1}, chars:"3"}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_3, + modifiers:{altKey:1, shiftKey:1}, chars:"#"}, + "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4, + modifiers:{}, chars:"4"}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4, + modifiers:{shiftKey:1}, chars:"$"}, + "$", "Digit4", KeyboardEvent.DOM_VK_4, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4, + modifiers:{ctrlKey:1}, chars:""}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "$", "Digit4", KeyboardEvent.DOM_VK_4, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4, + modifiers:{altKey:1}, chars:"4"}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_4, + modifiers:{altKey:1, shiftKey:1}, chars:"$"}, + "$", "Digit4", KeyboardEvent.DOM_VK_4, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5, + modifiers:{}, chars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5, + modifiers:{shiftKey:1}, chars:"%"}, + "%", "Digit5", KeyboardEvent.DOM_VK_5, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5, + modifiers:{ctrlKey:1}, chars:""}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "%", "Digit5", KeyboardEvent.DOM_VK_5, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5, + modifiers:{altKey:1}, chars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_5, + modifiers:{altKey:1, shiftKey:1}, chars:"%"}, + "%", "Digit5", KeyboardEvent.DOM_VK_5, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6, + modifiers:{}, chars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6, + modifiers:{shiftKey:1}, chars:"^"}, + "^", "Digit6", KeyboardEvent.DOM_VK_6, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6, + modifiers:{ctrlKey:1}, chars:""}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001E"}, + "^", "Digit6", KeyboardEvent.DOM_VK_6, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6, + modifiers:{altKey:1}, chars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_6, + modifiers:{altKey:1, shiftKey:1}, chars:"^"}, + "^", "Digit6", KeyboardEvent.DOM_VK_6, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7, + modifiers:{}, chars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7, + modifiers:{shiftKey:1}, chars:"&"}, + "&", "Digit7", KeyboardEvent.DOM_VK_7, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7, + modifiers:{ctrlKey:1}, chars:""}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "&", "Digit7", KeyboardEvent.DOM_VK_7, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7, + modifiers:{altKey:1}, chars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_7, + modifiers:{altKey:1, shiftKey:1}, chars:"&"}, + "&", "Digit7", KeyboardEvent.DOM_VK_7, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8, + modifiers:{}, chars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8, + modifiers:{shiftKey:1}, chars:"*"}, + "*", "Digit8", KeyboardEvent.DOM_VK_8, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8, + modifiers:{ctrlKey:1, }, chars:""}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "*", "Digit8", KeyboardEvent.DOM_VK_8, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8, + modifiers:{altKey:1}, chars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_8, + modifiers:{altKey:1, shiftKey:1}, chars:"*"}, + "*", "Digit8", KeyboardEvent.DOM_VK_8, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9, + modifiers:{}, chars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9, + modifiers:{shiftKey:1}, chars:"("}, + "(", "Digit9", KeyboardEvent.DOM_VK_9, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9, + modifiers:{ctrlKey:1}, chars:""}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "(", "Digit9", KeyboardEvent.DOM_VK_9, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9, + modifiers:{altKey:1}, chars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_9, + modifiers:{altKey:1, shiftKey:1}, chars:"("}, + "(", "Digit9", KeyboardEvent.DOM_VK_9, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // OEM keys + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS, + modifiers:{}, chars:"-"}, + "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS, + modifiers:{shiftKey:1}, chars:"_"}, + "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS, + modifiers:{ctrlKey:1}, chars:""}, + "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u001F"}, + "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS, + modifiers:{altKey:1}, chars:"-"}, + "-", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_MINUS, + modifiers:{altKey:1, shiftKey:1}, chars:"_"}, + "_", "Minus", KeyboardEvent.DOM_VK_HYPHEN_MINUS, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS, + modifiers:{}, chars:"="}, + "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS, + modifiers:{shiftKey:1}, chars:"+"}, + "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS, + modifiers:{ctrlKey:1}, chars:""}, + "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS, + modifiers:{altKey:1}, chars:"="}, + "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PLUS, + modifiers:{altKey:1, shiftKey:1}, chars:"+"}, + "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4, + modifiers:{}, chars:"["}, + "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4, + modifiers:{shiftKey:1}, chars:"{"}, + "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4, + modifiers:{ctrlKey:1}, chars:"\u001B"}, + "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4, + modifiers:{altKey:1}, chars:"["}, + "[", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_4, + modifiers:{altKey:1, shiftKey:1}, chars:"{"}, + "{", "BracketLeft", KeyboardEvent.DOM_VK_OPEN_BRACKET, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6, + modifiers:{}, chars:"]"}, + "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6, + modifiers:{shiftKey:1}, chars:"}"}, + "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6, + modifiers:{ctrlKey:1}, chars:"\u001D"}, + "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6, + modifiers:{altKey:1}, chars:"]"}, + "]", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_6, + modifiers:{altKey:1, shiftKey:1}, chars:"}"}, + "}", "BracketRight", KeyboardEvent.DOM_VK_CLOSE_BRACKET, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1, + modifiers:{}, chars:";"}, + ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1, + modifiers:{shiftKey:1}, chars:":"}, + ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1, + modifiers:{ctrlKey:1}, chars:""}, + ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1}, chars:";"}, + ";", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1, shiftKey:1}, chars:":"}, + ":", "Semicolon", KeyboardEvent.DOM_VK_SEMICOLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7, + modifiers:{}, chars:"'"}, + "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7, + modifiers:{shiftKey:1}, chars:"\""}, + "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7, + modifiers:{ctrlKey:1}, chars:""}, + "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7, + modifiers:{altKey:1}, chars:"'"}, + "'", "Quote", KeyboardEvent.DOM_VK_QUOTE, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7, + modifiers:{altKey:1, shiftKey:1}, chars:"\""}, + "\"", "Quote", KeyboardEvent.DOM_VK_QUOTE, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5, + modifiers:{}, chars:"\\"}, + "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5, + modifiers:{shiftKey:1}, chars:"|"}, + "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5, + modifiers:{ctrlKey:1}, chars:"\u001C"}, + "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5, + modifiers:{altKey:1}, chars:"\\"}, + "\\", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_5, + modifiers:{altKey:1, shiftKey:1}, chars:"|"}, + "|", "Backslash", KeyboardEvent.DOM_VK_BACK_SLASH, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA, + modifiers:{}, chars:","}, + ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA, + modifiers:{shiftKey:1}, chars:"<"}, + "<", "Comma", KeyboardEvent.DOM_VK_COMMA, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA, + modifiers:{ctrlKey:1}, chars:""}, + ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "<", "Comma", KeyboardEvent.DOM_VK_COMMA, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA, + modifiers:{altKey:1}, chars:","}, + ",", "Comma", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_COMMA, + modifiers:{altKey:1, shiftKey:1}, chars:"<"}, + "<", "Comma", KeyboardEvent.DOM_VK_COMMA, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{}, chars:"."}, + ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{shiftKey:1}, chars:">"}, + ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{ctrlKey:1}, chars:""}, + ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{altKey:1}, chars:"."}, + ".", "Period", KeyboardEvent.DOM_VK_PERIOD, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{altKey:1, shiftKey:1}, chars:">"}, + ">", "Period", KeyboardEvent.DOM_VK_PERIOD, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2, + modifiers:{}, chars:"/"}, + "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2, + modifiers:{shiftKey:1}, chars:"?"}, + "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2, + modifiers:{ctrlKey:1}, chars:""}, + "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2, + modifiers:{altKey:1}, chars:"/"}, + "/", "Slash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_2, + modifiers:{altKey:1, shiftKey:1}, chars:"?"}, + "?", "Slash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3, + modifiers:{}, chars:"`"}, + "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3, + modifiers:{shiftKey:1}, chars:"~"}, + "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "~", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3, + modifiers:{ctrlKey:1}, chars:""}, + "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "~", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3, + modifiers:{altKey:1}, chars:"`"}, + "`", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "`", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_3, + modifiers:{altKey:1, shiftKey:1}, chars:"~"}, + "~", "Backquote", KeyboardEvent.DOM_VK_BACK_QUOTE, "~", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Numpad + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD0, + modifiers:{numLockKey:1}, chars:"0"}, + "0", "Numpad0", KeyboardEvent.DOM_VK_NUMPAD0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD1, + modifiers:{numLockKey:1}, chars:"1"}, + "1", "Numpad1", KeyboardEvent.DOM_VK_NUMPAD1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD2, + modifiers:{numLockKey:1}, chars:"2"}, + "2", "Numpad2", KeyboardEvent.DOM_VK_NUMPAD2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD3, + modifiers:{numLockKey:1}, chars:"3"}, + "3", "Numpad3", KeyboardEvent.DOM_VK_NUMPAD3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD4, + modifiers:{numLockKey:1}, chars:"4"}, + "4", "Numpad4", KeyboardEvent.DOM_VK_NUMPAD4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD5, + modifiers:{numLockKey:1}, chars:"5"}, + "5", "Numpad5", KeyboardEvent.DOM_VK_NUMPAD5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD6, + modifiers:{numLockKey:1}, chars:"6"}, + "6", "Numpad6", KeyboardEvent.DOM_VK_NUMPAD6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD7, + modifiers:{numLockKey:1}, chars:"7"}, + "7", "Numpad7", KeyboardEvent.DOM_VK_NUMPAD7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD8, + modifiers:{numLockKey:1}, chars:"8"}, + "8", "Numpad8", KeyboardEvent.DOM_VK_NUMPAD8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD9, + modifiers:{numLockKey:1}, chars:"9"}, + "9", "Numpad9", KeyboardEvent.DOM_VK_NUMPAD9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_MULTIPLY, + modifiers:{numLockKey:1}, chars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_MULTIPLY, + modifiers:{numLockKey:1, shiftKey:1}, chars:"*"}, + "*", "NumpadMultiply", KeyboardEvent.DOM_VK_MULTIPLY, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_ADD, + modifiers:{numLockKey:1}, chars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_ADD, + modifiers:{numLockKey:1, shiftKey:1}, chars:"+"}, + "+", "NumpadAdd", KeyboardEvent.DOM_VK_ADD, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + // VK_SEPARATOR is keycode for NEC's PC-98 series whose keyboard layout was + // different from current PC's keyboard layout and it cannot connect to + // current PC. Note that even if we synthesize WM_KEYDOWN with + // VK_SEPARATOR, it doesn't work on Win7. + //yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SEPARATOR, + // modifiers:{numLockKey:1}, chars:""}, + // "", "", KeyboardEvent.DOM_VK_SEPARATOR, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + //yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SEPARATOR, + // modifiers:{numLockKey:1, shiftKey:1}, chars:""}, + // "", "", KeyboardEvent.DOM_VK_SEPARATOR, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SUBTRACT, + modifiers:{numLockKey:1}, chars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_SUBTRACT, + modifiers:{numLockKey:1, shiftKey:1}, chars:"-"}, + "-", "NumpadSubtract", KeyboardEvent.DOM_VK_SUBTRACT, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DECIMAL, + modifiers:{numLockKey:1}, chars:"."}, + ".", "NumpadDecimal", KeyboardEvent.DOM_VK_DECIMAL, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DECIMAL, + modifiers:{numLockKey:1, shiftKey:1}, chars:"."}, + ".", "NumpadDecimal", KeyboardEvent.DOM_VK_DECIMAL, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DIVIDE, + modifiers:{numLockKey:1}, chars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_DIVIDE, + modifiers:{numLockKey:1, shiftKey:1}, chars:"/"}, + "/", "NumpadDivide", KeyboardEvent.DOM_VK_DIVIDE, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_RETURN, + modifiers:{numLockKey:1}, chars:"\r"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_RETURN, + modifiers:{numLockKey:1, shiftKey:1}, chars:"\r"}, + "Enter", "NumpadEnter", KeyboardEvent.DOM_VK_RETURN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + + // Numpad without NumLock + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_PRIOR, + modifiers:{}, chars:""}, + "PageUp", "Numpad9", KeyboardEvent.DOM_VK_PAGE_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_NEXT, + modifiers:{}, chars:""}, + "PageDown", "Numpad3", KeyboardEvent.DOM_VK_PAGE_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_END, + modifiers:{}, chars:""}, + "End", "Numpad1", KeyboardEvent.DOM_VK_END, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_HOME, + modifiers:{}, chars:""}, + "Home", "Numpad7", KeyboardEvent.DOM_VK_HOME, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_LEFT, + modifiers:{}, chars:""}, + "ArrowLeft", "Numpad4", KeyboardEvent.DOM_VK_LEFT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_UP, + modifiers:{}, chars:""}, + "ArrowUp", "Numpad8", KeyboardEvent.DOM_VK_UP, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_RIGHT, + modifiers:{}, chars:""}, + "ArrowRight", "Numpad6", KeyboardEvent.DOM_VK_RIGHT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_DOWN, + modifiers:{}, chars:""}, + "ArrowDown", "Numpad2", KeyboardEvent.DOM_VK_DOWN, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_INSERT, + modifiers:{}, chars:""}, + "Insert", "Numpad0", KeyboardEvent.DOM_VK_INSERT, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_NUMPAD_DELETE, + modifiers:{}, chars:""}, + "Delete", "NumpadDecimal", KeyboardEvent.DOM_VK_DELETE, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_CLEAR, + modifiers:{}, chars:""}, + "Clear", "Numpad5", KeyboardEvent.DOM_VK_CLEAR, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + + // Even if widget receives unknown keycode, it should dispatch key events + // whose keycode is 0 rather than native keycode. + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:0x3A, + modifiers:{numLockKey:1}, chars:""}, + "Unidentified", "", 0, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // French + // Numeric + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0, + modifiers:{}, chars:"\u00E0"}, + "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0, + modifiers:{shiftKey:1}, chars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1, + modifiers:{}, chars:"&"}, + "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1, + modifiers:{shiftKey:1}, chars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2, + modifiers:{}, chars:"\u00E9"}, + "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "\u00E9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2, + modifiers:{shiftKey:1}, chars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3, + modifiers:{}, chars:"\""}, + "\"", "Digit3", KeyboardEvent.DOM_VK_3, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3, + modifiers:{shiftKey:1}, chars:"3"}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4, + modifiers:{}, chars:"'"}, + "'", "Digit4", KeyboardEvent.DOM_VK_4, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4, + modifiers:{shiftKey:1}, chars:"4"}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5, + modifiers:{}, chars:"("}, + "(", "Digit5", KeyboardEvent.DOM_VK_5, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5, + modifiers:{shiftKey:1}, chars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6, + modifiers:{}, chars:"-"}, + "-", "Digit6", KeyboardEvent.DOM_VK_6, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6, + modifiers:{shiftKey:1}, chars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7, + modifiers:{}, chars:"\u00E8"}, + "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "\u00E8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7, + modifiers:{shiftKey:1}, chars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8, + modifiers:{}, chars:"_"}, + "_", "Digit8", KeyboardEvent.DOM_VK_8, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8, + modifiers:{shiftKey:1}, chars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9, + modifiers:{}, chars:"\u00E7"}, + "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00E7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9, + modifiers:{shiftKey:1}, chars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Numeric with ShiftLock + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0, + modifiers:{capsLockKey:1}, chars:"0"}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00E0"}, + "\u00E0", "Digit0", KeyboardEvent.DOM_VK_0, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1, + modifiers:{capsLockKey:1}, chars:"1"}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"&"}, + "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2, + modifiers:{capsLockKey:1}, chars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00E9"}, + "\u00E9", "Digit2", KeyboardEvent.DOM_VK_2, "\u00E9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3, + modifiers:{capsLockKey:1}, chars:"3"}, + "3", "Digit3", KeyboardEvent.DOM_VK_3, "3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"\""}, + "\"", "Digit3", KeyboardEvent.DOM_VK_3, "\"", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4, + modifiers:{capsLockKey:1}, chars:"4"}, + "4", "Digit4", KeyboardEvent.DOM_VK_4, "4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"'"}, + "'", "Digit4", KeyboardEvent.DOM_VK_4, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5, + modifiers:{capsLockKey:1}, chars:"5"}, + "5", "Digit5", KeyboardEvent.DOM_VK_5, "5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"("}, + "(", "Digit5", KeyboardEvent.DOM_VK_5, "(", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6, + modifiers:{capsLockKey:1}, chars:"6"}, + "6", "Digit6", KeyboardEvent.DOM_VK_6, "6", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"-"}, + "-", "Digit6", KeyboardEvent.DOM_VK_6, "-", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7, + modifiers:{capsLockKey:1}, chars:"7"}, + "7", "Digit7", KeyboardEvent.DOM_VK_7, "7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00E8"}, + "\u00E8", "Digit7", KeyboardEvent.DOM_VK_7, "\u00E8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8, + modifiers:{capsLockKey:1}, chars:"8"}, + "8", "Digit8", KeyboardEvent.DOM_VK_8, "8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"_"}, + "_", "Digit8", KeyboardEvent.DOM_VK_8, "_", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9, + modifiers:{capsLockKey:1}, chars:"9"}, + "9", "Digit9", KeyboardEvent.DOM_VK_9, "9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00E7"}, + "\u00E7", "Digit9", KeyboardEvent.DOM_VK_9, "\u00E7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // OEM keys + // If the key doesn't cause ASCII character even with or without Shift key, keyCode value should be same as + // the key which causes the virtual keycode on ANSI keyboard layout. + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_7, + modifiers:{}, chars:"\u00B2"}, + "\u00B2", "Backquote", KeyboardEvent.DOM_VK_QUOTE, "\u00B2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_7, + modifiers:{shiftKey:1}, chars:""}, + "", "Backquote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4, + modifiers:{}, chars:")"}, + ")", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4, + modifiers:{shiftKey:1}, chars:"\u00B0"}, + "\u00B0", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, "\u00B0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS, + modifiers:{}, chars:"="}, + "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS, + modifiers:{shiftKey:1}, chars:"+"}, + "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + // modifiers:{}, chars:""}, + // "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key + //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + // modifiers:{shiftKey:1}, chars:""}, + // ["^^", "^", "^", "^"], "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_1, + modifiers:{}, chars:"$"}, + "$", "BracketRight", KeyboardEvent.DOM_VK_DOLLAR, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_1, + modifiers:{shiftKey:1}, chars:"\u00A3"}, + "\u00A3", "BracketRight", KeyboardEvent.DOM_VK_DOLLAR, "\u00A3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_3, + modifiers:{}, chars:"\u00F9"}, + "\u00F9", "Quote", KeyboardEvent.DOM_VK_PERCENT, "\u00F9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_3, + modifiers:{shiftKey:1}, chars:"%"}, + "%", "Quote", KeyboardEvent.DOM_VK_PERCENT, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_5, + modifiers:{}, chars:"*"}, + "*", "Backslash", KeyboardEvent.DOM_VK_ASTERISK, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_5, + modifiers:{shiftKey:1}, chars:"\u00B5"}, + "\u00B5", "Backslash", KeyboardEvent.DOM_VK_ASTERISK, "\u00B5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_102, + modifiers:{}, chars:"<"}, + "<", "IntlBackslash", KeyboardEvent.DOM_VK_LESS_THAN, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_102, + modifiers:{shiftKey:1}, chars:">"}, + ">", "IntlBackslash", KeyboardEvent.DOM_VK_LESS_THAN, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_COMMA, + modifiers:{}, chars:","}, + ",", "KeyM", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_COMMA, + modifiers:{shiftKey:1}, chars:"?"}, + "?", "KeyM", KeyboardEvent.DOM_VK_COMMA, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{}, chars:";"}, + ";", "Comma", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{shiftKey:1}, chars:"."}, + ".", "Comma", KeyboardEvent.DOM_VK_SEMICOLON, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_2, + modifiers:{}, chars:":"}, + ":", "Period", KeyboardEvent.DOM_VK_COLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_2, + modifiers:{shiftKey:1}, chars:"/"}, + "/", "Period", KeyboardEvent.DOM_VK_COLON, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_8, + modifiers:{}, chars:"!"}, + "!", "Slash", KeyboardEvent.DOM_VK_EXCLAMATION, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_8, + modifiers:{shiftKey:1}, chars:"\u00A7"}, + "\u00A7", "Slash", KeyboardEvent.DOM_VK_EXCLAMATION, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // OEM keys with ShiftLock + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_7, + modifiers:{capsLockKey:1}, chars:"\u00B2"}, + "\u00B2", "Backquote", KeyboardEvent.DOM_VK_QUOTE, "\u00B2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_7, + modifiers:{capsLockKey:1, shiftKey:1}, chars:""}, + "", "Backquote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4, + modifiers:{capsLockKey:1}, chars:"\u00B0"}, + "\u00B0", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, "\u00B0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4, + modifiers:{capsLockKey:1, shiftKey:1}, chars:")"}, + ")", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, ")", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS, + modifiers:{capsLockKey:1}, chars:"+"}, + "+", "Equal", KeyboardEvent.DOM_VK_EQUALS, "+", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"="}, + "=", "Equal", KeyboardEvent.DOM_VK_EQUALS, "=", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + // modifiers:{capsLockKey:1}, chars:""}, + // "Dead", "BracketLeft", KeyboardLayout.DOM_VK_CLOSE_BRACKET, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key + //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + // modifiers:{capsLockKey:1, shiftKey:1}, chars:""}, + // ["\u00A8\u00A8", "\u00A8", "\u00A8", "\u00A8"], "BracketLeft", KeyboardLayout.DOM_VK_CLOSE_BRACKET, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_1, + modifiers:{capsLockKey:1}, chars:"\u00A3"}, + "\u00A3", "BracketRight", KeyboardEvent.DOM_VK_DOLLAR, "\u00A3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_1, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"$"}, + "$", "BracketRight", KeyboardEvent.DOM_VK_DOLLAR, "$", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_3, + modifiers:{capsLockKey:1}, chars:"%"}, + "%", "Quote", KeyboardEvent.DOM_VK_PERCENT, "%", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_3, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"\u00F9"}, + "\u00F9", "Quote", KeyboardEvent.DOM_VK_PERCENT, "\u00F9", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_5, + modifiers:{capsLockKey:1}, chars:"\u00B5"}, + "\u00B5", "Backslash", KeyboardEvent.DOM_VK_ASTERISK, "\u00B5", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_5, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"*"}, + "*", "Backslash", KeyboardEvent.DOM_VK_ASTERISK, "*", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_102, + modifiers:{capsLockKey:1}, chars:"<"}, + "<", "IntlBackslash", KeyboardEvent.DOM_VK_LESS_THAN, "<", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_102, + modifiers:{capsLockKey:1, shiftKey:1}, chars:">"}, + ">", "IntlBackslash", KeyboardEvent.DOM_VK_LESS_THAN, ">", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_COMMA, + modifiers:{capsLockKey:1}, chars:"?"}, + "?", "KeyM", KeyboardEvent.DOM_VK_COMMA, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_COMMA, + modifiers:{capsLockKey:1, shiftKey:1}, chars:","}, + ",", "KeyM", KeyboardEvent.DOM_VK_COMMA, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{capsLockKey:1}, chars:"."}, + ".", "Comma", KeyboardEvent.DOM_VK_SEMICOLON, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{capsLockKey:1, shiftKey:1}, chars:";"}, + ";", "Comma", KeyboardEvent.DOM_VK_SEMICOLON, ";", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_2, + modifiers:{capsLockKey:1}, chars:"/"}, + "/", "Period", KeyboardEvent.DOM_VK_COLON, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_2, + modifiers:{capsLockKey:1, shiftKey:1}, chars:":"}, + ":", "Period", KeyboardEvent.DOM_VK_COLON, ":", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_8, + modifiers:{capsLockKey:1}, chars:"\u00A7"}, + "\u00A7", "Slash", KeyboardEvent.DOM_VK_EXCLAMATION, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_8, + modifiers:{capsLockKey:1, shiftKey:1}, chars:"!"}, + "!", "Slash", KeyboardEvent.DOM_VK_EXCLAMATION, "!", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // AltGr + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0, + modifiers:{altGrKey:1}, chars:"@"}, + "@", "Digit0", KeyboardEvent.DOM_VK_0, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // AltGr + Digit1 does not cause text input in French layout. In this case, + // AltGr shouldn't be used for a modifier of shortcut. Therefore, not + // receiving `keypress` event even in the system group is fine. + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1, + modifiers:{altGrKey:1}, chars:""}, + "&", "Digit1", KeyboardEvent.DOM_VK_1, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_2, + // modifiers:{altGrKey:1}, chars:""}, + // "Dead", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_3, + modifiers:{altGrKey:1}, chars:"#"}, + "#", "Digit3", KeyboardEvent.DOM_VK_3, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_4, + modifiers:{altGrKey:1}, chars:"{"}, + "{", "Digit4", KeyboardEvent.DOM_VK_4, "{", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_5, + modifiers:{altGrKey:1}, chars:"["}, + "[", "Digit5", KeyboardEvent.DOM_VK_5, "[", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_6, + modifiers:{altGrKey:1}, chars:"|"}, + "|", "Digit6", KeyboardEvent.DOM_VK_6, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + //yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_7, + // modifiers:{altGrKey:1}, chars:""}, + // "Dead", "Digit7", KeyboardEvent.DOM_VK_7, "", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); // Dead-key + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_8, + modifiers:{altGrKey:1}, chars:"\\"}, + "\\", "Digit8", KeyboardEvent.DOM_VK_8, "\\", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_9, + modifiers:{altGrKey:1}, chars:"^"}, + "^", "Digit9", KeyboardEvent.DOM_VK_9, "^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_4, + modifiers:{altGrKey:1}, chars:"]"}, + "]", "Minus", KeyboardEvent.DOM_VK_CLOSE_PAREN, "]", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_PLUS, + modifiers:{altGrKey:1}, chars:"}"}, + "}", "Equal", KeyboardEvent.DOM_VK_EQUALS, "}", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // AltGr emulated with Ctrl and Alt + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0, + modifiers:{ctrlKey:1, altKey:1}, chars:"@", isInputtingCharacters:true}, + "@", "Digit0", KeyboardEvent.DOM_VK_0, "@", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_0, + modifiers:{ctrlKey:1, altKey:1, shiftKey:1}, chars:"", isInputtingCharacters:false}, + "0", "Digit0", KeyboardEvent.DOM_VK_0, "0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + // Different from AltGr + Digit1 case, Ctrl + Alt + Digit1 should be + // available as a shortcut key. Therefore, `keypress` event needs to be + // fired. + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1, + modifiers:{ctrlKey:1, altKey:1}, chars:"", isInputtingCharacters:false}, + "&", "Digit1", KeyboardEvent.DOM_VK_1, "&", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_1, + modifiers:{ctrlKey:1, altKey:1, shiftKey:1}, chars:"", isInputtingCharacters:false}, + "1", "Digit1", KeyboardEvent.DOM_VK_1, "1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // German + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_OEM_2, + modifiers:{}, chars:"#"}, + "#", "Backslash", KeyboardEvent.DOM_VK_HASH, "#", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_OEM_2, + modifiers:{shiftKey:1}, chars:"'"}, + "'", "Backslash", KeyboardEvent.DOM_VK_HASH, "'", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Khmer + yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, + modifiers:{}, chars:"\u17E2"}, + "\u17E2", "Digit2", KeyboardEvent.DOM_VK_2, "\u17E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, + modifiers:{shiftKey:1}, chars:"\u17D7"}, + "\u17D7", "Digit2", KeyboardEvent.DOM_VK_2, "\u17D7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, // Ctrl+2 should cause inputting Euro sign. + modifiers:{ctrlKey:1}, chars:"\u20AC", isInputtingCharacters:true}, + "\u20AC", "Digit2", KeyboardEvent.DOM_VK_2, "\u20AC", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, // Ctrl+Shift+2 shouldn't cause any input. + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "\u17D7", "Digit2", KeyboardEvent.DOM_VK_2, "\u17D7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, + modifiers:{altKey:1}, chars:"\u17E2"}, + "\u17E2", "Digit2", KeyboardEvent.DOM_VK_2, "\u17E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, + modifiers:{altKey:1, shiftKey:1}, chars:"\u17D7"}, + "\u17D7", "Digit2", KeyboardEvent.DOM_VK_2, "\u17D7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, + modifiers:{altGrKey:1}, chars:"2"}, + "2", "Digit2", KeyboardEvent.DOM_VK_2, "2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_KHMER, keyCode:WIN_VK_2, + modifiers:{altGrKey:1, shiftKey:1}, chars:"\u19E2"}, + "\u19E2", "Digit2", KeyboardEvent.DOM_VK_2, "\u19E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Norwegian + yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_OEM_5, + modifiers:{}, chars:"|"}, + "|", "Backquote", KeyboardEvent.DOM_VK_PIPE, "|", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_OEM_5, + modifiers:{shiftKey:1}, chars:"\u00A7"}, + "\u00A7", "Backquote", KeyboardEvent.DOM_VK_PIPE, "\u00A7", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // Brazilian ABNT + yield testKey({layout:KEYBOARD_LAYOUT_BRAZILIAN_ABNT, keyCode:WIN_VK_ABNT_C1, + modifiers:{}, chars:"/"}, + "/", "IntlBackslash", KeyboardEvent.DOM_VK_SLASH, "/", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_BRAZILIAN_ABNT, keyCode:WIN_VK_ABNT_C1, + modifiers:{shiftKey:1}, chars:"?"}, + "?", "IntlBackslash", KeyboardEvent.DOM_VK_SLASH, "?", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_BRAZILIAN_ABNT, keyCode:WIN_VK_ABNT_C2, + modifiers:{numLockKey:1}, chars:"."}, + ".", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_BRAZILIAN_ABNT, keyCode:WIN_VK_DECIMAL, + modifiers:{numLockKey:1}, chars:","}, + ",", "NumpadDecimal", KeyboardEvent.DOM_VK_DECIMAL, ",", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + + // Mac JIS keyboard + // The separator key on JIS keyboard for Mac doesn't cause any character even with Japanese keyboard layout. + yield testKey({layout:KEYBOARD_LAYOUT_JAPANESE, keyCode:WIN_VK_ABNT_C2, + modifiers:{numLockKey:1}, chars:""}, + "", "NumpadComma", KeyboardEvent.DOM_VK_SEPARATOR, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + yield testKey({layout:KEYBOARD_LAYOUT_JAPANESE, keyCode:WIN_VK_DECIMAL, + modifiers:{numLockKey:1}, chars:"."}, + ".", "NumpadDecimal", KeyboardEvent.DOM_VK_DECIMAL, ".", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD); + + // Dead keys on any layouts + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{}, chars:"^^"}, + ["^^", "^", "^", "^"], "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "^^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_A, + modifiers:{}, chars:"\u00E2"}, + ["\u00E2", "\u00E2", "a"], "KeyQ", KeyboardEvent.DOM_VK_A, "\u00E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_A, + modifiers:{shiftKey:1}, chars:"\u00C2"}, + ["\u00C2", "\u00C2", "A"], "KeyQ", KeyboardEvent.DOM_VK_A, "\u00C2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_Q, + modifiers:{}, chars:"^q"}, + ["^q", "^", "q", "q"], "KeyA", KeyboardEvent.DOM_VK_Q, "^q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{shiftKey:1}, chars:"\u00A8\u00A8"}, + ["\u00A8\u00A8", "\u00A8", "\u00A8", "\u00A8"], "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "\u00A8\u00A8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_A, + modifiers:{shiftKey:1}, chars:"\u00C4"}, + ["\u00C4", "\u00C4", "A"], "KeyQ", KeyboardEvent.DOM_VK_A, "\u00C4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_A, + modifiers:{}, chars:"\u00E4"}, + ["\u00E4", "\u00E4", "a"], "KeyQ", KeyboardEvent.DOM_VK_A, "\u00E4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_OEM_6, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_FRENCH, keyCode:WIN_VK_Q, + modifiers:{shiftKey:1}, chars:"\u00A8Q"}, + ["\u00A8Q", "\u00A8", "Q", "Q"], "KeyA", KeyboardEvent.DOM_VK_Q, "\u00A8Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{}, chars:"``"}, + ["``", "`", "`", "`"], "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "``", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{}, chars:"\u00E0"}, + ["\u00E0", "\u00E0", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{shiftKey:1}, chars:"\u00C0"}, + ["\u00C0", "\u00C0", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q, + modifiers:{}, chars:"`q"}, + ["`q", "`", "q", "q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "`q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{shiftKey:1}, chars:"^^"}, + ["^^", "^", "^", "^"], "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "^^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{shiftKey:1}, chars:"\u00C2"}, + ["\u00C2", "\u00C2", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{}, chars:"\u00E2"}, + ["\u00E2", "\u00E2", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q, + modifiers:{shiftKey:1}, chars:"^Q"}, + ["^Q", "^", "Q", "Q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "^Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{}, chars:""}, + "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{}, chars:"\u00B4\u00B4"}, + ["\u00B4\u00B4", "\u00B4", "\u00B4", "\u00B4"], "Quote", KeyboardEvent.DOM_VK_QUOTE, "\u00B4\u00B4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{}, chars:""}, + "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{}, chars:"\u00E1"}, + ["\u00E1", "\u00E1", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{}, chars:""}, + "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{shiftKey:1}, chars:"\u00C1"}, + ["\u00C1", "\u00C1", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C1", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{}, chars:""}, + "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q, + modifiers:{}, chars:"\u00B4q"}, + ["\u00B4q", "\u00B4", "q", "q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "\u00B4q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{shiftKey:1}, chars:"\u00A8\u00A8"}, + ["\u00A8\u00A8", "\u00A8", "\u00A8", "\u00A8"], "Quote", KeyboardEvent.DOM_VK_QUOTE, "\u00A8\u00A8", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{shiftKey:1}, chars:"\u00C4"}, + ["\u00C4", "\u00C4", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{}, chars:"\u00E4"}, + ["\u00E4", "\u00E4", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E4", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_7, + modifiers:{shiftKey:1}, chars:""}, + "Dead", "Quote", KeyboardEvent.DOM_VK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q, + modifiers:{shiftKey:1}, chars:"\u00A8Q"}, + ["\u00A8Q", "\u00A8", "Q", "Q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "\u00A8Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_OEM_1, + modifiers:{altGrKey:1}, chars:""}, + "Dead", "BracketRight", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_A, + modifiers:{}, chars:"\u00E3"}, + ["\u00E3", "\u00E3", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_OEM_1, + modifiers:{ctrlKey:1, altKey:1}, chars:""}, + "Dead", "BracketRight", KeyboardEvent.DOM_VK_CIRCUMFLEX, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_NORWEGIAN, keyCode:WIN_VK_A, + modifiers:{}, chars:"\u00E3"}, + ["\u00E3", "\u00E3", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E3", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + if (OS_VERSION >= WIN8) { + // On Russian Mnemonic layout, both 'KeyS' and 'KeyC' are dead key. However, the sequence 'KeyS' -> 'KeyC' causes a composite character. + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_S, + modifiers:{}, chars:""}, + "Dead", "KeyS", KeyboardEvent.DOM_VK_S, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_S, + modifiers:{}, chars:"\u0441\u0441"}, + ["\u0441\u0441", "\u0441", "\u0441", "\u0441"], "KeyS", KeyboardEvent.DOM_VK_S, "\u0441\u0441", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_C, + modifiers:{}, chars:""}, + "Dead", "KeyC", KeyboardEvent.DOM_VK_C, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_C, + modifiers:{}, chars:"\u0446\u0446"}, + ["\u0446\u0446", "\u0446", "\u0446", "\u0446"], "KeyC", KeyboardEvent.DOM_VK_C, "\u0446\u0446", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_S, + modifiers:{}, chars:""}, + "Dead", "KeyS", KeyboardEvent.DOM_VK_S, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, keyCode:WIN_VK_C, + modifiers:{}, chars:"\u0449"}, + ["\u0449", "\u0449", "\u0446"], "KeyC", KeyboardEvent.DOM_VK_C, "\u0449", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + } + + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_A, + modifiers:{altKey:1,ctrlKey:1}, chars:""}, + "\u0444", "KeyA", KeyboardEvent.DOM_VK_A, "a", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_A, + modifiers:{altKey:1,ctrlKey:1,shiftKey:1}, chars:""}, + "\u0424", "KeyA", KeyboardEvent.DOM_VK_A, "A", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_T, + modifiers:{altKey:1,ctrlKey:1}, chars:""}, + "\u0435", "KeyT", KeyboardEvent.DOM_VK_T, "t", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_T, + modifiers:{altKey:1,ctrlKey:1,shiftKey:1}, chars:""}, + "\u0415", "KeyT", KeyboardEvent.DOM_VK_T, "T", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + // When Alt key is pressed, dead key sequence is generated with WM_SYSKEYDOWN, WM_SYSDEADCHAR, WM_SYSCHAR and WM_SYSKEYUP. + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1}, chars:"``"}, + ["``", "`", "`", "`"], "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "``", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{altKey:1}, chars:"\u00E0"}, + ["\u00E0", "\u00E0", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00C0"}, + ["\u00C0", "\u00C0", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C0", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q, + modifiers:{altKey:1}, chars:"`q"}, + ["`q", "`", "q", "q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "`q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1, shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1, shiftKey:1}, chars:"^^"}, + ["^^", "^", "^", "^"], "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "^^", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1, shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{altKey:1, shiftKey:1}, chars:"\u00C2"}, + ["\u00C2", "\u00C2", "A"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00C2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1, shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_A, + modifiers:{altKey:1}, chars:"\u00E2"}, + ["\u00E2", "\u00E2", "a"], "KeyA", KeyboardEvent.DOM_VK_A, "\u00E2", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_OEM_1, + modifiers:{altKey:1, shiftKey:1}, chars:""}, + "Dead", "BracketLeft", KeyboardEvent.DOM_VK_BACK_QUOTE, "", SHOULD_DELIVER_KEYDOWN_KEYUP, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + yield testKey({layout:KEYBOARD_LAYOUT_SPANISH, keyCode:WIN_VK_Q, + modifiers:{altKey:1, shiftKey:1}, chars:"^Q"}, + ["^Q", "^", "Q", "Q"], "KeyQ", KeyboardEvent.DOM_VK_Q, "^Q", SHOULD_DELIVER_ALL, KeyboardEvent.DOM_KEY_LOCATION_STANDARD); + + cleanup(); + } + + + if (IS_WIN) { + yield* testKeysOnWindows(); + } else if (IS_MAC) { + yield* testKeysOnMac(); + } else { + cleanup(); + } +} + +// Test the activation (or not) of an HTML accesskey +function* runAccessKeyTests() +{ + var button = document.getElementById("button"); + var activationCount; + + function onClick(e) + { + ++activationCount; + } + + // The first parameter is the complete input event. The second and third parameters are + // what to test against. + function testKey(aEvent, aAccessKey, aShouldActivate) + { + activationCount = 0; + button.setAttribute("accesskey", aAccessKey); + + return synthesizeKey(aEvent, "button", function() { + + var currentTestName = eventToString(aEvent); + + is(activationCount, aShouldActivate ? 1 : 0, + currentTestName + ", activating '" + aAccessKey + "'"); + + continueTest(); + }); + } + + button.addEventListener("click", onClick); + + // These tests have to be per-plaform. + if (IS_MAC) { + // Basic sanity checks + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{}, chars:"a", unmodifiedChars:"a"}, + "a", false); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"}, + "a", false); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1}, chars:"\u0001", unmodifiedChars:"a"}, + "A", false); + + // Shift-ctrl does not activate accesskeys + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"A"}, + "a", false); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0001", unmodifiedChars:"A"}, + "A", false); + // Alt-ctrl activate accesskeys + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"a"}, + "a", true); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"a"}, + "A", true); + + // Greek layout can activate a Latin accesskey + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"}, + "a", true); + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"}, + "A", true); + // ... and a Greek accesskey! + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"}, + "\u03b1", true); + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, altKey:1}, chars:"\u0001", unmodifiedChars:"\u03b1"}, + "\u0391", true); + + // bug 359638 + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Period, + modifiers:{ctrlKey:1, altKey:1}, chars:".", unmodifiedChars:"."}, + ".", true); + + // German (KCHR/KeyTranslate case) + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, altKey:1}, chars:"a", unmodifiedChars:"a"}, + "a", true); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_A, + modifiers:{ctrlKey:1, altKey:1}, chars:"a", unmodifiedChars:"a"}, + "A", true); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{ctrlKey:1, altKey:1}, chars:"\u00fc", unmodifiedChars:"\u00fc"}, + "\u00fc", true); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:MAC_VK_ANSI_LeftBracket, + modifiers:{ctrlKey:1, altKey:1}, chars:"\u00fc", unmodifiedChars:"\u00fc"}, + "\u00dc", true); + } + else if (IS_WIN) { + // Basic sanity checks + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{}, chars:"a"}, + "a", false); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{shiftKey:1, altKey:1}, chars:"A"}, + "a", true); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{shiftKey:1, altKey:1}, chars:"A"}, + "A", true); + + // shift-alt-ctrl does not activate accesskeys + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1, shiftKey:1, altKey:1}, chars:""}, + "a", false); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1, shiftKey:1, altKey:1}, chars:""}, + "A", false); + + // Greek layout can activate a Latin accesskey + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A, + modifiers:{shiftKey:1, altKey:1}, chars:"\u0391"}, + "a", true); + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A, + modifiers:{shiftKey:1, altKey:1}, chars:"\u0391"}, + "A", true); + // ... and a Greek accesskey! + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A, + modifiers:{shiftKey:1, altKey:1}, chars:"\u0391"}, + "\u03b1", true); + yield testKey({layout:KEYBOARD_LAYOUT_GREEK, keyCode:WIN_VK_A, + modifiers:{shiftKey:1, altKey:1}, chars:"\u0391"}, + "\u0391", true); + + // bug 359638 + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_PERIOD, + modifiers:{shiftKey:1, altKey:1}, chars:".", unmodifiedChars:"."}, + ".", true); + } + + button.removeEventListener("click", onClick); +} + +function* runXULKeyTests() +{ + var commandElements = { + expectedCommand: document.getElementById("expectedCommand"), + unexpectedCommand: document.getElementById("unexpectedCommand"), + expectedReservedCommand: document.getElementById("expectedReservedCommand") + }; + // Enable all command elements. + for (var id in commandElements) { + commandElements[id].removeAttribute("disabled"); + } + + var keyEvents = []; + + function onKeyInDefaultEventGroup(aDOMEvent) + { + if (isModifierKeyEvent(aDOMEvent)) { + return; + } + keyEvents.push({ type: aDOMEvent.type, group: "default", phase: getPhase(aDOMEvent), currentTarget: eventTargetToString(aDOMEvent.currentTarget) }); + } + + function onKeyInSystemEventGroup(aDOMEvent) + { + if (isModifierKeyEvent(aDOMEvent)) { + return; + } + keyEvents.push({ type: aDOMEvent.type, group: "system", phase: getPhase(aDOMEvent), currentTarget: eventTargetToString(aDOMEvent.currentTarget) }); + } + + var buttonParent = document.getElementById("button").parentNode; + buttonParent.addEventListener("keydown", onKeyInDefaultEventGroup, true); + buttonParent.addEventListener("keypress", onKeyInDefaultEventGroup, true); + buttonParent.addEventListener("keydown", onKeyInDefaultEventGroup); + buttonParent.addEventListener("keypress", onKeyInDefaultEventGroup); + SpecialPowers.addSystemEventListener(buttonParent, "keydown", onKeyInSystemEventGroup, true); + SpecialPowers.addSystemEventListener(buttonParent, "keypress", onKeyInSystemEventGroup, true); + SpecialPowers.addSystemEventListener(buttonParent, "keydown", onKeyInSystemEventGroup, false); + SpecialPowers.addSystemEventListener(buttonParent, "keypress", onKeyInSystemEventGroup, false); + + function finializeKeyElementTest() + { + buttonParent.removeEventListener("keydown", onKeyInDefaultEventGroup, true); + buttonParent.removeEventListener("keypress", onKeyInDefaultEventGroup, true); + buttonParent.removeEventListener("keydown", onKeyInDefaultEventGroup); + buttonParent.removeEventListener("keypress", onKeyInDefaultEventGroup); + SpecialPowers.removeSystemEventListener(buttonParent, "keydown", onKeyInSystemEventGroup, true); + SpecialPowers.removeSystemEventListener(buttonParent, "keypress", onKeyInSystemEventGroup, true); + SpecialPowers.removeSystemEventListener(buttonParent, "keydown", onKeyInSystemEventGroup, false); + SpecialPowers.removeSystemEventListener(buttonParent, "keypress", onKeyInSystemEventGroup, false); + } + + // If aKeyElement is empty string, this function tests if the event kicks + // no key elements. + function testKeyElement(aEvent, aKeyElementId) + { + var testName = "testKeyElement (with non-reserved command element): " + eventToString(aEvent) + " "; + var keyElement = aKeyElementId == "" ? null : document.getElementById(aKeyElementId); + if (keyElement) { + keyElement.setAttribute("command", "expectedCommand"); + } + + /* eslint-disable-next-line no-shadow */ + for (var id in commandElements) { + commandElements[id].activeCount = 0; + } + + keyEvents = []; + return synthesizeKey(aEvent, "button", function() { + if (keyElement) { + is(commandElements.expectedCommand.activeCount, 1, testName + "the command element (id='expectedCommand') should be preformed"); + } else { + is(commandElements.expectedCommand.activeCount, 0, testName + "the command element (id='expectedCommand') shouldn't be preformed"); + } + is(commandElements.unexpectedCommand.activeCount, 0, testName + "the command element (id='unexpectedCommand') shouldn't be preformed"); + is(commandElements.expectedReservedCommand.activeCount, 0, testName + "the command element (id='expectedReservedCommand') shouldn't be preformed"); + + function checkFiredEvents() + { + let expectKeyPressEvent = aKeyElementId != "" || + ((aEvent.modifiers.ctrlKey || aEvent.modifiers.altKey || aEvent.modifiers.metaKey) && + (!IS_WIN || !aEvent.modifiers.altGrKey)); + is(keyEvents.length, expectKeyPressEvent ? 8 : 4, testName + "wrong number events fired"); + is(JSON.stringify(keyEvents[0]), JSON.stringify({ type: "keydown", group: "default", phase: "capture", currentTarget: eventTargetToString(buttonParent) }), testName + "keydown event should be fired on chrome in the default event group #0"); + is(JSON.stringify(keyEvents[1]), JSON.stringify({ type: "keydown", group: "default", phase: "bubble", currentTarget: eventTargetToString(buttonParent) }), testName + "keydown event should be fired on chrome in the default event group #1"); + + is(JSON.stringify(keyEvents[2]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(buttonParent) }), testName + "keydown event should be fired on chrome in the system event group #2"); + is(JSON.stringify(keyEvents[3]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(buttonParent) }), testName + "keydown event should be fired on chrome in the system event group #3"); + + if (expectKeyPressEvent) { + is(JSON.stringify(keyEvents[4]), JSON.stringify({ type: "keypress", group: "default", phase: "capture", currentTarget: eventTargetToString(buttonParent) }), testName + "keypress event should be fired on chrome in the default event group #4"); + is(JSON.stringify(keyEvents[5]), JSON.stringify({ type: "keypress", group: "default", phase: "bubble", currentTarget: eventTargetToString(buttonParent) }), testName + "keypress event should be fired on chrome in the default event group #5"); + + is(JSON.stringify(keyEvents[6]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(buttonParent) }), testName + "keypress event should be fired on chrome in the system event group #6"); + is(JSON.stringify(keyEvents[7]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(buttonParent) }), testName + "keypress event should be fired on chrome in the system event group #7"); + } + } + + checkFiredEvents(); + + if (keyElement) { + testName = "testKeyElement (with reserved command element): " + eventToString(aEvent) + " "; + keyElement.setAttribute("command", "expectedReservedCommand"); + + for (id in commandElements) { + commandElements[id].activeCount = 0; + } + keyEvents = []; + synthesizeKey(aEvent, "button", function() { + is(commandElements.expectedCommand.activeCount, 0, testName + "the command element (id='expectedCommand') shouldn't be preformed"); + is(commandElements.unexpectedCommand.activeCount, 0, testName + "the command element (id='unexpectedCommand') shouldn't be preformed"); + is(commandElements.expectedReservedCommand.activeCount, 1, testName + "the command element (id='expectedReservedCommand') should be preformed"); + + checkFiredEvents(); + + if (keyElement) { + keyElement.setAttribute("command", "unexpectedCommand"); + } + continueTest(); + }); + } else { + if (keyElement) { + keyElement.setAttribute("command", "unexpectedCommand"); + } + continueTest(); + } + }); + } + + if (IS_MAC) { + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{metaKey:1}, chars:";", unmodifiedChars:";"}, + "unshiftedKey"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Semicolon, + modifiers:{metaKey:1, shiftKey:1}, chars:";", unmodifiedChars:":"}, + "shiftedKey"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{metaKey:1}, chars:"'", unmodifiedChars:"'"}, + "reservedUnshiftedKey"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote, + modifiers:{metaKey:1, shiftKey:1}, chars:"\"", unmodifiedChars:"'"}, + "reservedShiftedKey"); + } + else if (IS_WIN) { + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1, + modifiers:{ctrlKey:1}, chars:""}, + "unshiftedKey"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "shiftedKey"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7, + modifiers:{ctrlKey:1}, chars:""}, + "reservedUnshiftedKey"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "reservedShiftedKey"); + } + + // 429160 + if (IS_MAC) { + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F, + modifiers:{metaKey:1, altKey:1}, chars:"\u0192", unmodifiedChars:"f"}, + "commandOptionF"); + } + else if (IS_WIN) { + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_F, + modifiers:{ctrlKey:1, altKey:1}, chars:"\u0006"}, + "commandOptionF"); + } + + // 432112 + if (IS_MAC) { + yield testKeyElement({layout:KEYBOARD_LAYOUT_SWEDISH, keyCode:MAC_VK_ANSI_Minus, + modifiers:{metaKey:1, shiftKey:1}, chars:"+", unmodifiedChars:"?"}, + "question"); + } + else if (IS_WIN) { + yield testKeyElement({layout:KEYBOARD_LAYOUT_SWEDISH, keyCode:WIN_VK_OEM_PLUS, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "question"); + // For testing if Ctrl+? is kicked without Shift state, temporarily disable + // Ctrl-+ key element. + var unshiftedPlusKeyElement = document.getElementById("unshiftedPlus"); + unshiftedPlusKeyElement.setAttribute("disabled", "true"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_SWEDISH, keyCode:WIN_VK_OEM_PLUS, + modifiers:{ctrlKey:1}, chars:""}, + ""); + unshiftedPlusKeyElement.removeAttribute("disabled"); + } + + // bug 433192 + if (IS_WIN) { + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X, + modifiers:{ctrlKey:1}, chars:"\u0018"}, + "unshiftedX"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_X, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018"}, + "shiftedX"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_ARABIC, keyCode:WIN_VK_X, + modifiers:{ctrlKey:1}, chars:"\u0018"}, + "unshiftedX"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_ARABIC, keyCode:WIN_VK_X, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018"}, + "shiftedX"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_HEBREW, keyCode:WIN_VK_X, + modifiers:{ctrlKey:1}, chars:"\u0018"}, + "unshiftedX"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_HEBREW, keyCode:WIN_VK_X, + modifiers:{ctrlKey:1, shiftKey:1}, chars:"\u0018"}, + "shiftedX"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_JAPANESE, keyCode:WIN_VK_OEM_PLUS, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "unshiftedPlus"); + } + + // bug 759346 + if (IS_WIN) { + yield testKeyElement({layout:KEYBOARD_LAYOUT_THAI, keyCode:WIN_VK_1, + modifiers:{ctrlKey:1}, chars:""}, + ""); + yield testKeyElement({layout:KEYBOARD_LAYOUT_THAI, keyCode:WIN_VK_1, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + "unshiftedPlus"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_THAI, keyCode:WIN_VK_OEM_PLUS, + modifiers:{ctrlKey:1}, chars:""}, + "unshiftedPlus"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_THAI, keyCode:WIN_VK_OEM_PLUS, + modifiers:{ctrlKey:1, shiftKey:1}, chars:""}, + ""); + } + + // bug 1596916 + if (IS_WIN) { + // Ctrl + Alt + foo should be performed only when AltGr key is not + // pressed, i.e., only when Ctrl key and left Alt key are pressed if + // active keyboard layout has AltGr key. + yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A, + modifiers:{altGrKey:1}, chars:""}, + ""); + yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1, altGrKey:1}, chars:""}, + ""); + yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A, + modifiers:{altKey:1, altGrKey:1}, chars:""}, + ""); + yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1, altKey:1}, chars:""}, + "ctrlAltA"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1, altKey:1, altGrKey:1}, chars:""}, + ""); + } + + // bug 1874727 + if (IS_WIN) { + yield testKeyElement({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_A, + modifiers:{altKey:1, ctrlKey:1}, chars:""}, + "ctrlAltA"); + yield testKeyElement({layout:KEYBOARD_LAYOUT_RUSSIAN, keyCode:WIN_VK_A, + modifiers:{altKey:1, ctrlKey:1, shiftKey:1}, chars:""}, + "ctrlAltShiftA"); + } + + for (id in commandElements) { + commandElements[id].setAttribute("disabled", "true"); + } + finializeKeyElementTest(); +} + +function* runReservedKeyTests() +{ + var browser = document.getElementById("browser"); + var contents = [ + browser.contentWindow, + browser.contentDocument, + browser.contentDocument.documentElement, + browser.contentDocument.body, + browser.contentDocument.getElementById("content_button") + ]; + + for (var i = 0; i < contents.length; i++) { + contents[i].addEventListener("keydown", onKeyInDefaultEventGroup, true); + contents[i].addEventListener("keypress", onKeyInDefaultEventGroup, true); + contents[i].addEventListener("keydown", onKeyInDefaultEventGroup); + contents[i].addEventListener("keypress", onKeyInDefaultEventGroup); + SpecialPowers.addSystemEventListener(contents[i], "keydown", onKeyInSystemEventGroup, true); + SpecialPowers.addSystemEventListener(contents[i], "keypress", onKeyInSystemEventGroup, true); + SpecialPowers.addSystemEventListener(contents[i], "keydown", onKeyInSystemEventGroup, false); + SpecialPowers.addSystemEventListener(contents[i], "keypress", onKeyInSystemEventGroup, false); + } + + var keyEvents = []; + + function onKeyInDefaultEventGroup(aDOMEvent) + { + if (isModifierKeyEvent(aDOMEvent)) { + return; + } + keyEvents.push({ type: aDOMEvent.type, group: "default", phase: getPhase(aDOMEvent), currentTarget: eventTargetToString(aDOMEvent.currentTarget) }); + } + + function onKeyInSystemEventGroup(aDOMEvent) + { + if (isModifierKeyEvent(aDOMEvent)) { + return; + } + keyEvents.push({ type: aDOMEvent.type, group: "system", phase: getPhase(aDOMEvent), currentTarget: eventTargetToString(aDOMEvent.currentTarget) }); + // prevents reserved default action + if (aDOMEvent.type == "keypress" && + aDOMEvent.eventPhase == aDOMEvent.BUBBLING_PHASE && + aDOMEvent.currentTarget == browser.contentWindow) { + aDOMEvent.preventDefault(); + } + } + + function finializeKeyElementTest() + { + /* eslint-disable-next-line no-shadow */ + for (var i = 0; i < contents.length; i++) { + contents[i].removeEventListener("keydown", onKeyInDefaultEventGroup, true); + contents[i].removeEventListener("keypress", onKeyInDefaultEventGroup, true); + contents[i].removeEventListener("keydown", onKeyInDefaultEventGroup); + contents[i].removeEventListener("keypress", onKeyInDefaultEventGroup); + SpecialPowers.removeSystemEventListener(contents[i], "keydown", onKeyInSystemEventGroup, true); + SpecialPowers.removeSystemEventListener(contents[i], "keypress", onKeyInSystemEventGroup, true); + SpecialPowers.removeSystemEventListener(contents[i], "keydown", onKeyInSystemEventGroup, false); + SpecialPowers.removeSystemEventListener(contents[i], "keypress", onKeyInSystemEventGroup, false); + } + } + + function testReservedKey(aEvent) + { + keyEvents = []; + return synthesizeKey(aEvent, "content_button", function() { + let testName = "testReservedKey: " + eventToString(aEvent) + " "; + is(keyEvents.length, 20, testName + "wrong number events fired"); + is(JSON.stringify(keyEvents[0]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[0]) }), testName + "keydown event should be fired on content only in the system event group #0"); + is(JSON.stringify(keyEvents[1]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[1]) }), testName + "keydown event should be fired on content only in the system event group #1"); + is(JSON.stringify(keyEvents[2]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[2]) }), testName + "keydown event should be fired on content only in the system event group #2"); + is(JSON.stringify(keyEvents[3]), JSON.stringify({ type: "keydown", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[3]) }), testName + "keydown event should be fired on content only in the system event group #3"); + + is(JSON.stringify(keyEvents[4]), JSON.stringify({ type: "keydown", group: "system", phase: "target", currentTarget: eventTargetToString(contents[4]) }), testName + "keydown event should be fired on content only in the system event group #4"); + is(JSON.stringify(keyEvents[5]), JSON.stringify({ type: "keydown", group: "system", phase: "target", currentTarget: eventTargetToString(contents[4]) }), testName + "keydown event should be fired on content only in the system event group #5"); + + is(JSON.stringify(keyEvents[6]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[3]) }), testName + "keydown event should be fired on content only in the system event group #6"); + is(JSON.stringify(keyEvents[7]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[2]) }), testName + "keydown event should be fired on content only in the system event group #7"); + is(JSON.stringify(keyEvents[8]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[1]) }), testName + "keydown event should be fired on content only in the system event group #8"); + is(JSON.stringify(keyEvents[9]), JSON.stringify({ type: "keydown", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[0]) }), testName + "keydown event should be fired on content only in the system event group #9"); + + is(JSON.stringify(keyEvents[10]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[0]) }), testName + "keypress event should be fired on content only in the system event group #10"); + is(JSON.stringify(keyEvents[11]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[1]) }), testName + "keypress event should be fired on content only in the system event group #11"); + is(JSON.stringify(keyEvents[12]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[2]) }), testName + "keypress event should be fired on content only in the system event group #12"); + is(JSON.stringify(keyEvents[13]), JSON.stringify({ type: "keypress", group: "system", phase: "capture", currentTarget: eventTargetToString(contents[3]) }), testName + "keypress event should be fired on content only in the system event group #13"); + + is(JSON.stringify(keyEvents[14]), JSON.stringify({ type: "keypress", group: "system", phase: "target", currentTarget: eventTargetToString(contents[4]) }), testName + "keypress event should be fired on content only in the system event group #14"); + is(JSON.stringify(keyEvents[15]), JSON.stringify({ type: "keypress", group: "system", phase: "target", currentTarget: eventTargetToString(contents[4]) }), testName + "keypress event should be fired on content only in the system event group #15"); + + is(JSON.stringify(keyEvents[16]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[3]) }), testName + "keypress event should be fired on content only in the system event group #16"); + is(JSON.stringify(keyEvents[17]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[2]) }), testName + "keypress event should be fired on content only in the system event group #17"); + is(JSON.stringify(keyEvents[18]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[1]) }), testName + "keypress event should be fired on content only in the system event group #18"); + is(JSON.stringify(keyEvents[19]), JSON.stringify({ type: "keypress", group: "system", phase: "bubble", currentTarget: eventTargetToString(contents[0]) }), testName + "keypress event should be fired on content only in the system event group #19"); + + continueTest(); + }); + } + + if (IS_MAC) { + // Cmd+T is reserved for opening new tab. + yield testReservedKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T, + modifiers:{metaKey:1}, chars:"t", unmodifiedChars:"t"}); + } else if (IS_WIN) { + // Ctrl+T is reserved for opening new tab. + yield testReservedKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T, + modifiers:{ctrlKey:1}, chars:"\u0014"}); + yield testReservedKey({layout:KEYBOARD_LAYOUT_ARABIC, keyCode:WIN_VK_T, + modifiers:{ctrlKey:1}, chars:"\u0014"}); + } + + finializeKeyElementTest(); +} + +function* runTextInputTests() +{ + var textbox = document.getElementById("textbox"); + + function testKey(aEvent, aExpectText) { + textbox.value = ""; + textbox.focus(); + + /* eslint-disable-next-line no-shadow */ + var currentTestName = eventToString(aEvent); + + // Check if the text comes with keypress events rather than composition events. + var keypress = 0; + /* eslint-disable-next-line no-shadow */ + function onKeypress(aEvent) { + keypress++; + if (keypress == 1 && aExpectText == "") { + if (!aEvent.ctrlKey && !aEvent.altKey) { + is(aEvent.charCode, 0, + currentTestName + ", the charCode value should be 0 when it shouldn't cause inputting text"); + } + return; + } + if (keypress > aExpectText.length) { + ok(false, currentTestName + " causes too many keypress events"); + return; + } + is(aEvent.key, aExpectText[keypress - 1], + currentTestName + ", " + keypress + "th keypress event's key value should be '" + aExpectText[keypress - 1] + "'"); + is(aEvent.charCode, aExpectText.charCodeAt(keypress - 1), + currentTestName + ", " + keypress + "th keypress event's charCode value should be 0x" + parseInt(aExpectText.charCodeAt(keypress - 1), 16)); + } + textbox.addEventListener("keypress", onKeypress, true); + + return synthesizeKey(aEvent, "textbox", () => { + textbox.removeEventListener("keypress", onKeypress, true); + if (aExpectText == "") { + if (aEvent.modifiers.ctrlKey || aEvent.modifiers.altKey) { + is(keypress, 1, + currentTestName + " should cause one keypress event because it should be available for shortcut key"); + } else { + is(keypress, 0, + currentTestName + " should cause no keypress event because simple key press only with Shift/AltGraph " + + "or without any modifiers shouldn't match with a shortcut key"); + } + } else { + is(keypress, aExpectText.length, + currentTestName + " should cause " + aExpectText.length + " keypress events"); + is(textbox.value, aExpectText, + currentTestName + " does not input correct text."); + } + + continueTest(); + }); + } + + if (IS_MAC) { + yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_G, + modifiers:{shiftKey:1}, chars:"\u0644\u0623", unmodifiedChars:"\u0644\u0623"}, + "\u0644\u0623"); + yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_T, + modifiers:{shiftKey:1}, chars:"\u0644\u0625", unmodifiedChars:"\u0644\u0625"}, + "\u0644\u0625"); + yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_B, + modifiers:{shiftKey:1}, chars:"\u0644\u0622", unmodifiedChars:"\u0644\u0622"}, + "\u0644\u0622"); + yield testKey({layout:KEYBOARD_LAYOUT_ARABIC_PC, keyCode:MAC_VK_ANSI_B, + modifiers:{}, chars:"\u0644\u0627", unmodifiedChars:"\u0644\u0627"}, + "\u0644\u0627"); + } else if (IS_WIN) { + // Basic sanity checks + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{}, chars:"a"}, + "a"); + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{shiftKey:1}, chars:"A"}, + "A"); + // When Ctrl+Alt are pressed, any text should not be inputted. + yield testKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1, altKey:1}, chars:""}, + ""); + // AltGr should input only when it's mapped to a character + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_0, + modifiers:{altGrKey:1}, chars:"}"}, + "}"); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A, + modifiers:{altGrKey:1}, chars:""}, + ""); + yield testKey({layout:KEYBOARD_LAYOUT_GERMAN, keyCode:WIN_VK_A, + modifiers:{ctrlKey:1, altKey:1}, chars:""}, + ""); + + // Lithuanian AltGr should be consumed at 9/0 keys pressed + yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_8, + modifiers:{}, chars:"\u016B"}, + "\u016B"); + yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_9, + modifiers:{}, chars:"9"}, + "9"); + yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_0, + modifiers:{}, chars:"0"}, + "0"); + yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_8, + modifiers:{ctrlKey:1, altKey:1}, chars:"8"}, + "8"); + yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_9, + modifiers:{ctrlKey:1, altKey:1}, chars:"9"}, + "9"); + yield testKey({layout:KEYBOARD_LAYOUT_LITHUANIAN, keyCode:WIN_VK_0, + modifiers:{ctrlKey:1, altKey:1}, chars:"0"}, + "0"); + } + + // XXX We need to move focus for canceling to search the autocomplete + // result. If we don't do here, Fx will crash at end of this tests. + document.getElementById("button").focus(); +} + +function* runAltRightKeyOnWindows() +{ + if (!IS_WIN) { + return; + } + + var button = document.getElementById("button"); + button.focus(); + + const kKeyboardLayouts = [ + { layout: KEYBOARD_LAYOUT_ARABIC }, + { layout: KEYBOARD_LAYOUT_BRAZILIAN_ABNT }, + { layout: KEYBOARD_LAYOUT_EN_US }, + { layout: KEYBOARD_LAYOUT_FRENCH }, + { layout: KEYBOARD_LAYOUT_GREEK }, + { layout: KEYBOARD_LAYOUT_GERMAN }, + { layout: KEYBOARD_LAYOUT_HEBREW }, + { layout: KEYBOARD_LAYOUT_JAPANESE }, + { layout: KEYBOARD_LAYOUT_KHMER }, + { layout: KEYBOARD_LAYOUT_LITHUANIAN }, + { layout: KEYBOARD_LAYOUT_NORWEGIAN }, + { layout: KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC, + canTestIt () { return OS_VERSION >= WIN8; } }, + { layout: KEYBOARD_LAYOUT_SPANISH }, + { layout: KEYBOARD_LAYOUT_SWEDISH }, + { layout: KEYBOARD_LAYOUT_THAI }, + ]; + var events = []; + function pushEvent(aEvent) { + events.push(aEvent); + if (aEvent.key === "Alt") { + // Prevent working the menubar. + aEvent.preventDefault(); + } + } + button.addEventListener("keydown", pushEvent); + button.addEventListener("keyup", pushEvent); + + function testKey(aKeyboardLayout) { + return synthesizeKey({layout: aKeyboardLayout.layout, keyCode: WIN_VK_RMENU, + modifiers: {}, chars: ""}, "button", function() { + const kDescription = + "runAltRightKeyOnWindows(" + aKeyboardLayout.layout.currentTestName + "): "; + if (aKeyboardLayout.layout.hasAltGrOnWin) { + is(events.length, 4, + kDescription + "AltRight should fire 2 pairs of keydown and keyup events"); + is(events[0].type, "keydown", + kDescription + "First event should be keydown of ControlLeft"); + is(events[0].key, "Control", + kDescription + "First event should be keydown of ControlLeft whose key should be Control"); + is(events[0].code, "ControlLeft", + kDescription + "First event should be keydown of ControlLeft"); + is(events[0].location, KeyboardEvent.DOM_KEY_LOCATION_LEFT, + kDescription + "First event should be keydown of ControlLeft whose location should be DOM_KEY_LOCATION_LEFT"); + is(events[0].keyCode, KeyboardEvent.DOM_VK_CONTROL, + kDescription + "First event should be keydown of ControlLeft whose keyCode should be DOM_VK_CONTROL"); + is(events[0].ctrlKey, true, + kDescription + "First event should be keydown of ControlLeft whose ctrlKey should be true"); + is(events[0].altKey, false, + kDescription + "First event should be keydown of ControlLeft whose altKey should be false"); + is(events[0].getModifierState("AltGraph"), false, + kDescription + "First event should be keydown of ControlLeft whose getModifierState(\"AltGraph\") should be false"); + is(events[1].type, "keydown", + kDescription + "Second event should be keydown of AltRight"); + is(events[1].key, "AltGraph", + kDescription + "Second event should be keydown of AltRight whose key should be AltGraph"); + is(events[1].code, "AltRight", + kDescription + "Second event should be keydown of AltRight"); + is(events[1].location, KeyboardEvent.DOM_KEY_LOCATION_RIGHT, + kDescription + "Second event should be keydown of AltRight whose location should be DOM_KEY_LOCATION_RIGHT"); + is(events[1].keyCode, KeyboardEvent.DOM_VK_ALT, + kDescription + "Second event should be keydown of AltRight whose keyCode should be DOM_VK_ALT"); + is(events[1].ctrlKey, false, + kDescription + "Second event should be keydown of AltRight whose ctrlKey should be false"); + is(events[1].altKey, false, + kDescription + "Second event should be keydown of AltRight whose altKey should be false"); + is(events[1].getModifierState("AltGraph"), true, + kDescription + "Second event should be keydown of AltRight whose getModifierState(\"AltGraph\") should be true"); + is(events[2].type, "keyup", + kDescription + "Third event should be keyup of ControlLeft"); + is(events[2].key, "Control", + kDescription + "Third event should be keyup of ControlLeft whose key should be Control"); + is(events[2].code, "ControlLeft", + kDescription + "Third event should be keyup of ControlLeft"); + is(events[2].location, KeyboardEvent.DOM_KEY_LOCATION_LEFT, + kDescription + "Third event should be keyup of ControlLeft whose location should be DOM_KEY_LOCATION_LEFT"); + is(events[2].keyCode, KeyboardEvent.DOM_VK_CONTROL, + kDescription + "Third event should be keyup of ControlLeft whose keyCode should be DOM_VK_CONTROL"); + is(events[2].ctrlKey, false, + kDescription + "Third event should be keyup of ControlLeft whose ctrlKey should be false"); + is(events[2].altKey, false, + kDescription + "Third event should be keyup of ControlLeft whose altKey should be false"); + is(events[2].getModifierState("AltGraph"), true, + kDescription + "Third event should be keyup of ControlLeft whose getModifierState(\"AltGraph\") should be true"); + is(events[3].type, "keyup", + kDescription + "Forth event should be keyup of AltRight"); + is(events[3].key, "AltGraph", + kDescription + "Forth event should be keyup of AltRight whose key should be AltGraph"); + is(events[3].code, "AltRight", + kDescription + "Forth event should be keyup of AltRight"); + is(events[3].location, KeyboardEvent.DOM_KEY_LOCATION_RIGHT, + kDescription + "Forth event should be keyup of AltRight whose location should be DOM_KEY_LOCATION_RIGHT"); + is(events[3].keyCode, KeyboardEvent.DOM_VK_ALT, + kDescription + "Forth event should be keyup of AltRight whose keyCode should be DOM_VK_ALT"); + is(events[3].ctrlKey, false, + kDescription + "Third event should be keyup of AltRight whose ctrlKey should be false"); + is(events[3].altKey, false, + kDescription + "Third event should be keyup of AltRight whose altKey should be false"); + is(events[3].getModifierState("AltGraph"), false, + kDescription + "Third event should be keyup of AltRight whose getModifierState(\"AltGraph\") should be false"); + } else { + is(events.length, 2, + kDescription + "AltRight should fire a pair of keydown and keyup events"); + is(events[0].type, "keydown", + kDescription + "First event should be keydown of AltRight"); + is(events[0].key, "Alt", + kDescription + "First event should be keydown of AltRight whose key should be Alt"); + is(events[0].code, "AltRight", + kDescription + "First event should be keydown of AltRight"); + is(events[0].location, KeyboardEvent.DOM_KEY_LOCATION_RIGHT, + kDescription + "First event should be keydown of AltRight whose location should be DOM_KEY_LOCATION_RIGHT"); + is(events[0].keyCode, KeyboardEvent.DOM_VK_ALT, + kDescription + "First event should be keydown of AltRight whose keyCode should be DOM_VK_ALT"); + is(events[0].ctrlKey, false, + kDescription + "First event should be keydown of AltRight whose ctrlKey should be false"); + is(events[0].altKey, true, + kDescription + "First event should be keydown of AltRight whose altKey should be true"); + is(events[0].getModifierState("AltGraph"), false, + kDescription + "First event should be keydown of AltRight whose getModifierState(\"AltGraph\") should be false"); + is(events[1].type, "keyup", + kDescription + "Second event should be keyup of AltRight"); + is(events[1].key, "Alt", + kDescription + "Second event should be keyup of AltRight whose key should be Alt"); + is(events[1].code, "AltRight", + kDescription + "Second event should be keyup of AltRight"); + is(events[1].location, KeyboardEvent.DOM_KEY_LOCATION_RIGHT, + kDescription + "Second event should be keyup of AltRight whose location should be DOM_KEY_LOCATION_RIGHT"); + is(events[1].keyCode, KeyboardEvent.DOM_VK_ALT, + kDescription + "Second event should be keyup of AltRight whose keyCode should be DOM_VK_ALT"); + is(events[1].ctrlKey, false, + kDescription + "Second event should be keyup of AltRight whose ctrlKey should be false"); + is(events[1].altKey, false, + kDescription + "Second event should be keyup of AltRight whose altKey should be false"); + is(events[1].getModifierState("AltGraph"), false, + kDescription + "Second event should be keyup of AltRight whose getModifierState(\"AltGraph\") should be false"); + } + + continueTest(); + }); + } + + for (const kKeyboardLayout of kKeyboardLayouts) { + if (typeof kKeyboardLayout.canTestIt === "function" && + !kKeyboardLayout.canTestIt()) { + continue; + } + events = []; + yield testKey(kKeyboardLayout); + } + + button.addEventListener("keydown", pushEvent); + button.addEventListener("keyup", pushEvent); +} + +function* runAllTests() { + yield* runKeyEventTests(); + yield* runAccessKeyTests(); + yield* runXULKeyTests(); + yield* runReservedKeyTests(); + yield* runTextInputTests(); + yield* runAltRightKeyOnWindows(); +} + +var gTestContinuation = null; + +function continueTest() +{ + if (!gTestContinuation) { + gTestContinuation = runAllTests(); + } + var ret = gTestContinuation.next(); + if (ret.done) { + SimpleTest.finish(); + } else { + is(ret.value, true, "Key synthesized successfully"); + } +} + +function runTest() +{ + if (!IS_MAC && !IS_WIN) { + todo(false, "This test is supported on MacOSX and Windows only. (Bug 431503)"); + return; + } + + if (IS_WIN && OS_VERSION >= WIN8) { + // Switching keyboard layout to Russian - Mnemonic causes 2 assertions in + // KeyboardLayout::LoadLayout(). + const kAssertionCountDueToRussainMnemonic = 2 * 2; + SimpleTest.expectAssertions(kAssertionCountDueToRussainMnemonic, + kAssertionCountDueToRussainMnemonic); + } + SimpleTest.waitForExplicitFinish(); + + clearInfobars(); + + continueTest(); +} + +]]> +</script> + +</window> diff --git a/widget/tests/test_keypress_event_with_alt_on_mac.html b/widget/tests/test_keypress_event_with_alt_on_mac.html new file mode 100644 index 0000000000..01d4100f97 --- /dev/null +++ b/widget/tests/test_keypress_event_with_alt_on_mac.html @@ -0,0 +1,106 @@ +<!DOCTYPE html> +<html> +<head> + <title>Testing if keypress event is fired when alt key is pressed</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/NativeKeyCodes.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> + <input id="input"> + <input id="password" type="password"> + <input id="readonly-input" readonly> + <textarea id="textarea"></textarea> + <textarea id="readonly-textarea" readonly></textarea> + <button id="button">button</button> +</div> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +async function testNativeKey(aKeyboardLayout, aNativeKeyCode, aModifiers, + aChars, aUnmodifiedChars) { + // XXX Need to listen keyup event here because synthesizeNativeKey() does not + // guarantee that its callback will be called after "keypress" and "keyup". + let waitForKeyUp = new Promise(resolve => { + document.addEventListener("keyup", resolve, {once: true}); + }); + let keypress; + document.addEventListener("keypress", (aKeyPressEvent) => { + keypress = aKeyPressEvent; + }, {once: true}); + synthesizeNativeKey(aKeyboardLayout, aNativeKeyCode, aModifiers, aChars, aUnmodifiedChars); + await waitForKeyUp; + return keypress; +} + +async function runTests() { + const kTests = + [ { target: "input", isEditable: true }, + { target: "password", isEditable: true }, + { target: "readonly-input", isEditable: false }, + { target: "textarea", isEditable: true }, + { target: "readonly-textarea", isEditable: false }, + { target: "button", isEditable: false } ]; + for (const kTest of kTests) { + let element = document.getElementById(kTest.target); + element.focus(); + + const kDescription = kTest.target + ": "; + + let keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {}, "a", "a"); + ok(keypress, + kDescription + "'a' key press should cause firing keypress event"); + + keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {shiftKey: true}, "A", "A"); + ok(keypress, + kDescription + "'a' key press with shift key should cause firing keypress event"); + ok(keypress.shiftKey, + kDescription + "shiftKey of 'a' key press with shift key should be true"); + + // When a key inputs a character with option key, we need to unset altKey for our editor. + // Otherwise, altKey should be true for compatibility with the other browsers. + keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {altKey: true}, "\u00E5", "a"); + ok(keypress, + kDescription + "'a' key press with option key should cause firing keypress event"); + is(keypress.altKey, !kTest.isEditable, + kDescription + "altKey of 'a' key press with option key should be " + !kTest.isEditable); + + keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {altKey: true, shiftKey: true}, "\u00C5", "A"); + ok(keypress, + kDescription + "'a' key press with option key and shift key should cause firing keypress event"); + is(keypress.altKey, !kTest.isEditable, + kDescription + "altKey of 'a' key press with option key and shift key should be " + !kTest.isEditable); + ok(keypress.shiftKey, + kDescription + "shiftKey of 'a' key press with option key and shift key should be true"); + + keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {ctrlKey: true}, "\u0001", "a"); + ok(!keypress, + kDescription + "'a' key press with control key should not cause firing keypress event"); + + keypress = await testNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {altKey: true, ctrlKey: true}, "\u0001", "a"); + ok(!keypress, + kDescription + "'a' key press with option key and control key should not cause firing keypress event"); + + // XXX Cannot test with command key for now since keyup event won't be fired due to macOS's limitation. + + // Some keys of Arabic - PC keyboard layout do not input any character with option key. + // In such case, we shouldn't dispatch keypress event. + keypress = await testNativeKey(KEYBOARD_LAYOUT_ARABIC_PC, MAC_VK_ANSI_7, {altKey: true}, "", "\u0667"); + ok(!keypress, + kDescription + "'7' key press with option key should not cause firing keypress event"); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); +</script> +</body> +</html>
\ No newline at end of file diff --git a/widget/tests/test_mouse_event_with_control_on_mac.html b/widget/tests/test_mouse_event_with_control_on_mac.html new file mode 100644 index 0000000000..52ce206d35 --- /dev/null +++ b/widget/tests/test_mouse_event_with_control_on_mac.html @@ -0,0 +1,116 @@ +<html> +<head> + <title>Test control+click on Mac</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/SpecialPowers.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <style> + #target { + width: 100px; + height: 100px; + background-color: lightgreen; + }; + </style> +</head> +<body> +<div id="target"></div> +<script type="application/javascript"> + +function waitAndCheckMouseEvents(aTarget, aExpectedEvents) { + return new Promise((aResolve, aReject) => { + let timer; + let cleanup = function() { + if (timer) { + clearTimeout(timer); + timer = null; + } + aTarget.removeEventListener("mousedown", listener); + aTarget.removeEventListener("mouseup", listener); + aTarget.removeEventListener("contextmenu", listener); + aTarget.removeEventListener("click", listener); + aTarget.removeEventListener("auxclick", listener); + }; + + let listener = function(aEvent) { + aEvent.preventDefault(); + let expectedEvent = aExpectedEvents.shift(); + if (!expectedEvent) { + cleanup(); + ok(false, `receive unexpected ${aEvent.type} event`); + aReject(new Error(`receive unexpected ${aEvent.type} event`)); + return; + } + + isDeeply([aEvent.type, aEvent.button, aEvent.ctrlKey], expectedEvent, + `check received ${aEvent.type} event`); + if (!aExpectedEvents.length) { + // Wait a bit to see if there is any unexpected event. + timer = setTimeout(function() { + cleanup(); + aResolve(); + }, 0); + } + }; + + aTarget.addEventListener("mousedown", listener); + aTarget.addEventListener("mouseup", listener); + aTarget.addEventListener("contextmenu", listener); + aTarget.addEventListener("click", listener); + aTarget.addEventListener("auxclick", listener); + }); +} + +add_task(async function Init() { + await SimpleTest.promiseFocus(); + await waitUntilApzStable(); + + let target = document.getElementById("target"); + target.addEventListener("click", function() { + ok(false, `should not receive click event`); + }); +}); + +add_task(async function TestMouseClickWithControl() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.event.treat_ctrl_click_as_right_click.disabled", true]], + }); + + let target = document.getElementById("target"); + let promise = waitAndCheckMouseEvents(target, [["mousedown", 0, true], + ["contextmenu", 0, true], + ["mouseup", 0, true]]); + synthesizeNativeMouseEvent({ + type: "click", + target, + offsetX: 10, + offsetY: 10, + modifiers: { ctrlKey: true }, + }); + await promise; +}); + +add_task(async function TestOldBehavior() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.event.treat_ctrl_click_as_right_click.disabled", false]], + }); + + let target = document.getElementById("target"); + let promise = waitAndCheckMouseEvents(target, [["mousedown", 2, true], + ["contextmenu", 2, true], + ["mouseup", 2, true], + ["auxclick", 2, true]]); + synthesizeNativeMouseEvent({ + type: "click", + target, + offsetX: 10, + offsetY: 10, + modifiers: { ctrlKey: true }, + }); + await promise; +}); +</script> +</body> +</html> diff --git a/widget/tests/test_mouse_scroll.xhtml b/widget/tests/test_mouse_scroll.xhtml new file mode 100644 index 0000000000..82cb6a3ea3 --- /dev/null +++ b/widget/tests/test_mouse_scroll.xhtml @@ -0,0 +1,35 @@ +<?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 title="Testing composition, text and query content events" + onload="setTimeout(onLoad, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +function onLoad() +{ + runTest(); +} + +function runTest() +{ + window.openDialog("window_mouse_scroll_win.html", "_blank", + "chrome,width=600,height=600,noopener", window); +} +]]> +</script> +</window> diff --git a/widget/tests/test_native_key_bindings_mac.html b/widget/tests/test_native_key_bindings_mac.html new file mode 100644 index 0000000000..8767a5a77d --- /dev/null +++ b/widget/tests/test_native_key_bindings_mac.html @@ -0,0 +1,336 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title>Native Key Bindings for Cocoa Test</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"></script> + </head> + <body> + <div id="editable" contenteditable> + <p>Stretching attack nullam stuck in a tree zzz, suspendisse cras nec + suspendisse lick suscipit. Nunc egestas amet litter box, nullam climb the + curtains biting I don't like that food tristique biting sleep on your + keyboard non. Lay down in your way cras nec tempus chase the red dot cras + nec, pharetra pharetra eat the grass leap run orci turpis attack. + Consectetur sleep in the sink eat I don't like that food, knock over the + lamp catnip in viverra tail flick zzz meow etiam enim. Ac ac hiss shed + everywhere kittens rhoncus, attack your ankles zzz iaculis kittens. Nullam + pellentesque rip the couch iaculis rhoncus nibh, give me fish orci turpis + purr sleep on your face quis nunc bibendum.</p> + + <p>Neque jump on the table bat iaculis, adipiscing sleep on your keyboard + jump vel justo shed everywhere suspendisse lick. Zzz enim faucibus + hairball faucibus, pharetra sunbathe biting bat leap rip the couch attack. + Tortor nibh in viverra quis hairball nam, vulputate adipiscing sleep on + your keyboard purr knock over the lamp orci turpis. Vestibulum I don't + like that food et chase the red dot, adipiscing neque bibendum rutrum + accumsan quis rhoncus claw. Leap accumsan vehicula enim biting sleep on + your face, pharetra nam accumsan egestas kittens sunbathe. Pharetra chase + the red dot sniff non eat the grass, vulputate fluffy fur aliquam puking + judging you.</p> + + <p>Claw purr sollicitudin sollicitudin lay down in your way consectetur, + pellentesque vehicula zzz orci turpis consectetur. I don't like that food + rhoncus pellentesque sniff attack, rhoncus tortor attack your ankles + iaculis scratched hiss vel. Tortor zzz tortor nullam rip the couch rutrum, + bat enim ut leap hairball iaculis. Bibendum sunbathe elit suspendisse + nibh, puking adipiscing sleep on your face sleep on your face zzz catnip. + Judging you rutrum bat sunbathe sleep on your face, jump on the table leap + tincidunt a faucibus sleep in the sink. Stuck in a tree tristique zzz hiss + in viverra nullam, quis tortor pharetra attack.</p> + </div> + + <textarea id="textarea" cols="80"> + Stretching attack nullam stuck in a tree zzz, suspendisse cras nec + suspendisse lick suscipit. Nunc egestas amet litter box, nullam climb the + curtains biting I don't like that food tristique biting sleep on your + keyboard non. Lay down in your way cras nec tempus chase the red dot cras + nec, pharetra pharetra eat the grass leap run orci turpis attack. + Consectetur sleep in the sink eat I don't like that food, knock over the + lamp catnip in viverra tail flick zzz meow etiam enim. Ac ac hiss shed + everywhere kittens rhoncus, attack your ankles zzz iaculis kittens. Nullam + pellentesque rip the couch iaculis rhoncus nibh, give me fish orci turpis + purr sleep on your face quis nunc bibendum. + + Neque jump on the table bat iaculis, adipiscing sleep on your keyboard + jump vel justo shed everywhere suspendisse lick. Zzz enim faucibus + hairball faucibus, pharetra sunbathe biting bat leap rip the couch attack. + Tortor nibh in viverra quis hairball nam, vulputate adipiscing sleep on + your keyboard purr knock over the lamp orci turpis. Vestibulum I don't + like that food et chase the red dot, adipiscing neque bibendum rutrum + accumsan quis rhoncus claw. Leap accumsan vehicula enim biting sleep on + your face, pharetra nam accumsan egestas kittens sunbathe. Pharetra chase + the red dot sniff non eat the grass, vulputate fluffy fur aliquam puking + judging you. + + Claw purr sollicitudin sollicitudin lay down in your way consectetur, + pellentesque vehicula zzz orci turpis consectetur. I don't like that food + rhoncus pellentesque sniff attack, rhoncus tortor attack your ankles + iaculis scratched hiss vel. Tortor zzz tortor nullam rip the couch rutrum, + bat enim ut leap hairball iaculis. Bibendum sunbathe elit suspendisse + nibh, puking adipiscing sleep on your face sleep on your face zzz catnip. + Judging you rutrum bat sunbathe sleep on your face, jump on the table leap + tincidunt a faucibus sleep in the sink. Stuck in a tree tristique zzz hiss + in viverra nullam, quis tortor pharetra attack. + </textarea> + + <input id="input" type="text" + value="Stretching attack nullam stuck in a tree zzz, suspendisse cras nec + suspendisse lick suscipit. Nunc egestas amet litter box, nullam climb the + curtains biting I don't like that food tristique biting sleep on your + keyboard non. Lay down in your way cras nec tempus chase the red dot cras + nec, pharetra pharetra eat the grass leap run orci turpis attack. + Consectetur sleep in the sink eat I don't like that food, knock over the + lamp catnip in viverra tail flick zzz meow etiam enim. Ac ac hiss shed + everywhere kittens rhoncus, attack your ankles zzz iaculis kittens. + Nullam pellentesque rip the couch iaculis rhoncus nibh, give me fish orci + turpis purr sleep on your face quis nunc bibendum."> + + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + let synthesizedKeys = []; + let expectations = []; + + // Move to beginning of line + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_LeftArrow, + {ctrlKey: true}, "\uf702", "\uf702"]); + expectations.push({ + editable: [0, 0], + textarea: [0, 0], + input: [0, 0], + }); + + // Move to end of line + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow, + {ctrlKey: true}, "\uf703", "\uf703"]); + expectations.push({ + editable: [73, 73], + textarea: [72, 72], + input: [732, 732], + }); + + // Move down + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_N, + {ctrlKey: true}, "\u000e", "n"]); + expectations.push({ + editable: [140, 140], + textarea: [145, 145], + input: [732, 732], + }); + + // Move to beginning of line + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_LeftArrow, + {ctrlKey: true}, "\uf702", "\uf702"]); + expectations.push({ + editable: [73, 73], + textarea: [73, 73], + input: [0, 0], + }); + + // Move word right and modify selection + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow, + {altKey: true, shiftKey: true}, "\uf703", "\uf703"]); + expectations.push({ + editable: [73, 84], + textarea: [73, 90], + input: [0, 10], + }); + + // Move word right + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow, + {altKey: true}, "\uf703", "\uf703"]); + expectations.push({ + editable: [84, 84], + textarea: [90, 90], + input: [10, 10], + }); + + // Move word right + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow, + {altKey: true}, "\uf703", "\uf703"]); + expectations.push({ + editable: [89, 89], + textarea: [95, 95], + input: [17, 17], + }); + + // Move down and modify selection + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_DownArrow, + {shiftKey: true}, "\uf701", "\uf701"]); + expectations.push({ + editable: [89, 171], + textarea: [95, 175], + input: [17, 732], + }); + + // Move backward and modify selection + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_B, + {ctrlKey: true, shiftKey: true}, "\u0002", "B"]); + expectations.push({ + editable: [89, 170], + textarea: [95, 174], + input: [17, 731], + }); + + // Delete forward + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_D, + {ctrlKey: true}, "\u0004", "d"]); + expectations.push({ + editable: [89, 89], + textarea: [95, 95], + input: [17, 17], + }); + + // Delete backward + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_H, + {ctrlKey: true}, "\u0008", "h"]); + expectations.push({ + editable: [88, 88], + textarea: [94, 94], + input: [16, 16], + }); + + // Move backward + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_B, + {ctrlKey: true}, "\u0002", "b"]); + expectations.push({ + editable: [87, 87], + textarea: [93, 93], + input: [15, 15], + }); + + // Move to beginning of paragraph (line for now) + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, + {ctrlKey: true}, "\u0001", "a"]); + expectations.push({ + editable: [73, 73], + textarea: [73, 73], + input: [0, 0], + }); + + // Move forward + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_F, + {ctrlKey: true}, "\u0006", "f"]); + expectations.push({ + editable: [74, 74], + textarea: [74, 74], + input: [1, 1], + }); + + // Move word right + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow, + {altKey: true}, "\uf703", "\uf703"]); + expectations.push({ + editable: [84, 84], + textarea: [90, 90], + input: [10, 10], + }); + + // Move word right + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_RightArrow, + {altKey: true}, "\uf703", "\uf703"]); + expectations.push({ + editable: [88, 88], + textarea: [94, 94], + input: [17, 17], + }); + + // Delete to end of paragraph (line for now) + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_K, + {ctrlKey: true}, "\u000b", "k"]); + expectations.push({ + editable: [88, 88], + textarea: [94, 94], + input: [17, 17], + }); + + // Move backward and modify selection + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_B, + {ctrlKey: true, shiftKey: true}, "\u0002", "B"]); + expectations.push({ + editable: [88, 87], + textarea: [93, 94], + input: [16, 17], + }); + + // Move to end of paragraph (line for now) + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_E, + {ctrlKey: true}, "\u0005", "e"]); + expectations.push({ + editable: [139, 139], + textarea: [94, 94], + input: [17, 17], + }); + + // Move up + synthesizedKeys.push([KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_P, + {ctrlKey: true}, "\u0010", "p"]); + expectations.push({ + editable: [73, 73], + textarea: [21, 21], + input: [0, 0], + }); + + function checkWindowSelection(aElement, aSelection) { + let selection = window.getSelection(); + + is(selection.anchorOffset, aSelection[aElement.id][0], + aElement.id + ": Incorrect anchor offset"); + is(selection.focusOffset, aSelection[aElement.id][1], + aElement.id + ": Incorrect focus offset"); + } + + function checkElementSelection(aElement, aSelection) { + is(aElement.selectionStart, aSelection[aElement.id][0], + aElement.id + ": Incorrect selection start"); + is(aElement.selectionEnd, aSelection[aElement.id][1], + aElement.id + ": Incorrect selection end"); + } + + function* testRun(aElement, aSelectionCheck, aCallback) { + if (document.activeElement) { + document.activeElement.blur(); + } + + aElement.focus(); + + for (let i = 0; i < synthesizedKeys.length; i++) { + synthesizedKeys[i].push(function() { + aSelectionCheck(aElement, expectations[i]); + continueTest(); + }); + var synthOk = synthesizeNativeKey.apply(null, synthesizedKeys[i]); + synthesizedKeys[i].pop(); + yield synthOk; + } + } + + function* doTest() { + yield* testRun(document.getElementById("editable"), checkWindowSelection); + yield* testRun(document.getElementById("textarea"), checkElementSelection); + yield* testRun(document.getElementById("input"), checkElementSelection); + } + + let gTestContinuation = null; + + function continueTest() { + if (!gTestContinuation) { + gTestContinuation = doTest(); + } + var ret = gTestContinuation.next(); + if (ret.done) { + SimpleTest.finish(); + } else { + is(ret.value, true, "Successfully synthesized key"); + } + } + + SimpleTest.waitForFocus(continueTest); + </script> + </body> +</html> diff --git a/widget/tests/test_native_menus.xhtml b/widget/tests/test_native_menus.xhtml new file mode 100644 index 0000000000..d62c57f21c --- /dev/null +++ b/widget/tests/test_native_menus.xhtml @@ -0,0 +1,29 @@ +<?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 title="Native menu system tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<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 class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("native_menus_window.xhtml", "NativeMenuWindow", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/widget/tests/test_panel_mouse_coords.xhtml b/widget/tests/test_panel_mouse_coords.xhtml new file mode 100644 index 0000000000..43c4e10249 --- /dev/null +++ b/widget/tests/test_panel_mouse_coords.xhtml @@ -0,0 +1,78 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=835044 +--> +<window title="Mozilla Bug 835044" + onload="startTest()" + 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"/> + +<panel id="thepanel" level="parent" + onpopupshown="sendMouseEvent();" + onmousemove="checkCoords(event);" + style="width: 80px; height: 80px"> +</panel> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=835044" + id="anchor" + target="_blank">Mozilla Bug 835044</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ +SimpleTest.waitForExplicitFinish(); + +let panel = document.getElementById('thepanel'); +let rect = null; + + +function startTest() { + // This first event is to ensure that the next event will have different + // coordinates to the previous mouse position, and so actually generates + // mouse events. The mouse is not moved off the window, as that might + // move focus to another application. + synthesizeNativeMouseEvent({ + type: "mousemove", + screenX: window.mozInnerScreenX, + screenY: window.mozInnerScreenY, + elementOnWidget: window.documentElement, + }); + + panel.openPopup(document.getElementById("anchor"), "after_start"); +} + +function sendMouseEvent() { + rect = panel.getBoundingClientRect(); + synthesizeNativeMouseEvent({ + type: "mousemove", + target: panel, + offsetX: 10, + offsetY: 20, + }); +} + +function checkCoords(event) { + if (!rect) { + return; + } + isfuzzy(event.clientX, rect.left + 10, window.devicePixelRatio, "Motion x coordinate"); + isfuzzy(event.clientY, rect.top + 20, window.devicePixelRatio, "Motion y coordinate"); + info(`Event client: ${event.clientX}, ${event.clientY}, panel client: ${rect.left}, ${rect.top}`); + info(`Event screen: ${event.screenX}, ${event.screenY}, panel screen: ${panel.screenX}, ${panel.screenY}`); + info(`offset client: ${event.clientX - rect.left}, ${event.clientY - rect.top}`); + info(`offset screen: ${event.screenX - panel.screenX}, ${event.screenY - panel.screenY}`); + done(); +} + +function done() { + SimpleTest.finish(); +} + ]]> + </script> +</window> diff --git a/widget/tests/test_picker_no_crash.html b/widget/tests/test_picker_no_crash.html new file mode 100644 index 0000000000..dbb75627b5 --- /dev/null +++ b/widget/tests/test_picker_no_crash.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<title>Test for crashes when the parent window of a file picker is closed via script</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<script> +SimpleTest.requestFlakyTimeout("testing we don't crash"); + +async function testPicker(id) { + let childWindow = window.open("window_picker_no_crash_child.html", "childWindow", "width=500,height=500"); + await SimpleTest.promiseFocus(childWindow); + ok(!childWindow.clicked, "Shouldn't have clicked"); + synthesizeMouseAtCenter(childWindow.document.getElementById(id), {}, childWindow); + ok(childWindow.clicked, "Should have clicked"); + childWindow.close(); +} + +add_task(async function test_simple() { + await testPicker("uploadbox"); +}); + +add_task(async function test_multiple() { + await testPicker("multiple"); +}); + +add_task(async function wait() { + await new Promise(r => setTimeout(r, 1000)); + ok(true, "browser didn't crash"); +}); +</script> diff --git a/widget/tests/test_platform_colors.xhtml b/widget/tests/test_platform_colors.xhtml new file mode 100644 index 0000000000..8f9860d85b --- /dev/null +++ b/widget/tests/test_platform_colors.xhtml @@ -0,0 +1,95 @@ +<?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 title="Mac platform colors" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=518395">Mozilla Bug 518395</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<box id="colorbox"></box> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var colors = { + "activeborder": ["rgb(0, 0, 0)"], + "activecaption": ["rgb(204, 204, 204)"], + "appworkspace": ["rgb(255, 255, 255)"], + "background": ["rgb(99, 99, 206)"], + "buttonface": ["rgb(240, 240, 240)"], + "buttonhighlight": ["rgb(255, 255, 255)"], + "buttonshadow": ["rgb(220, 220, 220)"], + "buttontext": ["rgb(0, 0, 0)"], + "captiontext": ["rgb(0, 0, 0)"], + "graytext": ["rgb(127, 127, 127)"], + "highlight": ["rgb(115, 132, 153)", "rgb(127, 127, 127)", "rgb(56, 117, 215)", "rgb(255, 193, 31)", "rgb(243, 70, 72)", "rgb(255, 138, 34)", "rgb(102, 197, 71)", "rgb(140, 78, 184)"], + "highlighttext": ["rgb(255, 255, 255)", "rgb(255, 254, 254)", "rgb(254, 255, 254)"], + "inactiveborder": ["rgb(255, 255, 255)"], + "inactivecaption": ["rgb(255, 255, 255)"], + "inactivecaptiontext": ["rgb(69, 69, 69)"], + "infobackground": ["rgb(255, 255, 199)"], + "infotext": ["rgb(0, 0, 0)"], + "menu": ["rgb(255, 255, 255)", "rgb(254, 255, 254)", "rgb(255, 254, 254)"], + "menutext": ["rgb(0, 0, 0)"], + "scrollbar": ["rgb(170, 170, 170)"], + "threeddarkshadow": ["rgb(220, 220, 220)"], + "threedface": ["rgb(240, 240, 240)"], + "threedhighlight": ["rgb(255, 255, 255)"], + "threedlightshadow": ["rgb(218, 218, 218)"], + "threedshadow": ["rgb(224, 224, 224)"], + "window": ["rgb(255, 255, 255)"], + "windowframe": ["rgb(204, 204, 204)"], + "windowtext": ["rgb(0, 0, 0)"], + "-moz-activehyperlinktext": ["rgb(238, 0, 0)"], + "-moz-buttonhoverface": ["rgb(240, 240, 240)"], + "-moz-buttonhovertext": ["rgb(0, 0, 0)"], + "-moz-cellhighlight": ["rgb(212, 212, 212)", "rgb(220, 220, 220)"], + "-moz-cellhighlighttext": ["rgb(0, 0, 0)"], + "-moz-eventreerow": ["rgb(255, 255, 255)"], + "-moz-field": ["rgb(255, 255, 255)"], + "-moz-fieldtext": ["rgb(0, 0, 0)"], + "-moz-dialog": ["rgb(232, 232, 232)"], + "-moz-dialogtext": ["rgb(0, 0, 0)"], + "-moz-hyperlinktext": ["rgb(0, 0, 238)"], + "-moz-html-cellhighlight": ["rgb(212, 212, 212)"], + "-moz-html-cellhighlighttext": ["rgb(0, 0, 0)"], + "-moz-buttonactivetext": ["rgb(0, 0, 0)", "rgb(255, 255, 255)"], + "-moz-mac-defaultbuttontext": ["rgb(0, 0, 0)", "rgb(255, 255, 255)"], + //"-moz-mac-focusring": ["rgb(83, 144, 210)", "rgb(95, 112, 130)", "rgb(63, 152, 221)", "rgb(108, 126, 141)"], + "-moz-mac-menutextdisable": ["rgb(152, 152, 152)"], + "-moz-mac-menutextselect": ["rgb(255, 255, 255)"], + "-moz-mac-disabledtoolbartext": ["rgb(127, 127, 127)"], + "-moz-menuhover": ["rgb(115, 132, 153)", "rgb(127, 127, 127)", "rgb(56, 117, 215)", "rgb(255, 193, 31)", "rgb(243, 70, 72)", "rgb(255, 138, 34)", "rgb(102, 197, 71)", "rgb(140, 78, 184)"], + "-moz-menuhovertext": ["rgb(255, 255, 255)", "rgb(255, 254, 254)", "rgb(254, 255, 254)"], + //"-moz-menubarhovertext": ["rgb(255, 255, 255)"], + "-moz-oddtreerow": ["rgb(236, 242, 254)", "rgb(240, 240, 240)", "rgb(243, 245, 250)", "rgb(243, 246, 250)", "rgb(245, 245, 245)"], + "-moz-visitedhyperlinktext": ["rgb(85, 26, 139)"], + "currentcolor": ["rgb(0, 0, 0)"], + "-moz-comboboxtext": ["rgb(0, 0, 0)"], + "-moz-combobox": ["rgb(255, 255, 255)"] +}; + +var colorbox = document.getElementById('colorbox'); + +for (var c in colors) { + dump("testing color " + c + "\n"); + colorbox.style.backgroundColor = c; + var rgb = document.defaultView.getComputedStyle(colorbox).getPropertyValue('background-color'); + ok(colors[c].includes(rgb) || colors[c].length == 8, 'platform color ' + c + ' is wrong: ' + rgb); +} + + +]]> +</script> + +</window> diff --git a/widget/tests/test_position_on_resize.xhtml b/widget/tests/test_position_on_resize.xhtml new file mode 100644 index 0000000000..a7c5551018 --- /dev/null +++ b/widget/tests/test_position_on_resize.xhtml @@ -0,0 +1,90 @@ +<?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 title="Window Position On Resize Test" + onload="startTest()" + 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 class="testbody" type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + let win, x, y; + + function startTest() { + win = window.browsingContext.topChromeWindow.openDialog( + "about:blank", + null, + "chrome,dialog=no,outerHeight=170,outerWidth=200"); + waitForSuccess(function() { return SpecialPowers.DOMWindowUtils.paintCount }, + "No paint received", checkInitialSize); + } + + function checkInitialSize() { + is(win.outerHeight,170, "initial outerHeight"); + is(win.outerWidth, 200, "initial outerWidth"); + x = win.screenX; + y = win.screenY; + shrink(); + } + function shrink() { + win.resizeTo(180, 160); + waitForSuccess(function() { return win.outerHeight == 160 }, + "outerHeight did not change to 160", checkShrink); + } + function checkShrink() { + is(win.outerWidth, 180, "resized outerWidth"); + is(win.screenY, y, "resized window top should not change"); + y = win.screenY; + restore(); + } + function restore() { + win.resizeBy(20, 10); + waitForSuccess(function() { return win.outerHeight == 170 }, + "outerHeight did not change to 170", checkRestore); + } + function checkRestore() { + is(win.outerWidth, 200, "restored outerWidth"); + is(win.screenX, x, "restored window left should not change"); + is(win.screenY, y, "restored window top should not change"); + done(); + } + function done() { + win.close(); + SimpleTest.finish(); + } + + function waitForSuccess(testForSuccess, failureMsg, nextFunc) { + var waitCount = 0; + + function repeatWait() { + ++waitCount; + + if (testForSuccess()) { + nextFunc(); + } + else if (waitCount > 50) { + ok(false, failureMsg); + nextFunc(); + } else { + setTimeout(repeatWait, 100); + } + } + + repeatWait(); + } +]]></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/widget/tests/test_secure_input.html b/widget/tests/test_secure_input.html new file mode 100644 index 0000000000..846465b4c2 --- /dev/null +++ b/widget/tests/test_secure_input.html @@ -0,0 +1,141 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for secure input mode</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<p> + <input id="input_text" type="text"><br> + <input id="input_password" type="password"><br> + <input id="input_text_readonly" type="text" readonly><br> + <input id="input_text_ime_mode_disabled" type="text" style="ime-mode: disabled;"><br> + <input id="input_change" type="text"><br> + <textarea id="textarea"></textarea><br> +</p> +<div id="contenteditable" contenteditable style="min-height: 3em;"></div> + +<script class="testbody" type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + function sendAKeyEvent() { + synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_A, {}, "a", "a"); + } + + function isFocused(aElement) { + return (SpecialPowers.focusManager.focusedElement == aElement); + } + + function runTest() { + sendAKeyEvent(); + ok(true, "Not crashed: input on the document"); + $("input_text").focus(); + sendAKeyEvent(); + ok(true, "Not crashed: input on <input type=\"text\">"); + $("input_password").focus(); + sendAKeyEvent(); + ok(true, "Not crashed: input on <input type=\"password\">"); + $("input_password").blur(); + sendAKeyEvent(); + ok(true, "Not crashed: input on the document after blur() of <input type=\"password\">"); + $("input_password").focus(); + $("input_text_readonly").focus(); + sendAKeyEvent(); + ok(true, "Not crashed: input on <input type=\"text\" readonly>"); + $("input_password").focus(); + $("input_text_ime_mode_disabled").focus(); + sendAKeyEvent(); + ok(true, "Not crashed: input on <input type=\"text\" style=\"ime-mode: disabled;\">"); + $("input_password").focus(); + $("textarea").focus(); + sendAKeyEvent(); + ok(true, "Not crashed: input on <textarea>"); + $("input_password").focus(); + $("contenteditable").focus(); + sendAKeyEvent(); + ok(true, "Not crashed: input on <div contenteditable>"); + + $("input_change").focus(); + $("input_change").type = "password"; + sendAKeyEvent(); + ok(true, "Not crashed: input on <input type=\"password\"> changed from type=\"text\""); + $("input_change").type = "text"; + sendAKeyEvent(); + ok(true, "Not crashed: input on <input type=\"text\"> changed from type=\"password\""); + + var otherWindow = + window.browsingContext.topChromeWindow.open("file_secure_input.html", + "_blank", "chrome,width=100,height=100"); + ok(otherWindow, "failed to open other window"); + if (!otherWindow) { + SimpleTest.finish(); + return; + } + + $("input_text").focus(); + otherWindow.focus(); + + SimpleTest.waitForFocus(function() { + window.focus(); + sendAKeyEvent(); + ok(isFocused($("input_text")), "focused element isn't <input type=\"text\">"); + ok(true, "Not crashed: input on <input type=\"text\"> after the other document has focus"); + + $("input_password").focus(); + otherWindow.focus(); + window.focus(); + sendAKeyEvent(); + ok(isFocused($("input_password")), "focused element isn't <input type=\"password\">"); + ok(true, "Not crashed: input on <input type=\"password\"> after the other document has focus"); + + $("input_text").focus(); + otherWindow.focus(); + otherWindow.document.getElementById("text").focus(); + window.focus(); + sendAKeyEvent(); + ok(isFocused($("input_text")), "focused element isn't <input type=\"text\">"); + ok(true, "Not crashed: input on <input type=\"text\"> after the other document's <input type=\"text\"> has focus"); + + $("input_password").focus(); + otherWindow.focus(); + otherWindow.document.getElementById("text").focus(); + window.focus(); + sendAKeyEvent(); + ok(isFocused($("input_password")), "focused element isn't <input type=\"password\">"); + ok(true, "Not crashed: input on <input type=\"password\"> after the other document's <input type=\"text\"> has focus"); + + $("input_text").focus(); + otherWindow.focus(); + otherWindow.document.getElementById("password").focus(); + window.focus(); + sendAKeyEvent(); + ok(isFocused($("input_text")), "focused element isn't <input type=\"text\">"); + ok(true, "Not crashed: input on <input type=\"text\"> after the other document's <input type=\"password\"> has focus"); + + $("input_password").focus(); + otherWindow.focus(); + otherWindow.document.getElementById("password").focus(); + window.focus(); + sendAKeyEvent(); + ok(isFocused($("input_password")), "focused element isn't <input type=\"password\">"); + ok(true, "Not crashed: input on <input type=\"password\"> after the other document's <input type=\"password\"> has focus"); + + SimpleTest.finish(); + }, otherWindow); + } + + SimpleTest.waitForFocus(runTest); +</script> +</body> +</html> diff --git a/widget/tests/test_sizemode_events.xhtml b/widget/tests/test_sizemode_events.xhtml new file mode 100644 index 0000000000..bd1e3a38d1 --- /dev/null +++ b/widget/tests/test_sizemode_events.xhtml @@ -0,0 +1,148 @@ +<?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 title="Test for bug 715867" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<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 class="testbody"> +<![CDATA[ + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +let gWindow = null; + +const kIsLinux = navigator.platform.includes("Lin"); +const kIsMacOS = navigator.platform.includes("Mac"); +// On Linux and macOS sizemode changes might be async. +const kAsyncChanges = kIsLinux || kIsMacOS; + +let gSizeModeDidChange = false; +let gSizeModeDidChangeTo = 0; + +function sizemodeChanged(e) { + gSizeModeDidChange = true; + gSizeModeDidChangeTo = gWindow.windowState; +} + +async function expectSizeModeChange(newMode, duringActionCallback) { + gSizeModeDidChange = false; + + let promise = null; + if (kAsyncChanges) { + if (newMode) { + promise = new Promise(resolve => { + gWindow.addEventListener("sizemodechange", function() { + SimpleTest.executeSoon(resolve); + }, { once: true }) + }); + } else { + promise = new Promise(SimpleTest.executeSoon); + } + } + + duringActionCallback(); + + if (promise) { + await promise; + } + + if (newMode == 0) { + // No change should have taken place, no event should have fired. + ok(!gSizeModeDidChange, "No sizemodechange event should have fired."); + } else { + // Size mode change event was expected to fire. + ok(gSizeModeDidChange, "A sizemodechanged event should have fired."); + is(gSizeModeDidChangeTo, newMode, "The new sizemode should have the expected value."); + const expectedHidden = newMode == gWindow.STATE_MINIMIZED || gWindow.isFullyOccluded; + if (gWindow.document.hidden != expectedHidden) { + await new Promise(resolve => { + gWindow.addEventListener("visibilitychange", resolve, { once: true }); + }); + } + is(gWindow.document.hidden, expectedHidden, "Should be inactive if minimized or occluded."); + } +} + +function startTest() { + openWindow(); +} + +function openWindow() { + gWindow = window.browsingContext.topChromeWindow + .open('empty_window.xhtml', '_blank', 'chrome,screenX=50,screenY=50,width=200,height=200,resizable'); + SimpleTest.waitForFocus(runTest, gWindow); +} + +async function runTest() { + // Install event handler. + gWindow.addEventListener("sizemodechange", sizemodeChanged); + + // Run tests. + info("Testing minimize()"); + await expectSizeModeChange(gWindow.STATE_MINIMIZED, function () { + gWindow.minimize(); + }); + + info("Testing restore() after minimize()"); + await expectSizeModeChange(gWindow.STATE_NORMAL, function () { + gWindow.restore(); + }); + + info("Testing maximize()"); + await expectSizeModeChange(gWindow.STATE_MAXIMIZED, function () { + gWindow.maximize(); + }); + + info("Testing restore() after maximize()"); + await expectSizeModeChange(gWindow.STATE_NORMAL, function () { + gWindow.restore(); + }); + + // Normal window resizing shouldn't fire a sizemodechanged event, bug 715867. + info("Testing resizeTo() horizontal"); + await expectSizeModeChange(0, function () { + gWindow.resizeTo(gWindow.outerWidth + 10, gWindow.outerHeight); + }); + + info("Testing resizeTo() vertical"); + await expectSizeModeChange(0, function () { + gWindow.resizeTo(gWindow.outerWidth, gWindow.outerHeight + 10); + }); + + // Resizing a maximized window should change to normal sizemode. + info("maximize() in preparation for resize"); + await expectSizeModeChange(gWindow.STATE_MAXIMIZED, function () { + gWindow.maximize(); + }); + + info("Testing resizeTo() from maximized"); + await expectSizeModeChange(gWindow.STATE_NORMAL, function () { + // MacOS treats windows close to the available screen size as maximized. + // Shrinking the window by only 10px isn't enough to change the sizemode. + gWindow.resizeTo(gWindow.outerWidth / 2, gWindow.outerHeight / 2); + }); + + gWindow.removeEventListener("sizemodechange", sizemodeChanged); + gWindow.close(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(startTest); + +]]> +</script> + +</window> diff --git a/widget/tests/test_standalone_native_menu.xhtml b/widget/tests/test_standalone_native_menu.xhtml new file mode 100644 index 0000000000..96e41036c3 --- /dev/null +++ b/widget/tests/test_standalone_native_menu.xhtml @@ -0,0 +1,29 @@ +<?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 title="Standalone Native Menu tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<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 class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("standalone_native_menu_window.xhtml", "StandaloneNativeMenuWindow", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/widget/tests/test_surrogate_pair_native_key_handling.xhtml b/widget/tests/test_surrogate_pair_native_key_handling.xhtml new file mode 100644 index 0000000000..98834e1206 --- /dev/null +++ b/widget/tests/test_surrogate_pair_native_key_handling.xhtml @@ -0,0 +1,178 @@ +<?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> diff --git a/widget/tests/test_system_font_changes.xhtml b/widget/tests/test_system_font_changes.xhtml new file mode 100644 index 0000000000..036c775463 --- /dev/null +++ b/widget/tests/test_system_font_changes.xhtml @@ -0,0 +1,28 @@ +<?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 title="Native menu system tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<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 class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("system_font_changes.xhtml", "system_font_changes_window", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/widget/tests/test_system_status_bar.xhtml b/widget/tests/test_system_status_bar.xhtml new file mode 100644 index 0000000000..f2348fa6f5 --- /dev/null +++ b/widget/tests/test_system_status_bar.xhtml @@ -0,0 +1,53 @@ +<?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 title="Testing composition, text and query content events" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> + +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<menupopup id="menuContainer"> + <menu id="menu1" image="data:image/svg+xml,<svg%20xmlns="http://www.w3.org/2000/svg"%20width="32"%20height="32"><circle%20cx="16"%20cy="16"%20r="16"/></svg>"> + <menupopup> + <menuitem label="Item 1 in menu 1"/> + <menuitem label="Item 2 in menu 1"/> + </menupopup> + </menu> + <menu id="menu2" image="data:image/svg+xml,<svg%20xmlns="http://www.w3.org/2000/svg"%20width="32"%20height="32"><path%20d="M0 16 L 16 0 L 32 16 L 16 32 Z"/></svg>"> + <menupopup> + <menuitem label="Item 1 in menu 2"/> + <menuitem label="Item 2 in menu 2"/> + </menupopup> + </menu> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + let systemStatusBar = Cc["@mozilla.org/widget/systemstatusbar;1"].getService(Ci.nsISystemStatusBar); + ok(systemStatusBar, "should have got an nsISystemStatusBar instance"); + + let menu1 = document.getElementById("menu1"); + let menu2 = document.getElementById("menu2"); + + // Add and remove the item, just to get basic leak testing coverage. + systemStatusBar.addItem(menu1); + systemStatusBar.removeItem(menu1); + + // Make sure that calling addItem twice with the same element doesn't leak. + systemStatusBar.addItem(menu2); + systemStatusBar.addItem(menu2); + systemStatusBar.removeItem(menu2); + +]]> +</script> +</window> diff --git a/widget/tests/test_taskbar_progress.xhtml b/widget/tests/test_taskbar_progress.xhtml new file mode 100644 index 0000000000..f2494a27bb --- /dev/null +++ b/widget/tests/test_taskbar_progress.xhtml @@ -0,0 +1,117 @@ +<?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 title="Taskbar Previews Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="loaded();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script class="testbody" type="application/javascript"> + <![CDATA[ + let TP = Ci.nsITaskbarProgress; + + function IsWin7OrHigher() { + try { + var ver = parseFloat(Services.sysinfo.getProperty("version")); + if (ver >= 6.1) + return true; + } catch (ex) { } + return false; + } + + function winProgress() { + let taskbar = Cc["@mozilla.org/windows-taskbar;1"]; + if (!taskbar) { + ok(false, "Taskbar service is always available"); + return null; + } + taskbar = taskbar.getService(Ci.nsIWinTaskbar); + + is(taskbar.available, IsWin7OrHigher(), "Expected availability of taskbar"); + if (!taskbar.available) + return null; + + // HACK from mconnor: + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let docShell = win.docShell; + + let progress = taskbar.getTaskbarProgress(docShell); + isnot(progress, null, "Progress is not null"); + + try { + taskbar.getTaskbarProgress(null); + ok(false, "Got progress for null docshell"); + } catch (e) { + ok(true, "Cannot get progress for null docshell"); + } + + return progress; + } + + function macProgress() { + let progress = Cc["@mozilla.org/widget/macdocksupport;1"]; + if (!progress) { + ok(false, "Should have gotten Mac progress service."); + return null; + } + return progress.getService(TP); + } + + SimpleTest.waitForExplicitFinish(); + + function loaded() + { + let isWin = /Win/.test(navigator.platform); + let progress = isWin ? winProgress() : macProgress(); + if (!TP || !progress) { + SimpleTest.finish(); + return; + } + + function shouldThrow(s,c,m) { + try { + progress.setProgressState(s,c,m); + return false; + } catch (e) { + return true; + } + } + + function doesntThrow(s,c,m) { + return !shouldThrow(s,c,m); + } + + ok(doesntThrow(TP.STATE_NO_PROGRESS, 0, 0), "No progress state can be set"); + ok(doesntThrow(TP.STATE_INDETERMINATE, 0, 0), "Indeterminate state can be set"); + ok(doesntThrow(TP.STATE_NORMAL, 0, 0), "Normal state can be set"); + ok(doesntThrow(TP.STATE_ERROR, 0, 0), "Error state can be set"); + ok(doesntThrow(TP.STATE_PAUSED, 0, 0), "Paused state can be set"); + + ok(shouldThrow(TP.STATE_NO_PROGRESS, 1, 1), "Cannot set no progress with values"); + ok(shouldThrow(TP.STATE_INDETERMINATE, 1, 1), "Cannot set indeterminate with values"); + + // Technically passes since unsigned(-1) > 10 + ok(shouldThrow(TP.STATE_NORMAL, -1, 10), "Cannot set negative progress"); + todo(shouldThrow(TP.STATE_NORMAL, 1, -1), "Cannot set negative progress"); + todo(shouldThrow(TP.STATE_NORMAL, -1, -1), "Cannot set negative progress"); + todo(shouldThrow(TP.STATE_NORMAL, -2, -1), "Cannot set negative progress"); + + ok(shouldThrow(TP.STATE_NORMAL, 5, 3), "Cannot set progress greater than max"); + + ok(doesntThrow(TP.STATE_NORMAL, 1, 5), "Normal state can be set with values"); + ok(doesntThrow(TP.STATE_ERROR, 3, 6), "Error state can be set with values"); + ok(doesntThrow(TP.STATE_PAUSED, 2, 9), "Paused state can be set with values"); + + SimpleTest.finish(); + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/widget/tests/test_textScaleFactor_system_font.html b/widget/tests/test_textScaleFactor_system_font.html new file mode 100644 index 0000000000..2d0333e5fa --- /dev/null +++ b/widget/tests/test_textScaleFactor_system_font.html @@ -0,0 +1,139 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test that system font sizing is independent from ui.textScaleFactor</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <style> + p { width: max-content } + #menu { font: menu } + </style> +</head> +<body> + <p id="menu">"menu" text.</p> + <p id="default">Default text.</p> +</body> +<script> +"use strict"; + +const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +// Returns a Number for the font size in CSS pixels. +function elementFontSize(element) { + return parseFloat(getComputedStyle(element).getPropertyValue("font-size")); +} + +// "look-and-feel-changed" may be dispatched twice: once for the pref +// change and once after receiving the new values from +// ContentChild::RecvThemeChanged(). +// pushPrefEnv() resolves after the former. This resolves after the latter. +function promiseNewFontSizeOnThemeChange(element) { + return new Promise(resolve => { + const lastSize = elementFontSize(element); + + function ThemeChanged() { + const size = elementFontSize(element); + if (size != lastSize) { + resolve(size); + } + } + // "look-and-feel-changed" is dispatched before the style system is flushed, + // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/layout/base/nsPresContext.cpp#1684,1703-1705 + // so use an async observer to get a notification after style changes. + SpecialPowers.addAsyncObserver(ThemeChanged, "look-and-feel-changed"); + SimpleTest.registerCleanupFunction(function() { + SpecialPowers.removeAsyncObserver(ThemeChanged, "look-and-feel-changed"); + }); + }); +} + +function fuzzyCompareLength(actual, expected, message, tolerance, expectFn) { + expectFn(Math.abs(actual-expected) <= tolerance, + `${message} - got ${actual}, expected ${expected} +/- ${tolerance}`); +} + +add_task(async () => { + // MOZ_HEADLESS is set in content processes with GTK regardless of the + // headless state of the parent process. Check the parent state. + const headless = await SpecialPowers.spawnChrome([], function get_headless() { + return Services.env.get("MOZ_HEADLESS"); + }); + // LookAndFeel::TextScaleFactor::FloatID is implemented only for WINNT and + // GTK. ui.textScaleFactor happens to scale CSS pixels on other platforms + // but system font integration has not been implemented. + const expectSuccess = AppConstants.MOZ_WIDGET_TOOLKIT == "windows" || + (AppConstants.MOZ_WIDGET_TOOLKIT == "gtk" && + // Headless GTK doesn't get system font sizes from the system, but + // uses sizes fixed in CSS pixels. + !headless); + + async function setScaleAndPromiseFontSize(scale, element) { + const prefPromise = SpecialPowers.pushPrefEnv({ + set: [["ui.textScaleFactor", scale]], + }); + if (!expectSuccess) { + // The size is not expected to change but get it afresh to check our + // assumption. + await prefPromise; + return elementFontSize(element); + } + const [size] = await Promise.all([ + promiseNewFontSizeOnThemeChange(element), + prefPromise, + ]); + return size; + } + + const menu = document.getElementById("menu"); + const def = document.getElementById("default"); + // Choose a scaleFactor value different enough from possible default values + // that app unit rounding does not prevent a change in devicePixelRatio. + // A scaleFactor of 120 also has no rounding of app units per dev pixel. + const referenceScale = 120; + const menuSize1 = await setScaleAndPromiseFontSize(referenceScale, menu); + const menuRect1 = menu.getBoundingClientRect(); + const defSize1 = elementFontSize(def); + const defRect1 = def.getBoundingClientRect(); + + const expectFn = expectSuccess ? ok : todo; + const menuSize2 = await setScaleAndPromiseFontSize(2*referenceScale, menu); + { + const singlePrecisionULP = Math.pow(2, -23); + // Precision seems to be lost in the conversion to decimal string for the + // property value. + const reltolerance = 30 * singlePrecisionULP; + fuzzyCompareLength(menuSize2, menuSize1/2, "size of menu font", + reltolerance*menuSize1/2, expectFn); + } + { + const menuRect2 = menu.getBoundingClientRect(); + // The menu font text renders exactly the same and app-unit rects are + // equal, but the DOMRect conversion is rounded to 1/65536 CSS pixels. + // https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/dom/base/DOMRect.cpp#151-159 + // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1640441#c28 + const absTolerance = 1/65536 + fuzzyCompareLength(menuRect2.width, menuRect1.width/2, + "width of menu font <p> in px", absTolerance, expectFn); + fuzzyCompareLength(menuRect2.height, menuRect1.height/2, + "height of menu font <p> in px", absTolerance, expectFn); + } + + const defSize2 = elementFontSize(def); + is(defSize2, defSize1, "size of default font"); + { + const defRect2 = def.getBoundingClientRect(); + // Wider tolerance for hinting and snapping + const relTolerance = 1/12; + fuzzyCompareLength(defRect2.width, defRect1.width, + "width of default font <p> in px", + relTolerance*defRect1.width, ok); + fuzzyCompareLength(defRect2.height, defRect1.height, + "height of default font <p> in px", + relTolerance*defRect1.height, ok); + } +}); +</script> +</html> diff --git a/widget/tests/test_transferable_overflow.xhtml b/widget/tests/test_transferable_overflow.xhtml new file mode 100644 index 0000000000..dca9edcc61 --- /dev/null +++ b/widget/tests/test_transferable_overflow.xhtml @@ -0,0 +1,155 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<window title="nsTransferable with large string" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="RunTest();"> + <title>nsTransferable with large string</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + // This value is chosen such that the size of the memory for the string exceeds + // the kLargeDatasetSize threshold in nsTransferable.h (one million). + // Each character of a JS string is internally represented by two bytes, + // so the following string of length 500 001 uses 1 000 002 bytes. + const BIG_STRING = "x" + "BIGGY".repeat(100000); + + // Some value with a length that is exactly kLargeDatasetSize (1 000 000). + const SMALL_STRING = "small".repeat(100000); + + const nsTransferable = Components.Constructor("@mozilla.org/widget/transferable;1", "nsITransferable"); + const nsSupportsString = Components.Constructor("@mozilla.org/supports-string;1", "nsISupportsString"); + + function assignTextToTransferable(transferable, string) { + var Suppstr = nsSupportsString(); + Suppstr.data = string; + transferable.setTransferData("text/plain", Suppstr); + } + + function checkTransferableText(transferable, expectedString, description) { + var data = {}; + transferable.getTransferData("text/plain", data); + var actualValue = data.value.QueryInterface(Ci.nsISupportsString).data; + // Use ok + shortenString instead of is(...) to avoid dumping millions of characters in the output. + ok(actualValue === expectedString, description + ": text should match. " + + "Expected " + shortenString(expectedString) + ", got " + shortenString(actualValue)); + + function shortenString(str) { + return str && str.length > 30 ? str.slice(0, 10) + "..." + str.slice(-10) : String(str); + } + } + + function isFDCountingSupported() { + // On on-Windows we can count the number of file handles for the current process, + // while on Windows we need to count the number of files in ${TempD}\mozilla-temp-files\, + // which can be unreliable, especially because nsAnonymousTemporaryFile has documented + // that the deletion might not be immediate. + // + // To avoid intermittents, we only check the file descriptor counts on non-Windows. + // test_bug1123480.xhtml will do some basic testing for Windows. + const {AppConstants} = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + return AppConstants.platform !== 'win'; + } + + function getClipboardCacheFDCount() { + var dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath("/dev/fd"); + var count = 0; + for (var de = dir.directoryEntries; de.hasMoreElements(); ) { + var fdFile = de.nextFile; + var fileSize; + try { + fileSize = fdFile.fileSize; + } catch (e) { + // This can happen on macOS. + continue; + } + if (fileSize === BIG_STRING.length * 2 || + // We are not expecting files of this small size, + // but include them in the count anyway + // in case the files are unexpectedly created. + fileSize === SMALL_STRING.length * 2) { + // Assume that the file was created by us if the size matches. + ++count; + } + } + return count; + } + + function RunTest() { + const {PrivateBrowsingUtils} = ChromeUtils.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" + ); + + var win = window.browsingContext.topChromeWindow.open("about:blank", "_blank", "chrome, width=500, height=200"); + ok(win, "should open window"); + is(PrivateBrowsingUtils.isContentWindowPrivate(win), false, "used correct window context"); + + // ### Part 1 - Writing to the clipboard. + + var Loadctx = PrivateBrowsingUtils.privacyContextFromWindow(win); + var Transfer = nsTransferable(); + Transfer.init(Loadctx); + Transfer.addDataFlavor("text/plain"); + var initialFdCount = isFDCountingSupported() ? getClipboardCacheFDCount() : -1; + + assignTextToTransferable(Transfer, BIG_STRING); + checkTransferableText(Transfer, BIG_STRING, "transferable after assigning BIG_STRING"); + if (isFDCountingSupported()) { + is(getClipboardCacheFDCount(), initialFdCount + 1, "should use a file for BIG_STRING"); + } + + // Share the transferable with the system. + Services.clipboard.setData(Transfer, null, Services.clipboard.kGlobalClipboard); + + // Sanity check: Copying to the clipboard should not have altered the transferable. + checkTransferableText(Transfer, BIG_STRING, "transferable after copying to clipboard"); + if (isFDCountingSupported()) { + // We are only counting file descriptors for the current process, + // so even if the test were to be multi-process and the parent process creates another + // nsTransferable, then the count should still be the same. + is(getClipboardCacheFDCount(), initialFdCount + 1, "should still be using files for previously stored BIG_STRING"); + + // Re-establish baseline for the second part of the test below. + initialFdCount = getClipboardCacheFDCount(); + } + + // ### Part 2 - Reading from the clipboard. + + var Transfer2 = nsTransferable(); + Transfer2.init(Loadctx); + Transfer2.addDataFlavor("text/plain"); + + // Iniitalize with a small string, so we can see that mData -> mCacheFD works. + assignTextToTransferable(Transfer2, SMALL_STRING); + checkTransferableText(Transfer2, SMALL_STRING, "transferable after assigning SMALL_STRING"); + if (isFDCountingSupported()) { + is(getClipboardCacheFDCount(), initialFdCount, "should not use file to store SMALL_STRING."); + } + + // Check whether the clipboard data can be read, and simulatenously trigger mData -> mCacheFD. + Services.clipboard.getData(Transfer2, Services.clipboard.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext); + checkTransferableText(Transfer2, BIG_STRING, "transferable after retrieving from clipboard"); + if (isFDCountingSupported()) { + is(getClipboardCacheFDCount(), initialFdCount + 1, "should use a file for BIG_STRING (read from clipboard)."); + } + + // Store a small string, to exercise the code path from mCacheFD -> mData. + assignTextToTransferable(Transfer2, SMALL_STRING); + checkTransferableText(Transfer2, SMALL_STRING, "transferable after assigning SMALL_STRING"); + if (isFDCountingSupported()) { + is(getClipboardCacheFDCount(), initialFdCount, "should release the file after clearing the transferable."); + } + } + ]]> + </script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + This test checks whether a big string can be copied to the clipboard, and then retrieved in the same form. + On non-Windows, the test also checks whether the data of the transferable is really stored in a file. + </body> +</window> diff --git a/widget/tests/test_wheeltransaction.xhtml b/widget/tests/test_wheeltransaction.xhtml new file mode 100644 index 0000000000..23e855c39b --- /dev/null +++ b/widget/tests/test_wheeltransaction.xhtml @@ -0,0 +1,27 @@ +<?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 title="Wheel scroll transaction tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("window_wheeltransaction.xhtml", "_blank", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> +</window> diff --git a/widget/tests/unit/test_macsharingservice.js b/widget/tests/unit/test_macsharingservice.js new file mode 100644 index 0000000000..f6b0a8e3fc --- /dev/null +++ b/widget/tests/unit/test_macsharingservice.js @@ -0,0 +1,61 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +// Basic tests to verify that MacSharingService returns expected data + +function test_getSharingProviders() { + let sharingService = Cc["@mozilla.org/widget/macsharingservice;1"].getService( + Ci.nsIMacSharingService + ); + + // Ensure these URL's are accepted without error by the getSharingProviders() + // method. This does not test if the URL's are interpreted correctly by + // the platform implementation and does not test that the URL will be + // successfully shared to the target application if the shareURL method is + // used. It does indicate the Mac API's used to get the sharing providers + // successfully created a URL object for the URL provided and returned at + // least one provider. + let urls = [ + "http://example.org", + "http://example.org/#", + "http://example.org/dkl??", + "http://example.org/dkl?a=b;c=d#thisisaref", + "http://example.org/dkl?a=b;c=d#thisisaref#double", + "http://example.org/#/", + "http://example.org/#/#", + "http://example.org/#/#/", + // This test fails due to the '|' in the path which needs additional + // encoding before conversion to NSURL. See bug 1740565. + // "http://example.org/foo/bar/x|page.html#this_is_a_fragment", + "http://example.org/page.html#this_is_a_fragment", + "http://example.org/page.html#this_is_a_fragment#and_another", + "http://example.org/foo/bar;#foo", + "http://example.org/a file with spaces.html", + "https://chat.mozilla.org/#/room/#macdev:mozilla.org", + "https://chat.mozilla.org/#/room/%23macdev:mozilla.org", + ]; + + urls.forEach(url => testGetSharingProvidersForUrl(sharingService, url)); +} + +function testGetSharingProvidersForUrl(sharingService, url) { + let providers = sharingService.getSharingProviders(url); + Assert.greater(providers.length, 1, "There are providers returned"); + providers.forEach(provider => { + Assert.ok("name" in provider, "Provider has name"); + Assert.ok("menuItemTitle" in provider, "Provider has menuItemTitle"); + Assert.ok("image" in provider, "Provider has image"); + + Assert.notEqual( + provider.title, + "Mail", + "Known filtered provider not returned" + ); + }); +} + +function run_test() { + test_getSharingProviders(); +} diff --git a/widget/tests/unit/test_macwebapputils.js b/widget/tests/unit/test_macwebapputils.js new file mode 100644 index 0000000000..8967f8a593 --- /dev/null +++ b/widget/tests/unit/test_macwebapputils.js @@ -0,0 +1,34 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +// Basic tests to verify that MacWebAppUtils works + +function test_find_app() { + var mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"].createInstance( + Ci.nsIMacWebAppUtils + ); + let sig = "com.apple.TextEdit"; + + let path; + path = mwaUtils.pathForAppWithIdentifier(sig); + info("TextEdit path: " + path + "\n"); + Assert.notEqual(path, ""); +} + +function test_dont_find_fake_app() { + var mwaUtils = Cc["@mozilla.org/widget/mac-web-app-utils;1"].createInstance( + Ci.nsIMacWebAppUtils + ); + let sig = "calliope.penitentiary.dramamine"; + + let path; + path = mwaUtils.pathForAppWithIdentifier(sig); + Assert.equal(path, ""); +} + +function run_test() { + test_find_app(); + test_dont_find_fake_app(); +} diff --git a/widget/tests/unit/test_taskbar_legacyjumplistitems.js b/widget/tests/unit/test_taskbar_legacyjumplistitems.js new file mode 100644 index 0000000000..e0173fd29e --- /dev/null +++ b/widget/tests/unit/test_taskbar_legacyjumplistitems.js @@ -0,0 +1,229 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +// This tests taskbar jump list functionality available on win7 and up. + +function test_basics() { + var item = Cc["@mozilla.org/windows-legacyjumplistitem;1"].createInstance( + Ci.nsILegacyJumpListItem + ); + + var sep = Cc["@mozilla.org/windows-legacyjumplistseparator;1"].createInstance( + Ci.nsILegacyJumpListSeparator + ); + + var shortcut = Cc[ + "@mozilla.org/windows-legacyjumplistshortcut;1" + ].createInstance(Ci.nsILegacyJumpListShortcut); + + var link = Cc["@mozilla.org/windows-legacyjumplistlink;1"].createInstance( + Ci.nsILegacyJumpListLink + ); + + Assert.ok(!item.equals(sep)); + Assert.ok(!item.equals(shortcut)); + Assert.ok(!item.equals(link)); + + Assert.ok(!sep.equals(item)); + Assert.ok(!sep.equals(shortcut)); + Assert.ok(!sep.equals(link)); + + Assert.ok(!shortcut.equals(item)); + Assert.ok(!shortcut.equals(sep)); + Assert.ok(!shortcut.equals(link)); + + Assert.ok(!link.equals(item)); + Assert.ok(!link.equals(sep)); + Assert.ok(!link.equals(shortcut)); + + Assert.ok(item.equals(item)); + Assert.ok(sep.equals(sep)); + Assert.ok(link.equals(link)); + Assert.ok(shortcut.equals(shortcut)); +} + +function test_separator() { + // separators: + + var item = Cc[ + "@mozilla.org/windows-legacyjumplistseparator;1" + ].createInstance(Ci.nsILegacyJumpListSeparator); + + Assert.ok(item.type == Ci.nsILegacyJumpListItem.JUMPLIST_ITEM_SEPARATOR); +} + +function test_links() { + // links: + var link1 = Cc["@mozilla.org/windows-legacyjumplistlink;1"].createInstance( + Ci.nsILegacyJumpListLink + ); + var link2 = Cc["@mozilla.org/windows-legacyjumplistlink;1"].createInstance( + Ci.nsILegacyJumpListLink + ); + + var uri1 = Cc["@mozilla.org/network/simple-uri-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("http://www.test.com/") + .finalize(); + var uri2 = Cc["@mozilla.org/network/simple-uri-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("http://www.test.com/") + .finalize(); + + link1.uri = uri1; + link1.uriTitle = "Test"; + link2.uri = uri2; + link2.uriTitle = "Test"; + + Assert.ok(link1.equals(link2)); + + link2.uriTitle = "Testing"; + + Assert.ok(!link1.equals(link2)); + + link2.uriTitle = "Test"; + uri2 = uri2.mutate().setSpec("http://www.testing.com/").finalize(); + link2.uri = uri2; + + Assert.ok(!link1.equals(link2)); +} + +function test_shortcuts() { + // shortcuts: + var sc = Cc["@mozilla.org/windows-legacyjumplistshortcut;1"].createInstance( + Ci.nsILegacyJumpListShortcut + ); + + var handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + + handlerApp.name = "TestApp"; + handlerApp.detailedDescription = "TestApp detailed description."; + handlerApp.appendParameter("-test"); + + sc.iconIndex = 1; + Assert.equal(sc.iconIndex, 1); + + var faviconPageUri = Cc["@mozilla.org/network/simple-uri-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("http://www.123.com/") + .finalize(); + sc.faviconPageUri = faviconPageUri; + Assert.equal(sc.faviconPageUri, faviconPageUri); + + var notepad = Services.dirsvc.get("WinD", Ci.nsIFile); + notepad.append("notepad.exe"); + if (notepad.exists()) { + handlerApp.executable = notepad; + sc.app = handlerApp; + Assert.equal(sc.app.detailedDescription, "TestApp detailed description."); + Assert.equal(sc.app.name, "TestApp"); + Assert.ok(sc.app.parameterExists("-test")); + Assert.ok(!sc.app.parameterExists("-notset")); + } +} + +async function test_legacyjumplist() { + // Jump lists can't register links unless the application is the default + // protocol handler for the protocol of the link, so we skip off testing + // those in these tests. We'll init the jump list for the xpc shell harness, + // add a task item, and commit it. + + // not compiled in + if (Ci.nsIWinTaskbar == null) { + return; + } + + var taskbar = Cc["@mozilla.org/windows-taskbar;1"].getService( + Ci.nsIWinTaskbar + ); + + // Since we're only testing the general functionality of the JumpListBuilder + // et. al, we can just test the non-private browsing version. + // (The only difference between the two at this level is the App User Model ID.) + var builder = taskbar.createLegacyJumpListBuilder(false); + + Assert.notEqual(builder, null); + + // Win7 and up only + try { + var ver = parseFloat(Services.sysinfo.getProperty("version")); + if (ver < 6.1) { + Assert.ok(!builder.available); + return; + } + } catch (ex) {} + + Assert.ok(taskbar.available); + + builder.deleteActiveList(); + + var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + + var sc = Cc["@mozilla.org/windows-legacyjumplistshortcut;1"].createInstance( + Ci.nsILegacyJumpListShortcut + ); + + var handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + + handlerApp.name = "Notepad"; + handlerApp.detailedDescription = "Testing detailed description."; + + var notepad = Services.dirsvc.get("WinD", Ci.nsIFile); + notepad.append("notepad.exe"); + if (notepad.exists()) { + // To ensure "profile-before-change" will fire before + // "xpcom-shutdown-threads" + do_get_profile(); + + handlerApp.executable = notepad; + sc.app = handlerApp; + items.appendElement(sc); + + var removed = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + Assert.ok(builder.initListBuild(removed)); + Assert.ok(builder.addListToBuild(builder.JUMPLIST_CATEGORY_TASKS, items)); + Assert.ok(builder.addListToBuild(builder.JUMPLIST_CATEGORY_RECENT)); + Assert.ok(builder.addListToBuild(builder.JUMPLIST_CATEGORY_FREQUENT)); + let rv = new Promise(resolve => { + builder.commitListBuild(resolve); + }); + Assert.ok(await rv); + + builder.deleteActiveList(); + + Assert.ok(builder.initListBuild(removed)); + Assert.ok( + builder.addListToBuild( + builder.JUMPLIST_CATEGORY_CUSTOMLIST, + items, + "Custom List" + ) + ); + rv = new Promise(resolve => { + builder.commitListBuild(resolve); + }); + Assert.ok(await rv); + + builder.deleteActiveList(); + } +} + +function run_test() { + if (mozinfo.os != "win") { + return; + } + test_basics(); + test_separator(); + test_links(); + test_shortcuts(); + + run_next_test(); +} + +add_task(test_legacyjumplist); diff --git a/widget/tests/unit/xpcshell.toml b/widget/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..4e702ca356 --- /dev/null +++ b/widget/tests/unit/xpcshell.toml @@ -0,0 +1,11 @@ +[DEFAULT] +head = "" + +["test_macsharingservice.js"] +run-if = ["os == 'mac'"] + +["test_macwebapputils.js"] +run-if = ["os == 'mac'"] + +["test_taskbar_legacyjumplistitems.js"] +skip-if = ["os == 'win'"] diff --git a/widget/tests/window_bug429954.xhtml b/widget/tests/window_bug429954.xhtml new file mode 100644 index 0000000000..ca26d52621 --- /dev/null +++ b/widget/tests/window_bug429954.xhtml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Mozilla Bug 478536" + onload="start();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml" id="body"> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.browsingContext.topChromeWindow.opener.wrappedJSObject.SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function start() { + var oldWidth = window.outerWidth, oldHeight = window.outerHeight; + window.maximize(); + window.restore(); + is(window.outerWidth, oldWidth, "wrong window width after maximize+restore"); + is(window.outerHeight, oldHeight, "wrong window height after maximize+restore"); + window.arguments[0].SimpleTest.finish(); + window.close(); +} + + +]]> +</script> + +</window> diff --git a/widget/tests/window_bug478536.xhtml b/widget/tests/window_bug478536.xhtml new file mode 100644 index 0000000000..7318eb0bff --- /dev/null +++ b/widget/tests/window_bug478536.xhtml @@ -0,0 +1,211 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Mozilla Bug 478536" + width="600" height="600" + onload="onload();" + onunload="onunload();" + 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/paint_listener.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml" id="body"> +<style type="text/css"> + #view { + overflow: auto; + width: 100px; + height: 100px; + border: 1px solid; + margin: 0; + } +</style> +<pre id="view" onscroll="onScrollView(event);"> +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.arguments[0].SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage); +} + +var gBody = document.getElementById("body"); +var gView = document.getElementById("view"); + +/** + * Description: + * + * First, lock the wheel scrolling target to "view" at first step. + * Next, scroll back to top most of the "view" at second step. + * Finally, scroll back again at third step. This fails to scroll the "view", + * then, |onMouseScrollFailed| event should be fired. And at that time, we + * can remove the "view". So, in post processing of the event firere, the + * "view" should not be referred. + * + * For suppressing random test failure, all tests will be retried if we handle + * unexpected timeout event. + */ + +var gTests = [ + { scrollToForward: true, shouldScroll: true }, + { scrollToForward: false, shouldScroll: true }, + { scrollToForward: false, shouldScroll: false } +]; +var gCurrentTestIndex = -1; +var gIgnoreScrollEvent = true; + +var gPrefSvc = SpecialPowers.Services.prefs; +const kPrefSmoothScroll = "general.smoothScroll"; +const kPrefNameTimeout = "mousewheel.transaction.timeout"; +const kDefaultTimeout = gPrefSvc.getIntPref(kPrefNameTimeout); + +gPrefSvc.setBoolPref(kPrefSmoothScroll, false); + +var gTimeout = kDefaultTimeout; + +gBody.addEventListener("MozMouseScrollFailed", onMouseScrollFailed); +gBody.addEventListener("MozMouseScrollTransactionTimeout", + onTransactionTimeout); + +function setTimeoutPrefs(aTimeout) +{ + gPrefSvc.setIntPref(kPrefNameTimeout, aTimeout); + gTimeout = aTimeout; +} + +function resetTimeoutPrefs() +{ + if (gTimeout == kDefaultTimeout) + return; + setTimeoutPrefs(kDefaultTimeout); +} + +function growUpTimeoutPrefs() +{ + if (gTimeout != kDefaultTimeout) + return; + setTimeoutPrefs(5000); +} + +function onload() +{ + disableNonTestMouseEvents(true); + setTimeout(runNextTest, 0); +} + +function onunload() +{ + resetTimeoutPrefs(); + disableNonTestMouseEvents(false); + gPrefSvc.clearUserPref(kPrefSmoothScroll); + SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); + window.arguments[0].SimpleTest.finish(); +} + +function finish() +{ + window.close(); +} + +// testing code + +var gTimer; +function clearTimer() +{ + clearTimeout(gTimer); + gTimer = 0; +} + +function runNextTest() +{ + clearTimer(); + if (++gCurrentTestIndex >= gTests.length) { + ok(true, "didn't crash, succeeded"); + finish(); + return; + } + fireWheelScrollEvent(gTests[gCurrentTestIndex].scrollToForward); +} + +var gRetryCount = 5; +function retryAllTests() +{ + clearTimer(); + if (--gRetryCount >= 0) { + gView.scrollTop = 0; + gView.scrollLeft = 0; + gCurrentTestIndex = -1; + growUpTimeoutPrefs(); + ok(true, "WARNING: retry current test-list..."); + gTimer = setTimeout(runNextTest, 0); + } else { + ok(false, "Failed by unexpected timeout"); + finish(); + } +} + +function fireWheelScrollEvent(aForward) +{ + gIgnoreScrollEvent = false; + var event = { deltaY: aForward ? 4.0 : -4.0, + deltaMode: WheelEvent.DOM_DELTA_LINE }; + sendWheelAndPaint(gView, 5, 5, event, function() { + // No callback - we're just forcing the refresh driver to tick. + }, window); +} + +function onScrollView(aEvent) +{ + if (gIgnoreScrollEvent) + return; + gIgnoreScrollEvent = true; + clearTimer(); + ok(gTests[gCurrentTestIndex].shouldScroll, "The view is scrolled"); + gTimer = setTimeout(runNextTest, 0); +} + +function onMouseScrollFailed(aEvent) +{ + clearTimer(); + gIgnoreScrollEvent = true; + ok(!gTests[gCurrentTestIndex].shouldScroll, "The view is not scrolled"); + if (!gTests[gCurrentTestIndex].shouldScroll) + gBody.removeChild(gView); + runNextTest(); +} + +function onTransactionTimeout(aEvent) +{ + if (!gTimer) + return; + gIgnoreScrollEvent = true; + retryAllTests(); +} + +]]> +</script> + +</window> diff --git a/widget/tests/window_bug522217.xhtml b/widget/tests/window_bug522217.xhtml new file mode 100644 index 0000000000..80eb4b6e5a --- /dev/null +++ b/widget/tests/window_bug522217.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Mozilla Bug 522217" + onload="start();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml" id="body"> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.arguments[0].SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function executeSoon() { + return new Promise(resolve => { + window.arguments[0].SimpleTest.executeSoon(resolve); + }); +} + +function waitForEvent(obj, name) { + return new Promise(resolve => { + obj.addEventListener(name, resolve, { once: true }); + }); +} + +async function start() { + await waitForEvent(window, "focus"); + var oldOuterWidth = window.outerWidth, oldOuterHeight = window.outerHeight; + var oldInnerWidth = window.innerWidth, oldInnerHeight = window.innerHeight; + document.documentElement.setAttribute("chromemargin", "0,0,0,0"); + + await executeSoon(); + is(window.outerWidth, oldOuterWidth, "chromemargin shouldn't change the window's outerWidth"); + is(window.outerHeight, oldOuterHeight, "chromemargin shouldn't change the window's outerHeight"); + is(window.innerWidth, oldOuterWidth, "if chromemargin is set, innerWidth and outerWidth should be the same"); + is(window.innerHeight, oldOuterHeight, "if chromemargin is set, innerHeight and outerHeight should be the same"); + + // Wait for going full screen and back. + let sizemodeChange = waitForEvent(window, "sizemodechange"); + window.fullScreen = true; + await sizemodeChange; + sizemodeChange = waitForEvent(window, "sizemodechange"); + window.fullScreen = false; + await sizemodeChange; + is(window.outerWidth, oldOuterWidth, "wrong outerWidth after fullscreen mode"); + is(window.outerHeight, oldOuterHeight, "wrong outerHeight after fullscreen mode"); + is(window.innerWidth, oldOuterWidth, "wrong innerWidth after fullscreen mode"); + is(window.innerHeight, oldOuterHeight, "wrong innerHeight after fullscreen mode"); + document.documentElement.removeAttribute("chromemargin"); + + await executeSoon(); + is(window.outerWidth, oldOuterWidth, "wrong outerWidth after removing chromemargin"); + is(window.outerHeight, oldOuterHeight, "wrong outerHeight after removing chromemargin"); + is(window.innerWidth, oldInnerWidth, "wrong innerWidth after removing chromemargin"); + is(window.innerHeight, oldInnerHeight, "wrong innerHeight after removing chromemargin"); + window.arguments[0].SimpleTest.finish(); + window.close(); +} + + +]]> +</script> + +</window> diff --git a/widget/tests/window_bug538242.xhtml b/widget/tests/window_bug538242.xhtml new file mode 100644 index 0000000000..fb878b1383 --- /dev/null +++ b/widget/tests/window_bug538242.xhtml @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<window title="Window for Test for Mozilla Bug 538242" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/> diff --git a/widget/tests/window_bug593307_centerscreen.xhtml b/widget/tests/window_bug593307_centerscreen.xhtml new file mode 100644 index 0000000000..dd73e42f84 --- /dev/null +++ b/widget/tests/window_bug593307_centerscreen.xhtml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Mozilla Bug 593307" + width="100" height="100" + onload="onload();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml" id="body"> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function onload() +{ + var SimpleTest = window.opener.SimpleTest; + SimpleTest.ok(window.screenX >= 0, "centerscreen window should not start offscreen (X coordinate): " + window.screenX); + SimpleTest.ok(window.screenY >= 0, "centerscreen window should not start offscreen (Y coordinate): " + window.screenY); + window.opener.finished(); +} +]]> +</script> + +</window> diff --git a/widget/tests/window_bug593307_offscreen.xhtml b/widget/tests/window_bug593307_offscreen.xhtml new file mode 100644 index 0000000000..10ab701a55 --- /dev/null +++ b/widget/tests/window_bug593307_offscreen.xhtml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Mozilla Bug 593307" + width="100" height="100" + onload="onLoad();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml" id="body"> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var centerscreen = null; +var SimpleTest = window.arguments[0]; +var finish = window.arguments[1]; + +function onLoad() +{ + setTimeout(() => { + centerscreen = window.openDialog('window_bug593307_centerscreen.xhtml','', 'chrome,centerscreen,dependent,dialog=no'); + }, 0); +} + +function finished() { + centerscreen.close(); + finish(); +} + +]]> +</script> + +</window> diff --git a/widget/tests/window_composition_text_querycontent.xhtml b/widget/tests/window_composition_text_querycontent.xhtml new file mode 100644 index 0000000000..db3b10ea30 --- /dev/null +++ b/widget/tests/window_composition_text_querycontent.xhtml @@ -0,0 +1,10923 @@ +<?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 title="Testing composition, text and query content events" + 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/WindowSnapshot.js" /> + + <panel id="panel" hidden="true" orient="vertical"> + <vbox id="vbox"> + <html:textarea id="textbox" cols="20" rows="4" style="font-size: 36px;"/> + </vbox> + </panel> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="display"> +<div id="div" style="margin: 0; padding: 0; font-size: 36px;">Here is a text frame.</div> +<textarea style="margin: 0; font-family: -moz-fixed;" id="textarea" cols="20" rows="4"></textarea><br/> +<iframe id="iframe" width="300" height="150" + src="data:text/html,<textarea id='textarea' cols='20' rows='4'></textarea>"></iframe><br/> +<iframe id="iframe2" width="300" height="150" + src="data:text/html,<body onload='document.designMode=%22on%22'>body content</body>"></iframe><br/> +<iframe id="iframe3" width="300" height="150" + src="data:text/html,<body onload='document.designMode=%22on%22'>body content</body>"></iframe><br/> +<iframe id="iframe4" width="300" height="150" + src="data:text/html,<div contenteditable id='contenteditable'></div>"></iframe><br/> +<!-- + NOTE: the width for the next two iframes is chosen to be small enough to make + the Show Password button (for type=password) be outside the viewport so that + it doesn't affect the rendering compared to the type=text control. + But still large enough to comfortably fit the input values we test. +--> +<iframe id="iframe5" style="width:10ch" height="50" src="data:text/html,<input id='input'>"></iframe> +<iframe id="iframe6" style="width:10ch" height="50" src="data:text/html,<input id='password' type='password'>"></iframe><br/> +<iframe id="iframe7" width="300" height="150" + src="data:text/html,<span contenteditable id='contenteditable'></span>"></iframe><br/> +<input id="input" type="text"/><br/> +<input id="password" type="password"/><br/> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.arguments[0].SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function isfuzzy(aLeft, aRight, aEpsilon, aMessage) { + window.arguments[0].SimpleTest.isfuzzy(aLeft, aRight, aEpsilon, aMessage); +} + +function todo(aCondition, aMessage) +{ + window.arguments[0].SimpleTest.todo(aCondition, aMessage); +} + +function todo_is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.todo_is(aLeft, aRight, aMessage); +} + +function todo_isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.todo_isnot(aLeft, aRight, aMessage); +} + +function isSimilarTo(aLeft, aRight, aAllowedDifference, aMessage) +{ + if (Math.abs(aLeft - aRight) <= aAllowedDifference) { + ok(true, aMessage); + } else { + ok(false, aMessage + ", got=" + aLeft + ", expected=" + (aRight - aAllowedDifference) + "~" + (aRight + aAllowedDifference)); + } +} + +function isGreaterThan(aLeft, aRight, aMessage) +{ + ok(aLeft > aRight, aMessage + ", got=" + aLeft + ", expected minimum value=" + aRight); +} + +/** + * synthesizeSimpleCompositionChange synthesizes a composition which has only + * one clause and put caret end of it. + * + * @param aComposition string or object. If string, it's treated as + * composition string whose attribute is + * COMPOSITION_ATTR_RAW_CLAUSE. + * If object, it must have .string whose type is "string". + * Additionally, .attr can be specified if you'd like to + * use the other attribute instead of + * COMPOSITION_ATTR_RAW_CLAUSE. + */ +function synthesizeSimpleCompositionChange(aComposition, aWindow, aCallback) { + const comp = (() => { + if (typeof aComposition == "string") { + return { string: aComposition, attr: COMPOSITION_ATTR_RAW_CLAUSE }; + } + return { + string: aComposition.string, + attr: aComposition.attr === undefined + ? COMPOSITION_ATTR_RAW_CLAUSE + : aComposition.attr + }; + })(); + synthesizeCompositionChange( + { + composition: { + string: comp.string, + clauses: [ + { length: comp.string.length, attr: comp.attr }, + ], + }, + caret: { start: comp.string.length, length: 0 }, + }, + aWindow, + aCallback + ); +} + + +var div = document.getElementById("div"); +var textarea = document.getElementById("textarea"); +var panel = document.getElementById("panel"); +var textbox = document.getElementById("textbox"); +var iframe = document.getElementById("iframe"); +var iframe2 = document.getElementById("iframe2"); +var iframe3 = document.getElementById("iframe3"); +var contenteditable; +var windowOfContenteditable; +var contenteditableBySpan; +var windowOfContenteditableBySpan; +var input = document.getElementById("input"); +var password = document.getElementById("password"); +var textareaInFrame; + +const nsITextInputProcessorCallback = Ci.nsITextInputProcessorCallback; +const nsIInterfaceRequestor = Ci.nsIInterfaceRequestor; +const nsIWebNavigation = Ci.nsIWebNavigation; +const nsIDocShell = Ci.nsIDocShell; +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +function waitForTick() { + return new Promise(resolve => { SimpleTest.executeSoon(resolve); }); +} + +async function waitForEventLoops(aTimes) +{ + for (let i = 1; i < aTimes; i++) { + await waitForTick(); + } + await new Promise(resolve => { setTimeout(resolve, 20); }); +} + +function getEditor(aNode) +{ + return aNode.editor; +} + +function getHTMLEditorIMESupport(aWindow) +{ + return aWindow.docShell.editor; +} + +const kIsWin = (navigator.platform.indexOf("Win") == 0); +const kIsMac = (navigator.platform.indexOf("Mac") == 0); + +const kLFLen = (kIsWin && false) ? 2 : 1; +const kLF = (kIsWin && false) ? "\r\n" : "\n"; + +function checkQueryContentResult(aResult, aMessage) +{ + ok(aResult, aMessage + ": the result is null"); + if (!aResult) { + return false; + } + ok(aResult.succeeded, aMessage + ": the query content failed"); + return aResult.succeeded; +} + +function checkContent(aExpectedText, aMessage, aID) +{ + if (!aID) { + aID = ""; + } + let textContent = synthesizeQueryTextContent(0, 100); + if (!checkQueryContentResult(textContent, aMessage + + ": synthesizeQueryTextContent " + aID)) { + return false; + } + is(textContent.text, aExpectedText, + aMessage + ": composition string is wrong " + aID); + return textContent.text == aExpectedText; +} + +function checkContentRelativeToSelection(aRelativeOffset, aLength, aExpectedOffset, aExpectedText, aMessage, aID) +{ + if (!aID) { + aID = ""; + } + aMessage += " (aRelativeOffset=" + aRelativeOffset + "): " + let textContent = synthesizeQueryTextContent(aRelativeOffset, aLength, true); + if (!checkQueryContentResult(textContent, aMessage + + "synthesizeQueryTextContent " + aID)) { + return false; + } + is(textContent.offset, aExpectedOffset, + aMessage + "offset is wrong " + aID); + is(textContent.text, aExpectedText, + aMessage + "text is wrong " + aID); + return textContent.offset == aExpectedOffset && + textContent.text == aExpectedText; +} + +function checkSelection(aExpectedOffset, aExpectedText, aMessage, aID) +{ + if (!aID) { + aID = ""; + } + let selectedText = synthesizeQuerySelectedText(); + if (!checkQueryContentResult(selectedText, aMessage + + ": synthesizeQuerySelectedText " + aID)) { + return false; + } + if (aExpectedOffset === null) { + is( + selectedText.notFound, + true, + `${aMessage}: selection should not be found ${aID}` + ); + return selectedText.notFound; + } + + is( + selectedText.notFound, + false, + `${aMessage}: selection should be found ${aID}` + ); + if (selectedText.notFound) { + return false; + } + is( + selectedText.offset, + aExpectedOffset, + `${aMessage}: selection offset should be ${aExpectedOffset} ${aID}` + ); + is( + selectedText.text, + aExpectedText, + `${aMessage}: selected text should be "${aExpectedText}" ${aID}` + ); + return selectedText.offset == aExpectedOffset && + selectedText.text == aExpectedText; +} + +function checkIMESelection( + aSelectionType, + aExpectedFound, + aExpectedOffset, + aExpectedText, + aMessage, + aID, + aToDo = {} +) { + if (!aID) { + aID = ""; + } + aMessage += " (" + aSelectionType + ")"; + let { + notFound = is, + offset = is, + text = is, + } = aToDo; + let selectionType = 0; + switch (aSelectionType) { + case "RawClause": + selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT; + break; + case "SelectedRawClause": + selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT; + break; + case "ConvertedClause": + selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT; + break; + case "SelectedClause": + selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT; + break; + default: + ok(false, aMessage + ": invalid selection type, " + aSelectionType); + } + isnot(selectionType, 0, aMessage + ": wrong value"); + let selectedText = synthesizeQuerySelectedText(selectionType); + if (!checkQueryContentResult(selectedText, aMessage + + ": synthesizeQuerySelectedText " + aID)) { + return false; + } + notFound( + selectedText.notFound, + !aExpectedFound, + `${aMessage}: selection should ${ + aExpectedFound ? "" : "not" + } be found ${aID}`); + if (selectedText.notFound) { + return selectedText.notFound == !aExpectedFound; + } + + offset( + selectedText.offset, + aExpectedOffset, + `${aMessage}: selection offset is wrong ${aID}` + ); + text( + selectedText.text, + aExpectedText, + `${aMessage}: selected text is wrong ${aID}` + ); + return selectedText.offset == aExpectedOffset && + selectedText.text == aExpectedText; +} + +function checkRect(aRect, aExpectedRect, aMessage) +{ + is(aRect.left, aExpectedRect.left, aMessage + ": left is wrong"); + is(aRect.top, aExpectedRect.top, aMessage + " top is wrong"); + is(aRect.width, aExpectedRect.width, aMessage + ": width is wrong"); + is(aRect.height, aExpectedRect.height, aMessage + ": height is wrong"); + return aRect.left == aExpectedRect.left && + aRect.top == aExpectedRect.top && + aRect.width == aExpectedRect.width && + aRect.height == aExpectedRect.height; +} + +function checkRectFuzzy(aRect, aExpectedRect, aEpsilon, aMessage) { + isfuzzy(aRect.left, aExpectedRect.left, aEpsilon.left, aMessage + ": left is wrong"); + isfuzzy(aRect.top, aExpectedRect.top, aEpsilon.top, aMessage + " top is wrong"); + isfuzzy(aRect.width, aExpectedRect.width, aEpsilon.width, aMessage + ": width is wrong"); + isfuzzy(aRect.height, aExpectedRect.height, aEpsilon.height, aMessage + ": height is wrong"); + return (aRect.left >= aExpectedRect.left - aEpsilon.left && + aRect.left <= aExpectedRect.left + aEpsilon.left) && + (aRect.top >= aExpectedRect.top - aEpsilon.top && + aRect.top <= aExpectedRect.top + aEpsilon.top) && + (aRect.width >= aExpectedRect.width - aEpsilon.width && + aRect.width <= aExpectedRect.width + aEpsilon.width) && + (aRect.height >= aExpectedRect.height - aEpsilon.height && + aRect.height <= aExpectedRect.height + aEpsilon.height); +} + +function getRectArray(aQueryTextRectArrayResult) { + let rects = []; + for (let i = 0; ; i++) { + let rect = { left: {}, top: {}, width: {}, height: {} }; + try { + aQueryTextRectArrayResult.getCharacterRect(i, rect.left, rect.top, rect.width, rect.height); + } catch (e) { + break; + } + rects.push({ + left: rect.left.value, + top: rect.top.value, + width: rect.width.value, + height: rect.height.value, + }); + } + return rects; +} + +function checkRectArray(aQueryTextRectArrayResult, aExpectedTextRectArray, aMessage) +{ + for (let i = 1; i < aExpectedTextRectArray.length; ++i) { + let rect = { left: {}, top: {}, width: {}, height: {} }; + try { + aQueryTextRectArrayResult.getCharacterRect(i, rect.left, rect.top, rect.width, rect.height); + } catch (e) { + ok(false, aMessage + ": failed to retrieve " + i + "th rect (" + e + ")"); + return false; + } + function toRect(aRect) + { + return { left: aRect.left.value, top: aRect.top.value, width: aRect.width.value, height: aRect.height.value }; + } + if (!checkRect(toRect(rect), aExpectedTextRectArray[i], aMessage + " " + i + "th rect")) { + return false; + } + } + return true; +} + +function checkRectContainsRect(aRect, aContainer, aMessage) +{ + let container = { left: Math.ceil(aContainer.left), + top: Math.ceil(aContainer.top), + width: Math.floor(aContainer.width), + height: Math.floor(aContainer.height) }; + + let ret = container.left <= aRect.left && + container.top <= aRect.top && + container.left + container.width >= aRect.left + aRect.width && + container.top + container.height >= aRect.top + aRect.height; + ret = ret && aMessage; + ok(ret, aMessage + " container={ left=" + container.left + ", top=" + + container.top + ", width=" + container.width + ", height=" + + container.height + " } rect={ left=" + aRect.left + ", top=" + aRect.top + + ", width=" + aRect.width + ", height=" + aRect.height + " }"); + return ret; +} + +// eslint-disable-next-line complexity +function runUndoRedoTest() +{ + textarea.value = ""; + textarea.focus(); + + // input raw characters + synthesizeCompositionChange( + { "composition": + { "string": "\u306D", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "," }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u306D\u3053", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "b" }, + }); + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u732B", + "clauses": + [ + { "length": 1, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: " " }, + }); + + // commit + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + // input raw characters + synthesizeCompositionChange( + { "composition": + { "string": "\u307E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "j" }, + }); + + // cancel the composition + synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Escape" } }); + + // input raw characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3080", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "]" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3080\u3059", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "r" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3080\u3059\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "/" }, + }); + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u5A18", + "clauses": + [ + { "length": 1, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: " " }, + }); + + // commit + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + sendString(" meant"); + synthesizeKey("KEY_Backspace"); + synthesizeKey("s \"cat-girl\". She is a "); + + // input raw characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3088", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "9" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3088\u3046", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "4" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3088\u3046\u304b", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "t" }, + }); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3088\u3046\u304b\u3044", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 }, + "key": { key: "e" }, + }); + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u5996\u602a", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: " " }, + }); + + // commit + synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } }); + + synthesizeKey("KEY_Backspace", {repeat: 12}); + + let i = 0; + if (!checkContent("\u732B\u5A18 means \"cat-girl\".", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(20, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a \u5996\u602A", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(32, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a ", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(30, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18 mean", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(7, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18 meant", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(8, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B\u5A18", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(2, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("\u732B", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(1, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u732B", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(1, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(0, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true}); + + if (!checkContent("", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(0, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(1, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u732B", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(1, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(2, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 meant", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(8, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 mean", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(7, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a ", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(30, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a \u5996\u602A", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(32, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\".", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(20, "", "runUndoRedoTest", "#" + i)) { + return; + } + + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + if (!checkContent("\u732B\u5A18 means \"cat-girl\".", + "runUndoRedoTest", "#" + ++i) || + !checkSelection(20, "", "runUndoRedoTest", "#" + i)) { + // eslint-disable-next-line no-useless-return + return; + } +} + +function checkInputEvent(aEvent, aIsComposing, aInputType, aData, aTargetRanges, aDescription) { + if (aEvent.type !== "input" && aEvent.type !== "beforeinput") { + throw new Error(`${aDescription}: "${aEvent.type}" is not InputEvent`); + } + ok(InputEvent.isInstance(aEvent), `"${aEvent.type}" event should be dispatched with InputEvent interface: ${aDescription}`); + let cancelable = aEvent.type === "beforeinput" && + aInputType !== "insertCompositionText" && + aInputType !== "deleteCompositionText"; + is(aEvent.cancelable, cancelable, `"${aEvent.type}" event should ${cancelable ? "be" : "be never"} cancelable: ${aDescription}`); + is(aEvent.bubbles, true, `"${aEvent.type}" event should always bubble: ${aDescription}`); + is(aEvent.isComposing, aIsComposing, `isComposing of "${aEvent.type}" event should be ${aIsComposing}: ${aDescription}`); + is(aEvent.inputType, aInputType, `inputType of "${aEvent.type}" event should be "${aInputType}": ${aDescription}`); + is(aEvent.data, aData, `data of "${aEvent.type}" event should be ${aData}: ${aDescription}`); + is(aEvent.dataTransfer, null, `dataTransfer of "${aEvent.type}" event should be null: ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aTargetRanges.length === 0) { + is(targetRanges.length, 0, + `getTargetRange() of "${aEvent.type}" event should return empty array: ${aDescription}`); + } else { + is(targetRanges.length, aTargetRanges.length, + `getTargetRange() of "${aEvent.type}" event should return static range array: ${aDescription}`); + if (targetRanges.length == aTargetRanges.length) { + for (let i = 0; i < targetRanges.length; i++) { + is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + } + } + } +} + +function runCompositionCommitAsIsTest() +{ + textarea.focus(); + + let result = []; + function clearResult() + { + result = []; + } + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + textarea.addEventListener("beforeinput", handler, true); + textarea.addEventListener("input", handler, true); + textarea.addEventListener("text", handler, true); + + // compositioncommitasis with composing string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #1"); + + clearResult(); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } }); + + is(result.length, 4, + "runCompositionCommitAsIsTest: 4 events should be fired after dispatching compositioncommitasis #1"); + is(result[0].type, "text", + "runCompositionCommitAsIsTest: text should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1"); + is(result[1].type, "beforeinput", + "runCompositionCommitAsIsTest: beforeinput should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1"); + checkInputEvent(result[1], true, "insertCompositionText", "\u3042", [], + "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1"); + is(result[2].type, "compositionend", + "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #1"); + is(result[3].type, "input", + "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #1"); + checkInputEvent(result[3], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1"); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #1"); + + // compositioncommitasis with committed string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #2"); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "KEY_Enter", type: "keydown" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2"); + + clearResult(); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter", type: "keyup" } }); + + is(result.length, 2, + "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #2"); + // XXX Do we need a "beforeinput" event here? Not sure. + is(result[0].type, "compositionend", + "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #2"); + is(result[1].type, "input", + "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #2"); + checkInputEvent(result[1], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #2"); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2"); + + // compositioncommitasis with committed string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #3"); + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "KEY_Escape", type: "keydown" }, + }); + is(textarea.value, "", "runCompositionCommitAsIsTest: textarea has non-empty composition string #3"); + + clearResult(); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Escape", type: "keyup" } }); + + is(result.length, 2, + "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #3"); + // XXX Do we need a "beforeinput" event here? Not sure. + is(result[0].type, "compositionend", + "runCompositionCommitAsIsTest: compositionend shouldn't be fired after dispatching compositioncommitasis #3"); + is(result[1].type, "input", + "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #3"); + checkInputEvent(result[1], false, "insertCompositionText", "", [], + "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #3"); + is(textarea.value, "", "runCompositionCommitAsIsTest: textarea doesn't have committed string #3"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + textarea.removeEventListener("beforeinput", handler, true); + textarea.removeEventListener("input", handler, true); + textarea.removeEventListener("text", handler, true); +} + +function runCompositionCommitTest() +{ + textarea.focus(); + + let result = []; + function clearResult() + { + result = []; + } + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + textarea.addEventListener("beforeinput", handler, true); + textarea.addEventListener("input", handler, true); + textarea.addEventListener("text", handler, true); + + // compositioncommit with different composing string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a", type: "keydown" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #1"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "a", type: "keyup" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #1"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #1"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #1"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #1"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #1"); + checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #1"); + is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #1"); + + // compositioncommit with different committed string when there is already committed string + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #2"); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "KEY_Enter", type: "keydown" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have committed string #2"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #2"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #2"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #2"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #2"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #2"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #2"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #2"); + checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #2"); + is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #2"); + + // compositioncommit with empty composition string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #3"); + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "KEY_Enter", type: "keydown" }, + }); + is(textarea.value, "", "runCompositionCommitTest: textarea has non-empty composition string #3"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #3"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #3"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #3"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #3"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #3"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #3"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #3"); + checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [], + "runCompositionCommitTest: after dispatching compositioncommit #3"); + is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #3"); + + // inserting empty string with simple composition. + textarea.value = "abc"; + textarea.setSelectionRange(3, 3); + synthesizeComposition({ type: "compositionstart" }); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "" }); + + is(result.length, 4, + "runCompositionCommitTest: 4 events should be fired when inserting empty string with composition"); + is(result[0].type, "text", + "runCompositionCommitTest: text should be fired when inserting empty string with composition"); + is(result[1].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired when inserting empty string with composition"); + checkInputEvent(result[1], true, "insertCompositionText", "", [], + "runCompositionCommitTest: when inserting empty string with composition"); + is(result[2].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired when inserting empty string with composition"); + is(result[3].type, "input", + "runCompositionCommitTest: input should be fired when inserting empty string with composition"); + checkInputEvent(result[3], false, "insertCompositionText", "", [], + "runCompositionCommitTest: when inserting empty string with composition"); + is(textarea.value, "abc", + "runCompositionCommitTest: textarea should keep original value when inserting empty string with composition"); + + // replacing selection with empty string with simple composition. + textarea.value = "abc"; + textarea.setSelectionRange(0, 3); + synthesizeComposition({ type: "compositionstart" }); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "" }); + + is(result.length, 4, + "runCompositionCommitTest: 4 events should be fired when replacing with empty string with composition"); + is(result[0].type, "text", + "runCompositionCommitTest: text should be fired when replacing with empty string with composition"); + is(result[1].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired when replacing with empty string with composition"); + checkInputEvent(result[1], true, "insertCompositionText", "", [], + "runCompositionCommitTest: when replacing with empty string with composition"); + is(result[2].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired when replacing with empty string with composition"); + is(result[3].type, "input", + "runCompositionCommitTest: input should be fired when replacing with empty string with composition"); + checkInputEvent(result[3], false, "insertCompositionText", "", [], + "runCompositionCommitTest: when replacing with empty string with composition"); + is(textarea.value, "", + "runCompositionCommitTest: textarea should become empty when replacing selection with empty string with composition"); + + // replacing selection with same string with simple composition. + textarea.value = "abc"; + textarea.setSelectionRange(0, 3); + synthesizeComposition({ type: "compositionstart" }); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "abc" }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired when replacing selection with same string with composition"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired when replacing selection with same string with composition"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired when replacing selection with same string with composition"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired when replacing selection with same string with composition"); + checkInputEvent(result[2], true, "insertCompositionText", "abc", [], + "runCompositionCommitTest: when replacing selection with same string with composition"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired when replacing selection with same string with composition"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired when replacing selection with same string with composition"); + checkInputEvent(result[4], false, "insertCompositionText", "abc", [], + "runCompositionCommitTest: when replacing selection with same string with composition"); + is(textarea.value, "abc", + "runCompositionCommitTest: textarea should keep same value when replacing selection with same string with composition"); + + // compositioncommit with non-empty composition string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #4"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Enter" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #4"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #4"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #4"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #4"); + checkInputEvent(result[2], true, "insertCompositionText", "", [], + "runCompositionCommitTest: after dispatching compositioncommit #4"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #4"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #4"); + checkInputEvent(result[4], false, "insertCompositionText", "", [], + "runCompositionCommitTest: after dispatching compositioncommit #4"); + is(textarea.value, "", "runCompositionCommitTest: textarea should be empty #4"); + + // compositioncommit immediately without compositionstart + textarea.value = ""; + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "a" } }); + + is(result.length, 5, + "runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #5"); + is(result[0].type, "compositionupdate", + "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #5"); + is(result[1].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #5"); + is(result[2].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #5"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #5"); + is(result[3].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #5"); + is(result[4].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #5"); + checkInputEvent(result[4], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #5"); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should be empty #5"); + + // compositioncommit with same composition string. + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #5"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter" } }); + + is(result.length, 4, + "runCompositionCommitTest: 4 events should be fired after dispatching compositioncommit #6"); + is(result[0].type, "text", + "runCompositionCommitTest: text should be fired after dispatching compositioncommit #6"); + is(result[1].type, "beforeinput", + "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #6"); + checkInputEvent(result[1], true, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #6"); + is(result[2].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #6"); + is(result[3].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #6"); + checkInputEvent(result[3], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #6"); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6"); + + // compositioncommit with same composition string when there is committed string + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6"); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "KEY_Enter", type: "keydown" }, + }); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter", type: "keyup" } }); + + is(result.length, 2, + "runCompositionCommitTest: 2 events should be fired after dispatching compositioncommit #7"); + // XXX Do we need a "beforeinput" event here? Not sure. + is(result[0].type, "compositionend", + "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #7"); + is(result[1].type, "input", + "runCompositionCommitTest: input should be fired after dispatching compositioncommit #7"); + checkInputEvent(result[1], false, "insertCompositionText", "\u3042", [], + "runCompositionCommitTest: after dispatching compositioncommit #7"); + is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + textarea.removeEventListener("beforeinput", handler, true); + textarea.removeEventListener("input", handler, true); + textarea.removeEventListener("text", handler, true); +} + +// eslint-disable-next-line complexity +async function runCompositionTest() +{ + textarea.value = ""; + textarea.focus(); + let caretRects = []; + + let caretRect = synthesizeQueryCaretRect(0); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #0")) { + return; + } + caretRects[0] = caretRect; + + // input first character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "o" }, + }); + + if (!checkContent("\u3089", "runCompositionTest", "#1-1") || + !checkSelection(1, "", "runCompositionTest", "#1-1")) { + return; + } + + caretRect = synthesizeQueryCaretRect(1); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-1")) { + return; + } + caretRects[1] = caretRect; + + // input second character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "\\", code: "IntlYen", keyCode: KeyboardEvent.DOM_VK_BACKSLASH }, + }); + + if (!checkContent("\u3089\u30FC", "runCompositionTest", "#1-2") || + !checkSelection(2, "", "runCompositionTest", "#1-2")) { + return; + } + + caretRect = synthesizeQueryCaretRect(2); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-2")) { + return; + } + caretRects[2] = caretRect; + + isnot(caretRects[2].left, caretRects[1].left, + "runCompositionTest: caret isn't moved (#1-2)"); + is(caretRects[2].top, caretRects[1].top, + "runCompositionTest: caret is moved to another line (#1-2)"); + is(caretRects[2].width, caretRects[1].width, + "runCompositionTest: caret width is wrong (#1-2)"); + is(caretRects[2].height, caretRects[1].height, + "runCompositionTest: caret width is wrong (#1-2)"); + + // input third character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "/" }, + }); + + if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3") || + !checkSelection(3, "", "runCompositionTest", "#1-3")) { + return; + } + + caretRect = synthesizeQueryCaretRect(3); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-3")) { + return; + } + caretRects[3] = caretRect; + + isnot(caretRects[3].left, caretRects[2].left, + "runCompositionTest: caret isn't moved (#1-3)"); + is(caretRects[3].top, caretRects[2].top, + "runCompositionTest: caret is moved to another line (#1-3)"); + is(caretRects[3].width, caretRects[2].width, + "runCompositionTest: caret width is wrong (#1-3)"); + is(caretRects[3].height, caretRects[2].height, + "runCompositionTest: caret height is wrong (#1-3)"); + + // moves the caret left + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "KEY_ArrowLeft" }, + }); + + if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3-1") || + !checkSelection(2, "", "runCompositionTest", "#1-3-1")) { + return; + } + + + caretRect = synthesizeQueryCaretRect(2); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-3-1")) { + return; + } + + is(caretRect.left, caretRects[2].left, + "runCompositionTest: caret rects are different (#1-3-1, left)"); + is(caretRect.top, caretRects[2].top, + "runCompositionTest: caret rects are different (#1-3-1, top)"); + // by bug 335359, the caret width depends on the right side's character. + is(caretRect.width, caretRects[2].width + Math.round(window.devicePixelRatio), + "runCompositionTest: caret rects are different (#1-3-1, width)"); + is(caretRect.height, caretRects[2].height, + "runCompositionTest: caret rects are different (#1-3-1, height)"); + + // moves the caret left + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "KEY_ArrowLeft" }, + }); + + if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3-2") || + !checkSelection(1, "", "runCompositionTest", "#1-3-2")) { + return; + } + + + caretRect = synthesizeQueryCaretRect(1); + if (!checkQueryContentResult(caretRect, + "runCompositionTest: synthesizeQueryCaretRect #1-3-2")) { + return; + } + + is(caretRect.left, caretRects[1].left, + "runCompositionTest: caret rects are different (#1-3-2, left)"); + is(caretRect.top, caretRects[1].top, + "runCompositionTest: caret rects are different (#1-3-2, top)"); + // by bug 335359, the caret width depends on the right side's character. + is(caretRect.width, caretRects[1].width + Math.round(window.devicePixelRatio), + "runCompositionTest: caret rects are different (#1-3-2, width)"); + is(caretRect.height, caretRects[1].height, + "runCompositionTest: caret rects are different (#1-3-2, height)"); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 }, + "key": { key: "y" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093", "runCompositionTest", "#1-4") || + !checkSelection(4, "", "runCompositionTest", "#1-4")) { + return; + } + + + // backspace + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "KEY_Backspace" }, + }); + + if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-5") || + !checkSelection(3, "", "runCompositionTest", "#1-5")) { + return; + } + + // re-input + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 }, + "key": { key: "y" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093", "runCompositionTest", "#1-6") || + !checkSelection(4, "", "runCompositionTest", "#1-6")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 }, + "key": { key: "x" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055", "runCompositionTest", "#1-7") || + !checkSelection(5, "", "runCompositionTest", "#1-7")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044", + "clauses": + [ + { "length": 6, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 6, "length": 0 }, + "key": { key: "e" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044", "runCompositionTest", "#1-8") || + !checkSelection(6, "", "runCompositionTest", "#1-8")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053", + "clauses": + [ + { "length": 7, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 7, "length": 0 }, + "key": { key: "b" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053", "runCompositionTest", "#1-8") || + !checkSelection(7, "", "runCompositionTest", "#1-8")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046", + "clauses": + [ + { "length": 8, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 8, "length": 0 }, + "key": { key: "4" }, + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046", + "runCompositionTest", "#1-9") || + !checkSelection(8, "", "runCompositionTest", "#1-9")) { + return; + } + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "clauses": + [ + { "length": 4, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 2, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 }, + "key": { key: " " }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "runCompositionTest", "#1-10") || + !checkSelection(4, "", "runCompositionTest", "#1-10")) { + return; + } + + // change the selected clause + synthesizeCompositionChange( + { "composition": + { "string": "\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "clauses": + [ + { "length": 4, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 2, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 6, "length": 0 }, + "key": { key: "KEY_ArrowLeft", shiftKey: true }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "runCompositionTest", "#1-11") || + !checkSelection(6, "", "runCompositionTest", "#1-11")) { + return; + } + + // reset clauses + synthesizeCompositionChange( + { "composition": + { "string": "\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046", + "clauses": + [ + { "length": 5, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 3, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 }, + "key": { key: "KEY_ArrowRight" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046", + "runCompositionTest", "#1-12") || + !checkSelection(5, "", "runCompositionTest", "#1-12")) { + return; + } + + + let textRect1 = synthesizeQueryTextRect(0, 1); + let textRect2 = synthesizeQueryTextRect(1, 1); + if (!checkQueryContentResult(textRect1, + "runCompositionTest: synthesizeQueryTextRect #1-12-1") || + !checkQueryContentResult(textRect2, + "runCompositionTest: synthesizeQueryTextRect #1-12-2")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046", + "runCompositionTest", "#1-13") || + !checkSelection(8, "", "runCompositionTest", "#1-13")) { + return; + } + + let textRect3 = synthesizeQueryTextRect(0, 1); + let textRect4 = synthesizeQueryTextRect(1, 1); + + if (!checkQueryContentResult(textRect3, + "runCompositionTest: synthesizeQueryTextRect #1-13-1") || + !checkQueryContentResult(textRect4, + "runCompositionTest: synthesizeQueryTextRect #1-13-2")) { + return; + } + + checkRect(textRect3, textRect1, "runCompositionTest: textRect #1-13-1"); + checkRect(textRect4, textRect2, "runCompositionTest: textRect #1-13-2"); + + // restart composition and input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3057", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "d" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3057", + "runCompositionTest", "#2-1") || + !checkSelection(8 + 1, "", "runCompositionTest", "#2-1")) { + return; + } + + let textRect3QueriedWithRelativeOffset = synthesizeQueryTextRect(-8, 1, true); + let textRect4QueriedWithRelativeOffset = synthesizeQueryTextRect(-8 + 1, 1, true); + checkRect(textRect3QueriedWithRelativeOffset, textRect3, "runCompositionTest: textRect #2-1-2"); + checkRect(textRect4QueriedWithRelativeOffset, textRect4, "runCompositionTest: textRect #2-1-3"); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3058", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "r" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058", + "runCompositionTest", "#2-2") || + !checkSelection(8 + 1, "", "runCompositionTest", "#2-2")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3058\u3087", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: ")", code: "Digit9", keyCode: KeyboardEvent.DOM_VK_9, shiftKey: true }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058\u3087", + "runCompositionTest", "#2-3") || + !checkSelection(8 + 2, "", "runCompositionTest", "#2-3")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3058\u3087\u3046", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + "key": { key: "4" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058\u3087\u3046", + "runCompositionTest", "#2-4") || + !checkSelection(8 + 3, "", "runCompositionTest", "#2-4")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3055\u884C\u3053\u3046\u3058\u3087\u3046", + "runCompositionTest", "#2-4") || + !checkSelection(8 + 3, "", "runCompositionTest", "#2-4")) { + return; + } + + // set selection + const selectionSetTest = await synthesizeSelectionSet(4, 7, false); + ok(selectionSetTest, "runCompositionTest: selectionSetTest failed"); + + if (!checkSelection(4, "\u3055\u884C\u3053\u3046\u3058\u3087\u3046", "runCompositionTest", "#3-1")) { + return; + } + + // start composition with selection + synthesizeCompositionChange( + { "composition": + { "string": "\u304A", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "6" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u304A", + "runCompositionTest", "#3-2") || + !checkSelection(4 + 1, "", "runCompositionTest", "#3-2")) { + return; + } + + // remove the composition string + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "KEY_Backspace" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#3-3") || + !checkSelection(4, "", "runCompositionTest", "#3-3")) { + return; + } + + // re-input the composition string + synthesizeCompositionChange( + { "composition": + { "string": "\u3046", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "4" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u3046", + "runCompositionTest", "#3-4") || + !checkSelection(4 + 1, "", "runCompositionTest", "#3-4")) { + return; + } + + // cancel the composition + synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Escape" } }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#3-5") || + !checkSelection(4, "", "runCompositionTest", "#3-5")) { + return; + } + + // bug 271815, some Chinese IMEs for Linux make empty composition string + // and compty clause information when it lists up Chinese characters on + // its candidate window. + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "a" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#4-1") || + !checkSelection(4, "", "runCompositionTest", "#4-1")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "b" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#4-2") || + !checkSelection(4, "", "runCompositionTest", "#4-2")) { + return; + } + + synthesizeComposition({ type: "compositioncommit", data: "\u6700", key: { key: "KEY_Enter" } }); + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#4-3") || + !checkSelection(5, "", "runCompositionTest", "#4-3")) { + return; + } + + // testing the canceling case + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "a" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#4-5") || + !checkSelection(5, "", "runCompositionTest", "#4-5")) { + return; + } + + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Escape" } }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#4-6") || + !checkSelection(5, "", "runCompositionTest", "#4-6")) { + return; + } + + // testing whether the empty composition string deletes selected string. + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "a" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#4-8") || + !checkSelection(4, "", "runCompositionTest", "#4-8")) { + return; + } + + synthesizeComposition({ type: "compositioncommit", data: "\u9AD8", key: { key: "KEY_Enter" } }); + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u9AD8", + "runCompositionTest", "#4-9") || + !checkSelection(5, "", "runCompositionTest", "#4-9")) { + return; + } + + synthesizeKey("KEY_Backspace"); + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#4-11") || + !checkSelection(4, "", "runCompositionTest", "#4-11")) { + return; + } + + // bug 23558, ancient Japanese IMEs on Window may send empty text event + // twice at canceling composition. + synthesizeCompositionChange( + { "composition": + { "string": "\u6700", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#5-1") || + !checkSelection(4 + 1, "", "runCompositionTest", "#5-1")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 }, + "key": { key: "KEY_Backspace", type: "keydown" }, + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#5-2") || + !checkSelection(4, "", "runCompositionTest", "#5-2")) { + return; + } + + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Backspace", type: "keyup" } }); + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#5-3") || + !checkSelection(4, "", "runCompositionTest", "#5-3")) { + return; + } + + // Undo tests for the testcases for bug 23558 and bug 271815 + synthesizeKey("z", { accelKey: true }); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#6-1") || + !checkSelection(4, "", "runCompositionTest", "#6-1")) { + return; + } + + synthesizeKey("z", { accelKey: true }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u9AD8", + "runCompositionTest", "#6-2") || + !checkSelection(5, "", "runCompositionTest", "#6-2")) { + return; + } + + synthesizeKey("z", { accelKey: true }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#6-3") || + !checkSelection(4, "", "runCompositionTest", "#6-3")) { + return; + } + + synthesizeKey("z", { accelKey: true }); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700", + "runCompositionTest", "#6-4") || + !checkSelection(5, "", "runCompositionTest", "#6-4")) { + return; + } + + synthesizeKey("z", { accelKey: true }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3", + "runCompositionTest", "#6-5") || + !checkSelection(4, "", "runCompositionTest", "#6-5")) { + // eslint-disable-next-line no-useless-return + return; + } +} + +function runCompositionEventTest() +{ + const kDescription = "runCompositionEventTest: "; + const kEvents = ["compositionstart", "compositionupdate", "compositionend", + "input"]; + + input.value = ""; + input.focus(); + + let windowEventCounts = [], windowEventData = [], windowEventLocale = []; + let inputEventCounts = [], inputEventData = [], inputEventLocale = []; + let preventDefault = false; + let stopPropagation = false; + + function initResults() + { + for (let i = 0; i < kEvents.length; i++) { + windowEventCounts[kEvents[i]] = 0; + windowEventData[kEvents[i]] = ""; + windowEventLocale[kEvents[i]] = ""; + inputEventCounts[kEvents[i]] = 0; + inputEventData[kEvents[i]] = ""; + inputEventLocale[kEvents[i]] = ""; + } + } + + function compositionEventHandlerForWindow(aEvent) + { + windowEventCounts[aEvent.type]++; + windowEventData[aEvent.type] = aEvent.data; + windowEventLocale[aEvent.type] = aEvent.locale; + if (preventDefault) { + aEvent.preventDefault(); + } + if (stopPropagation) { + aEvent.stopPropagation(); + } + } + + function formEventHandlerForWindow(aEvent) + { + ok(aEvent.isTrusted, "input events must be trusted events"); + windowEventCounts[aEvent.type]++; + windowEventData[aEvent.type] = input.value; + } + + function compositionEventHandlerForInput(aEvent) + { + inputEventCounts[aEvent.type]++; + inputEventData[aEvent.type] = aEvent.data; + inputEventLocale[aEvent.type] = aEvent.locale; + if (preventDefault) { + aEvent.preventDefault(); + } + if (stopPropagation) { + aEvent.stopPropagation(); + } + } + + function formEventHandlerForInput(aEvent) + { + inputEventCounts[aEvent.type]++; + inputEventData[aEvent.type] = input.value; + } + + window.addEventListener("compositionstart", compositionEventHandlerForWindow, + true, true); + window.addEventListener("compositionend", compositionEventHandlerForWindow, + true, true); + window.addEventListener("compositionupdate", compositionEventHandlerForWindow, + true, true); + window.addEventListener("input", formEventHandlerForWindow, + true, true); + + input.addEventListener("compositionstart", compositionEventHandlerForInput, + true, true); + input.addEventListener("compositionend", compositionEventHandlerForInput, + true, true); + input.addEventListener("compositionupdate", compositionEventHandlerForInput, + true, true); + input.addEventListener("input", formEventHandlerForInput, + true, true); + + // test for normal case + initResults(); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "o" }, + }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #1"); + is(windowEventData.compositionstart, "", + kDescription + "data of compositionstart isn't empty (window) #1"); + is(windowEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (window) #1"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by input #1"); + is(inputEventData.compositionstart, "", + kDescription + "data of compositionstart isn't empty (input) #1"); + is(inputEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (input) #1"); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #1"); + is(windowEventData.compositionupdate, "\u3089", + kDescription + "data of compositionupdate doesn't match (window) #1"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (window) #1"); + is(inputEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by input #1"); + is(inputEventData.compositionupdate, "\u3089", + kDescription + "data of compositionupdate doesn't match (input) #1"); + is(inputEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (input) #1"); + + is(windowEventCounts.compositionend, 0, + kDescription + "compositionend has been handled by window #1"); + is(inputEventCounts.compositionend, 0, + kDescription + "compositionend has been handled by input #1"); + + is(windowEventCounts.input, 1, + kDescription + "input hasn't been handled by window #1"); + is(windowEventData.input, "\u3089", + kDescription + "value of input element wasn't modified (window) #1"); + is(inputEventCounts.input, 1, + kDescription + "input hasn't been handled by input #1"); + is(inputEventData.input, "\u3089", + kDescription + "value of input element wasn't modified (input) #1"); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "\\", code: "IntlYen", keyCode: KeyboardEvent.DOM_VK_BACKSLASH }, + }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart has been handled more than once by window #2"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart has been handled more than once by input #2"); + + is(windowEventCounts.compositionupdate, 2, + kDescription + "compositionupdate hasn't been handled by window #2"); + is(windowEventData.compositionupdate, "\u3089\u30FC", + kDescription + "data of compositionupdate doesn't match (window) #2"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (window) #2"); + is(inputEventCounts.compositionupdate, 2, + kDescription + "compositionupdate hasn't been handled by input #2"); + is(inputEventData.compositionupdate, "\u3089\u30FC", + kDescription + "data of compositionupdate doesn't match (input) #2"); + is(inputEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (input) #2"); + + is(windowEventCounts.compositionend, 0, + kDescription + "compositionend has been handled during composition by window #2"); + is(inputEventCounts.compositionend, 0, + kDescription + "compositionend has been handled during composition by input #2"); + + is(windowEventCounts.input, 2, + kDescription + "input hasn't been handled by window #2"); + is(windowEventData.input, "\u3089\u30FC", + kDescription + "value of input element wasn't modified (window) #2"); + is(inputEventCounts.input, 2, + kDescription + "input hasn't been handled by input #2"); + is(inputEventData.input, "\u3089\u30FC", + kDescription + "value of input element wasn't modified (input) #2"); + + // text event shouldn't cause composition update, e.g., at committing. + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart has been handled more than once by window #3"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart has been handled more than once by input #3"); + + is(windowEventCounts.compositionupdate, 2, + kDescription + "compositionupdate has been fired unexpectedly on window #3"); + is(inputEventCounts.compositionupdate, 2, + kDescription + "compositionupdate has been fired unexpectedly on input #3"); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #3"); + is(windowEventData.compositionend, "\u3089\u30FC", + kDescription + "data of compositionend doesn't match (window) #3"); + is(windowEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (window) #3"); + is(inputEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by input #3"); + is(inputEventData.compositionend, "\u3089\u30FC", + kDescription + "data of compositionend doesn't match (input) #3"); + is(inputEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (input) #3"); + + is(windowEventCounts.input, 3, + kDescription + "input hasn't been handled by window #3"); + is(windowEventData.input, "\u3089\u30FC", + kDescription + "value of input element wasn't modified (window) #3"); + is(inputEventCounts.input, 3, + kDescription + "input hasn't been handled by input #3"); + is(inputEventData.input, "\u3089\u30FC", + kDescription + "value of input element wasn't modified (input) #3"); + + // select the second character, then, data of composition start should be + // the selected character. + initResults(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "o" }, + }); + + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #4"); + is(windowEventData.compositionstart, "\u30FC", + kDescription + "data of compositionstart is empty (window) #4"); + is(windowEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (window) #4"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by input #4"); + is(inputEventData.compositionstart, "\u30FC", + kDescription + "data of compositionstart is empty (input) #4"); + is(inputEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (input) #4"); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #4"); + is(windowEventData.compositionupdate, "\u3089", + kDescription + "data of compositionupdate doesn't match (window) #4"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (window) #4"); + is(inputEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by input #4"); + is(inputEventData.compositionupdate, "\u3089", + kDescription + "data of compositionupdate doesn't match (input) #4"); + is(inputEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (input) #4"); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #4"); + is(windowEventData.compositionend, "\u3089", + kDescription + "data of compositionend doesn't match (window) #4"); + is(windowEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (window) #4"); + is(inputEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by input #4"); + is(inputEventData.compositionend, "\u3089", + kDescription + "data of compositionend doesn't match (input) #4"); + is(inputEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (input) #4"); + + is(windowEventCounts.input, 2, + kDescription + "input hasn't been handled by window #4"); + is(windowEventData.input, "\u3089\u3089", + kDescription + "value of input element wasn't modified (window) #4"); + is(inputEventCounts.input, 2, + kDescription + "input hasn't been handled by input #4"); + is(inputEventData.input, "\u3089\u3089", + kDescription + "value of input element wasn't modified (input) #4"); + + // preventDefault() should effect nothing. + preventDefault = true; + + initResults(); + synthesizeKey("a", { accelKey: true }); // Select All + + synthesizeCompositionChange( + { "composition": + { "string": "\u306D", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "," }, + }); + + synthesizeComposition({ type: "compositioncommitasis" }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #5"); + is(windowEventData.compositionstart, "\u3089\u3089", + kDescription + "data of compositionstart is empty (window) #5"); + is(windowEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (window) #5"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by input #5"); + is(inputEventData.compositionstart, "\u3089\u3089", + kDescription + "data of compositionstart is empty (input) #5"); + is(inputEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty (input) #5"); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #5"); + is(windowEventData.compositionupdate, "\u306D", + kDescription + "data of compositionupdate doesn't match (window) #5"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (window) #5"); + is(inputEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by input #5"); + is(inputEventData.compositionupdate, "\u306D", + kDescription + "data of compositionupdate doesn't match (input) #5"); + is(inputEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty (input) #5"); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #5"); + is(windowEventData.compositionend, "\u306D", + kDescription + "data of compositionend doesn't match (window) #5"); + is(windowEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (window) #5"); + is(inputEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by input #5"); + is(inputEventData.compositionend, "\u306D", + kDescription + "data of compositionend doesn't match (input) #5"); + is(inputEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty (input) #5"); + + is(windowEventCounts.input, 2, + kDescription + "input hasn't been handled by window #5"); + is(windowEventData.input, "\u306D", + kDescription + "value of input element wasn't modified (window) #5"); + is(inputEventCounts.input, 2, + kDescription + "input hasn't been handled by input #5"); + is(inputEventData.input, "\u306D", + kDescription + "value of input element wasn't modified (input) #5"); + + preventDefault = false; + + // stopPropagation() should effect nothing (except event count) + stopPropagation = true; + + initResults(); + synthesizeKey("a", { accelKey: true }); // Select All + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "\\", code: "IntlRo", keyCode: KeyboardEvent.DOM_VK_BACKSLASH }, + }); + + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #6"); + is(windowEventData.compositionstart, "\u306D", + kDescription + "data of compositionstart is empty #6"); + is(windowEventLocale.compositionstart, "", + kDescription + "locale of compositionstart isn't empty #6"); + is(inputEventCounts.compositionstart, 0, + kDescription + "compositionstart has been handled by input #6"); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #6"); + is(windowEventData.compositionupdate, "\u306E", + kDescription + "data of compositionupdate doesn't match #6"); + is(windowEventLocale.compositionupdate, "", + kDescription + "locale of compositionupdate isn't empty #6"); + is(inputEventCounts.compositionupdate, 0, + kDescription + "compositionupdate has been handled by input #6"); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #6"); + is(windowEventData.compositionend, "\u306E", + kDescription + "data of compositionend doesn't match #6"); + is(windowEventLocale.compositionend, "", + kDescription + "locale of compositionend isn't empty #6"); + is(inputEventCounts.compositionend, 0, + kDescription + "compositionend has been handled by input #6"); + + is(windowEventCounts.input, 2, + kDescription + "input hasn't been handled by window #6"); + is(windowEventData.input, "\u306E", + kDescription + "value of input element wasn't modified (window) #6"); + is(inputEventCounts.input, 2, + kDescription + "input hasn't been handled by input #6"); + is(inputEventData.input, "\u306E", + kDescription + "value of input element wasn't modified (input) #6"); + + stopPropagation = false; + + // create event and dispatch it. + initResults(); + + input.value = "value of input"; + synthesizeKey("a", { accelKey: true }); // Select All + + let compositionstart = document.createEvent("CompositionEvent"); + compositionstart.initCompositionEvent("compositionstart", + true, true, document.defaultView, + "start data", "start locale"); + is(compositionstart.type, "compositionstart", + kDescription + "type doesn't match #7"); + is(compositionstart.data, "start data", + kDescription + "data doesn't match #7"); + is(compositionstart.locale, "start locale", + kDescription + "locale doesn't match #7"); + is(compositionstart.detail, 0, + kDescription + "detail isn't 0 #7"); + + input.dispatchEvent(compositionstart); + + is(windowEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by window #7"); + is(windowEventData.compositionstart, "start data", + kDescription + "data of compositionstart was changed (window) #7"); + is(windowEventLocale.compositionstart, "start locale", + kDescription + "locale of compositionstart was changed (window) #7"); + is(inputEventCounts.compositionstart, 1, + kDescription + "compositionstart hasn't been handled by input #7"); + is(inputEventData.compositionstart, "start data", + kDescription + "data of compositionstart was changed (input) #7"); + is(inputEventLocale.compositionstart, "start locale", + kDescription + "locale of compositionstart was changed (input) #7"); + + is(input.value, "value of input", + kDescription + "input value was changed #7"); + + let compositionupdate1 = document.createEvent("compositionevent"); + compositionupdate1.initCompositionEvent("compositionupdate", + true, false, document.defaultView, + "composing string", "composing locale"); + is(compositionupdate1.type, "compositionupdate", + kDescription + "type doesn't match #8"); + is(compositionupdate1.data, "composing string", + kDescription + "data doesn't match #8"); + is(compositionupdate1.locale, "composing locale", + kDescription + "locale doesn't match #8"); + is(compositionupdate1.detail, 0, + kDescription + "detail isn't 0 #8"); + + input.dispatchEvent(compositionupdate1); + + is(windowEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by window #8"); + is(windowEventData.compositionupdate, "composing string", + kDescription + "data of compositionupdate was changed (window) #8"); + is(windowEventLocale.compositionupdate, "composing locale", + kDescription + "locale of compositionupdate was changed (window) #8"); + is(inputEventCounts.compositionupdate, 1, + kDescription + "compositionupdate hasn't been handled by input #8"); + is(inputEventData.compositionupdate, "composing string", + kDescription + "data of compositionupdate was changed (input) #8"); + is(inputEventLocale.compositionupdate, "composing locale", + kDescription + "locale of compositionupdate was changed (input) #8"); + + is(input.value, "value of input", + kDescription + "input value was changed #8"); + + let compositionupdate2 = document.createEvent("compositionEvent"); + compositionupdate2.initCompositionEvent("compositionupdate", + true, false, document.defaultView, + "commit string", "commit locale"); + is(compositionupdate2.type, "compositionupdate", + kDescription + "type doesn't match #9"); + is(compositionupdate2.data, "commit string", + kDescription + "data doesn't match #9"); + is(compositionupdate2.locale, "commit locale", + kDescription + "locale doesn't match #9"); + is(compositionupdate2.detail, 0, + kDescription + "detail isn't 0 #9"); + + input.dispatchEvent(compositionupdate2); + + is(windowEventCounts.compositionupdate, 2, + kDescription + "compositionupdate hasn't been handled by window #9"); + is(windowEventData.compositionupdate, "commit string", + kDescription + "data of compositionupdate was changed (window) #9"); + is(windowEventLocale.compositionupdate, "commit locale", + kDescription + "locale of compositionupdate was changed (window) #9"); + is(inputEventCounts.compositionupdate, 2, + kDescription + "compositionupdate hasn't been handled by input #9"); + is(inputEventData.compositionupdate, "commit string", + kDescription + "data of compositionupdate was changed (input) #9"); + is(inputEventLocale.compositionupdate, "commit locale", + kDescription + "locale of compositionupdate was changed (input) #9"); + + is(input.value, "value of input", + kDescription + "input value was changed #9"); + + let compositionend = document.createEvent("Compositionevent"); + compositionend.initCompositionEvent("compositionend", + true, false, document.defaultView, + "end data", "end locale"); + is(compositionend.type, "compositionend", + kDescription + "type doesn't match #10"); + is(compositionend.data, "end data", + kDescription + "data doesn't match #10"); + is(compositionend.locale, "end locale", + kDescription + "locale doesn't match #10"); + is(compositionend.detail, 0, + kDescription + "detail isn't 0 #10"); + + input.dispatchEvent(compositionend); + + is(windowEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by window #10"); + is(windowEventData.compositionend, "end data", + kDescription + "data of compositionend was changed (window) #10"); + is(windowEventLocale.compositionend, "end locale", + kDescription + "locale of compositionend was changed (window) #10"); + is(inputEventCounts.compositionend, 1, + kDescription + "compositionend hasn't been handled by input #10"); + is(inputEventData.compositionend, "end data", + kDescription + "data of compositionend was changed (input) #10"); + is(inputEventLocale.compositionend, "end locale", + kDescription + "locale of compositionend was changed (input) #10"); + + is(input.value, "value of input", + kDescription + "input value was changed #10"); + + window.removeEventListener("compositionstart", + compositionEventHandlerForWindow, true); + window.removeEventListener("compositionend", + compositionEventHandlerForWindow, true); + window.removeEventListener("compositionupdate", + compositionEventHandlerForWindow, true); + window.removeEventListener("input", + formEventHandlerForWindow, true); + + input.removeEventListener("compositionstart", + compositionEventHandlerForInput, true); + input.removeEventListener("compositionend", + compositionEventHandlerForInput, true); + input.removeEventListener("compositionupdate", + compositionEventHandlerForInput, true); + input.removeEventListener("input", + formEventHandlerForInput, true); +} + +function runCompositionTestWhoseTextNodeModified() { + const selection = windowOfContenteditable.getSelection(); + + (function testInsertTextBeforeComposition() { + const description = + "runCompositionTestWhoseTextNodeModified: testInsertTextBeforeComposition:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>def</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "def".length); + // Insert composition to the end of a text node + synthesizeSimpleCompositionChange("g"); + is( + textNode.data, + "defg", + `${description} Composition should be inserted to end of the text node` + ); + + // Insert a character before the composition string + textNode.insertData(0, "c"); + is( + textNode.data, + "cdefg", + `${ + description + } Composition should be shifted when a character is inserted before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}cdef`.length, + "g", + `${ + description + } IME selection should be shifted when a character is inserted before it` + ); + + // Update composition string (appending a character) + synthesizeSimpleCompositionChange("gh"); + is( + textNode.data, + "cdefgh", + `${ + description + } Composition should be updated correctly after inserted a character before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}cdef`.length, + "gh", + `${ + description + } IME selection should be extended correctly at updating composition after inserted a character before it` + ); + + // Insert another character before the composition + textNode.insertData(0, "b"); + is( + textNode.data, + "bcdefgh", + `${ + description + } Composition should be shifted when a character is inserted again before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}bcdef`.length, + "gh", + `${ + description + } IME selection should be shifted when a character is inserted again before it` + ); + + // Update the composition string again (appending another character) + synthesizeSimpleCompositionChange("ghi"); + is( + textNode.data, + "bcdefghi", + `${ + description + } Composition should be updated correctly after inserted 2 characters before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}bcdef`.length, + "ghi", + `${ + description + } IME selection should be extended correctly at updating composition after inserted 2 characters before it` + ); + + // Insert a new character before the composition string + textNode.insertData(0, "a"); + is( + textNode.data, + "abcdefghi", + `${ + description + } Composition should be shifted when a character is inserted again and again before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}abcdef`.length, + "ghi", + `${ + description + } IME selection should be shifted when a character is inserted again and again before it` + ); + + // Commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abcdefghi", + `${ + description + } Composition should be committed as is` + ); + is( + selection.focusOffset, + "abcdefghi".length, + `${description} Selection should be collapsed at end of the commit string` + ); + + // Undo the commit + synthesizeKey("z", { accelKey: true }); + is( + textNode.data, + "abcdef", + `${ + description + } Composition should be undone correctly` + ); + is( + selection.focusOffset, + "abcdef".length, + `${ + description + } Selection should be collapsed at where the composition was after undoing` + ); + + // Redo the commit + synthesizeKey("z", { accelKey: true, shiftKey: true }); + is( + textNode.data, + "abcdefghi", + `${ + description + } Composition should be redone correctly` + ); + is( + selection.focusOffset, + "abcdefghi".length, + `${ + description + } focus offset of Selection should be at end of the commit string after redoing` + ); + })(); + + (function testInsertTextImmediatelyBeforeComposition() { + const description = + "runCompositionTestWhoseTextNodeModified: testInsertTextImmediatelyBeforeComposition:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>d</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, 0); + // Insert composition at start of the text node + synthesizeSimpleCompositionChange("b"); + is( + textNode.data, + "bd", + `${description} Composition should be inserted to start of the text node` + ); + + // Insert a character before the composition string + textNode.insertData(0, "a"); + is( + textNode.data, + "abd", + `${ + description + } Composition should be shifted when a character is inserted immediately before it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "b", + `${ + description + } IME selection should be shifted when a character is inserted immediately before it`, + "", + { offset: todo_is, text: todo_is } + ); + + // Update the composition string after inserting character immediately before it + synthesizeSimpleCompositionChange("bc"); + is( + textNode.data, + "abcd", + `${description} Composition should be updated after the inserted character` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "bc", + `${ + description + } IME selection should be set at the composition string after the inserted character` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abcd", + `${ + description + } Composition should be committed after the inserted character` + ); + is( + selection.focusOffset, + "abc".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + (function testInsertTextImmediatelyAfterComposition() { + const description = + "runCompositionTestWhoseTextNodeModified: testInsertTextImmediatelyAfterComposition:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>a</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "a".length); + // Insert composition at end of the text node + synthesizeSimpleCompositionChange("b"); + is( + textNode.data, + "ab", + `${description} Composition should be inserted to start of the text node` + ); + + // Insert a character after the composition string + textNode.insertData("ab".length, "d"); + is( + textNode.data, + "abd", + `${ + description + } Composition should stay when a character is inserted immediately after it` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "b", + `${ + description + } IME selection should stay when a character is inserted immediately after it` + ); + + // Update the composition string after inserting character immediately after it + synthesizeSimpleCompositionChange("bc"); + is( + textNode.data, + "abcd", + `${description} Composition should be updated before the inserted character` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "bc", + `${ + description + } IME selection should be set at the composition string before the inserted character` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abcd", + `${ + description + } Composition should be committed before the inserted character` + ); + is( + selection.focusOffset, + "abc".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // Inserting/replacing text before the last character of composition string + // should be contained by the composition, i.e., updated by next composition + // update. This is Chrome compatible. + (function testInsertTextMiddleOfComposition() { + const description = + "runCompositionTestWhoseTextNodeModified: testInsertTextMiddleOfComposition:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>a</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "a".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("bd"); + is( + textNode.data, + "abd", + `${description} Composition should be inserted to end of the text node` + ); + + // Insert a character before the composition string + textNode.insertData("ab".length, "c"); + is( + textNode.data, + "abcd", + `${ + description + } Inserted string should inserted into the middle of composition string` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "bcd", + `${ + description + } IME selection should be extended when a character is inserted into middle of it` + ); + + // Update the composition string after inserting character into it + synthesizeSimpleCompositionChange("BD"); + is( + textNode.data, + "aBD", + `${ + description + } Composition should be replace the range containing the inserted character` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "BD", + `${ + description + } IME selection should be set at the updated composition string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "aBD", + `${ + description + } Composition should be committed without the inserted character` + ); + is( + selection.focusOffset, + "aBD".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + (function testReplaceFirstCharOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceFirstCharOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted` + ); + + // Replace the composition string + textNode.replaceData("ab".length, "c".length, "XYZ"); + is( + textNode.data, + "abXYZdefg", + `${description} First character of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "XYZde", + `${description} IME selection should contain the replace string` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEfg", + `${description} Composition should update the replace string too` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should update the replace string too` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEfg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // Although Chrome commits composition if all composition string is removed, + // let's keep composition for making TSF stable... + (function testReplaceAllCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceAllCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted to the text node` + ); + + // Replace the composition string + textNode.replaceData("ab".length, "cde".length, "XYZ"); + is( + textNode.data, + "abXYZfg", + `${description} Composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "", + `${ + description + } IME selection should be collapsed before the replace string` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEXYZfg", + `${description} Composition should be inserted again` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should not contain the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEXYZfg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + (function testReplaceCompositionStringAndSurroundedCharacters() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceCompositionStringAndSurroundedCharacters:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted to the text node` + ); + + // Replace the composition string + textNode.replaceData("a".length, "bcdef".length, "XYZ"); + is( + textNode.data, + "aXYZg", + `${description} Composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "", + `${ + description + } IME selection should be collapsed before the replace string` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "aCDEXYZg", + `${description} Composition should be inserted again` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "CDE", + `${description} IME selection should not contain the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "aCDEXYZg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "aCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // If start boundary characters are replaced, the replace string should be + // contained into the composition range. This is Chrome compatible. + (function testReplaceStartBoundaryOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceStartBoundaryOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted to the text node` + ); + + // Replace some text + textNode.replaceData("a".length, "bc".length, "XYZ"); + is( + textNode.data, + "aXYZdefg", + `${ + description + } Start of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "XYZde", + `${description} IME selection should contain the replace string` + ); + + // Update the replace string and remaining composition. + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "aCDEfg", + `${description} Composition should update the replace string too` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}a`.length, + "CDE", + `${description} IME selection should contain the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "aCDEfg", + `${ + description + } Composition should be committed` + ); + is( + selection.focusOffset, + "aCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // If start boundary characters are replaced, the replace string should NOT + // be contained in the composition range. This is Chrome compatible. + (function testReplaceEndBoundaryOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceEndBoundaryOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted to the text node` + ); + + // Replace the composition string + textNode.replaceData("abcd".length, "ef".length, "XYZ"); + is( + textNode.data, + "abcdXYZg", + `${ + description + } End half of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "cd", + `${ + description + } IME selection should be shrunken to the non-replaced part` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEXYZg", + `${description} Only the remaining composition string should be updated` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should NOT include the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEXYZg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + // If the last character of composition is replaced, i.e., it should NOT be + // treated as a part of composition string. This is Chrome compatible. + (function testReplaceLastCharOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceLastCharOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted` + ); + + // Replace the composition string + textNode.replaceData("abcd".length, "e".length, "XYZ"); + is( + textNode.data, + "abcdXYZfg", + `${description} Last character of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "cd", + `${description} IME selection should be shrunken` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEXYZfg", + `${description} Composition should NOT update the replace string` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should not contain the replace string` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEXYZfg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); + + (function testReplaceMiddleCharOfCompositionString() { + const description = + "runCompositionTestWhoseTextNodeModified: testReplaceMiddleCharOfCompositionString:"; + contenteditable.focus(); + contenteditable.innerHTML = "<p>abfg</p>"; + const textNode = contenteditable.firstChild.firstChild; + selection.collapse(textNode, "ab".length); + // Insert composition at middle of the text node + synthesizeSimpleCompositionChange("cde"); + is( + textNode.data, + "abcdefg", + `${description} Composition should be inserted` + ); + + // Replace the composition string + textNode.replaceData("abc".length, "d".length, "XYZ"); + is( + textNode.data, + "abcXYZefg", + `${ + description + } Middle character of the composition should be replaced` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "cXYZe", + `${description} IME selection should be extended by the replace string` + ); + + // Update the composition string after replaced + synthesizeSimpleCompositionChange("CDE"); + is( + textNode.data, + "abCDEfg", + `${description} Composition should update the replace string` + ); + checkIMESelection( + "RawClause", + true, + `${kLF}ab`.length, + "CDE", + `${description} IME selection should be shrunken after update` + ); + + // Commit it + synthesizeComposition({ type: "compositioncommitasis" }); + is( + textNode.data, + "abCDEfg", + `${description} Composition should be committed` + ); + is( + selection.focusOffset, + "abCDE".length, + `${description} Selection should be collapsed at end of the commit string` + ); + })(); +} + +// eslint-disable-next-line complexity +function runQueryTextRectInContentEditableTest() +{ + contenteditable.focus(); + + contenteditable.innerHTML = "<p>abc</p><p>def</p>"; + // \n 0 123 4 567 + // \r\n 01 234 56 789 + + let description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "a" + let a = synthesizeQueryTextRect(kLFLen, 1); + if (!checkQueryContentResult(a, description + "rect for 'a'")) { + return; + } + + // "b" + let b = synthesizeQueryTextRect(kLFLen + 1, 1); + if (!checkQueryContentResult(b, description + "rect for 'b'")) { + return; + } + + is(b.top, a.top, description + "'a' and 'b' should be at same top"); + isSimilarTo(b.left, a.left + a.width, 2, description + "left of 'b' should be at similar to right of 'a'"); + is(b.height, a.height, description + "'a' and 'b' should be same height"); + + // "c" + let c = synthesizeQueryTextRect(kLFLen + 2, 1); + if (!checkQueryContentResult(c, description + "rect for 'c'")) { + return; + } + + is(c.top, b.top, description + "'b' and 'c' should be at same top"); + isSimilarTo(c.left, b.left + b.width, 2, description + "left of 'c' should be at similar to right of 'b'"); + is(c.height, b.height, description + "'b' and 'c' should be same height"); + + // "abc" as array + let abcAsArray = synthesizeQueryTextRectArray(kLFLen, 3); + if (!checkQueryContentResult(abcAsArray, description + "rect array for 'abc'") || + !checkRectArray(abcAsArray, [a, b, c], description + "query text rect array result of 'abc' should match with each query text rect result")) { + return; + } + + // 2nd <p> (can be computed with the rect of 'c') + let p2 = synthesizeQueryTextRect(kLFLen + 3, 1); + if (!checkQueryContentResult(p2, description + "rect for 2nd <p>")) { + return; + } + + is(p2.top, c.top, description + "'c' and a line breaker caused by 2nd <p> should be at same top"); + isSimilarTo(p2.left, c.left + c.width, 2, description + "left of a line breaker caused by 2nd <p> should be at similar to right of 'c'"); + is(p2.height, c.height, description + "'c' and a line breaker caused by 2nd <p> should be same height"); + + // 2nd <p> as array + let p2AsArray = synthesizeQueryTextRectArray(kLFLen + 3, 1); + if (!checkQueryContentResult(p2AsArray, description + "2nd <p>'s line breaker as array") || + !checkRectArray(p2AsArray, [p2], description + "query text rect array result of 2nd <p> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let p2_2 = synthesizeQueryTextRect(kLFLen + 4, 1); + if (!checkQueryContentResult(p2_2, description + "rect for \\n of \\r\\n caused by 2nd <p>")) { + return; + } + + is(p2_2.top, p2.top, description + "'\\r' and '\\n' should be at same top"); + is(p2_2.left, p2.left, description + "'\\r' and '\\n' should be at same top"); + is(p2_2.height, p2.height, description + "'\\r' and '\\n' should be same height"); + is(p2_2.width, p2.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let p2_2AsArray = synthesizeQueryTextRectArray(kLFLen + 4, 1); + if (!checkQueryContentResult(p2_2AsArray, description + "rect array for \\n of \\r\\n caused by 2nd <p>") || + !checkRectArray(p2_2AsArray, [p2_2], description + "query text rect array result of \\n of \\r\\n caused by 2nd <p> should match with each query text rect result")) { + return; + } + } + + // "d" + let d = synthesizeQueryTextRect(kLFLen * 2 + 3, 1); + if (!checkQueryContentResult(d, description + "rect for 'd'")) { + return; + } + + isGreaterThan(d.top, a.top + a.height, description + "top of 'd' should be greater than bottom of 'a'"); + is(d.left, a.left, description + "'a' and 'd' should be same at same left"); + is(d.height, a.height, description + "'a' and 'd' should be same height"); + + // "e" + let e = synthesizeQueryTextRect(kLFLen * 2 + 4, 1); + if (!checkQueryContentResult(e, description + "rect for 'e'")) { + return; + } + + is(e.top, d.top, description + "'d' and 'd' should be at same top"); + isSimilarTo(e.left, d.left + d.width, 2, description + "left of 'e' should be at similar to right of 'd'"); + is(e.height, d.height, description + "'d' and 'e' should be same height"); + + // "f" + let f = synthesizeQueryTextRect(kLFLen * 2 + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + + // "def" as array + let defAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 3, 3); + if (!checkQueryContentResult(defAsArray, description + "rect array for 'def'") || + !checkRectArray(defAsArray, [d, e, f], description + "query text rect array result of 'def' should match with each query text rect result")) { + return; + } + + // next of "f" (can be computed with rect of 'f') + let next_f = synthesizeQueryTextRect(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(next_f, description + "rect for next of 'f'")) { + return; + } + + is(next_f.top, d.top, 2, description + "'f' and next of 'f' should be at same top"); + isSimilarTo(next_f.left, f.left + f.width, 2, description + "left of next of 'f' should be at similar to right of 'f'"); + is(next_f.height, d.height, description + "'f' and next of 'f' should be same height"); + + // next of "f" as array + let next_fAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(next_fAsArray, description + "rect array for next of 'f'") || + !checkRectArray(next_fAsArray, [next_f], description + "query text rect array result of next of 'f' should match with each query text rect result")) { + return; + } + + // too big offset for the editor + let tooBigOffset = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_f.top, description + "too big offset and next of 'f' should be at same top"); + is(tooBigOffset.left, next_f.left, description + "too big offset and next of 'f' should be at same left"); + is(tooBigOffset.height, next_f.height, description + "too big offset and next of 'f' should be same height"); + is(tooBigOffset.width, next_f.width, description + "too big offset and next of 'f' should be same width"); + + // too big offset for the editors as array + let tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + contenteditable.innerHTML = "<p>abc</p><p>def</p><p><br></p>"; + // \n 0 123 4 567 8 9 + // \r\n 01 234 56 789 01 23 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "f" + f = synthesizeQueryTextRect(kLFLen * 2 + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + + // 3rd <p> (can be computed with rect of 'f') + let p3 = synthesizeQueryTextRect(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(p3, description + "rect for 3rd <p>")) { + return; + } + + is(p3.top, f.top, description + "'f' and a line breaker caused by 3rd <p> should be at same top"); + is(p3.height, f.height, description + "'f' and a line breaker caused by 3rd <p> should be same height"); + isSimilarTo(p3.left, f.left + f.width, 2, description + "left of a line breaker caused by 3rd <p> should be similar to right of 'f'"); + + // 3rd <p> as array + let p3AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(p3AsArray, description + "3rd <p>'s line breaker as array") || + !checkRectArray(p3AsArray, [p3], description + "query text rect array result of 3rd <p> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let p3_2 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(p3_2, description + "rect for \\n of \\r\\n caused by 3rd <p>")) { + return; + } + + is(p3_2.top, p3.top, description + "'\\r' and '\\n' should be at same top"); + is(p3_2.left, p3.left, description + "'\\r' and '\\n' should be at same top"); + is(p3_2.height, p3.height, description + "'\\r' and '\\n' should be same height"); + is(p3_2.width, p3.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let p3_2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(p3_2AsArray, description + "rect array for \\n of \\r\\n caused by 3rd <p>") || + !checkRectArray(p3_2AsArray, [p3_2], description + "query text rect array result of \\n of \\r\\n caused by 3rd <p> should match with each query text rect result")) { + return; + } + } + + // <br> in 3rd <p> + let br = synthesizeQueryTextRect(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(br, description + "rect for <br> in 3rd <p>")) { + return; + } + + isGreaterThan(br.top, d.top + d.height, description + "a line breaker caused by <br> in 3rd <p> should be greater than bottom of 'd'"); + isSimilarTo(br.height, d.height, 2, description + "'d' and a line breaker caused by <br> in 3rd <p> should be similar height"); + is(br.left, d.left, description + "left of a line breaker caused by <br> in 3rd <p> should be same left of 'd'"); + + // <br> in 3rd <p> as array + let brAsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(brAsArray, description + "<br> in 3rd <p> as array") || + !checkRectArray(brAsArray, [br], description + "query text rect array result of <br> in 3rd <p> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br_2 = synthesizeQueryTextRect(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(br_2, description + "rect for \\n of \\r\\n caused by <br> in 3rd <p>")) { + return; + } + + is(br_2.top, br.top, description + "'\\r' and '\\n' should be at same top"); + is(br_2.left, br.left, description + "'\\r' and '\\n' should be at same top"); + is(br_2.height, br.height, description + "'\\r' and '\\n' should be same height"); + is(br_2.width, br.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br_2AsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(br_2AsArray, description + "rect array for \\n of \\r\\n caused by <br> in 3rd <p>") || + !checkRectArray(br_2AsArray, [br_2], description + "query text rect array result of \\n of \\r\\n caused by <br> in 3rd <p> should match with each query text rect result")) { + return; + } + } + + // next of <br> in 3rd <p> + let next_br = synthesizeQueryTextRect(kLFLen * 4 + 6, 1); + if (!checkQueryContentResult(next_br, description + "rect for next of <br> in 3rd <p>")) { + return; + } + + is(next_br.top, br.top, description + "next of <br> and <br> should be at same top"); + is(next_br.left, br.left, description + "next of <br> and <br> should be at same left"); + is(next_br.height, br.height, description + "next of <br> and <br> should be same height"); + is(next_br.width, br.width, description + "next of <br> and <br> should be same width"); + + // next of <br> in 3rd <p> as array + let next_brAsArray = synthesizeQueryTextRectArray(kLFLen * 4 + 6, 1); + if (!checkQueryContentResult(next_brAsArray, description + "rect array for next of <br> in 3rd <p>") || + !checkRectArray(next_brAsArray, [next_br], description + "query text rect array result of next of <br> in 3rd <p> should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen * 4 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_br.top, description + "too big offset and next of 3rd <p> should be at same top"); + is(tooBigOffset.left, next_br.left, description + "too big offset and next of 3rd <p> should be at same left"); + is(tooBigOffset.height, next_br.height, description + "too big offset and next of 3rd <p> should be same height"); + is(tooBigOffset.width, next_br.width, description + "too big offset and next of 3rd <p> should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 4 + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + contenteditable.innerHTML = "<p>abc</p><p>def</p><p></p>"; + // \n 0 123 4 567 8 + // \r\n 01 234 56 789 0 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "f" + f = synthesizeQueryTextRect(kLFLen * 2 + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + + // 3rd <p> (can be computed with rect of 'f') + p3 = synthesizeQueryTextRect(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(p3, description + "rect for 3rd <p>")) { + return; + } + + is(p3.top, f.top, description + "'f' and a line breaker caused by 3rd <p> should be at same top"); + is(p3.height, f.height, description + "'f' and a line breaker caused by 3rd <p> should be same height"); + isSimilarTo(p3.left, f.left + f.width, 2, description + "left of a line breaker caused by 3rd <p> should be similar to right of 'f'"); + + // 3rd <p> as array + p3AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(p3AsArray, description + "3rd <p>'s line breaker as array") || + !checkRectArray(p3AsArray, [p3], description + "query text rect array result of 3rd <p> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let p3_2 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(p3_2, description + "rect for \\n of \\r\\n caused by 3rd <p>")) { + return; + } + + is(p3_2.top, p3.top, description + "'\\r' and '\\n' should be at same top"); + is(p3_2.left, p3.left, description + "'\\r' and '\\n' should be at same top"); + is(p3_2.height, p3.height, description + "'\\r' and '\\n' should be same height"); + is(p3_2.width, p3.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let p3_2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(p3_2AsArray, description + "rect array for \\n of \\r\\n caused by 3rd <p>") || + !checkRectArray(p3_2AsArray, [p3_2], description + "query text rect array result of \\n of \\r\\n caused by 3rd <p> should match with each query text rect result")) { + return; + } + } + + // next of 3rd <p> + let next_p3 = synthesizeQueryTextRect(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(next_p3, description + "rect for next of 3rd <p>")) { + return; + } + + isGreaterThan(next_p3.top, d.top + d.height, description + "top of next of 3rd <p> should equal to or be bigger than bottom of 'd'"); + isSimilarTo(next_p3.left, d.left, 2, description + "left of next of 3rd <p> should be at similar to left of 'd'"); + isSimilarTo(next_p3.height, d.height, 2, description + "next of 3rd <p> and 'd' should be similar height"); + + // next of 3rd <p> as array + let next_p3AsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(next_p3AsArray, description + "next of 3rd <p> as array") || + !checkRectArray(next_p3AsArray, [next_p3], description + "query text rect array result of next of 3rd <p> should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_p3.top, description + "too big offset and next of 3rd <p> should be at same top"); + is(tooBigOffset.left, next_p3.left, description + "too big offset and next of 3rd <p> should be at same left"); + is(tooBigOffset.height, next_p3.height, description + "too big offset and next of 3rd <p> should be same height"); + is(tooBigOffset.width, next_p3.width, description + "too big offset and next of 3rd <p> should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + contenteditable.innerHTML = "abc<br>def"; + // \n 0123 456 + // \r\n 01234 567 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "a" + a = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(a, description + "rect for 'a'")) { + return; + } + + // "b" + b = synthesizeQueryTextRect(1, 1); + if (!checkQueryContentResult(b, description + "rect for 'b'")) { + return; + } + + is(b.top, a.top, description + "'a' and 'b' should be at same top"); + isSimilarTo(b.left, a.left + a.width, 2, description + "left of 'b' should be at similar to right of 'a'"); + is(b.height, a.height, description + "'a' and 'b' should be same height"); + + // "c" + c = synthesizeQueryTextRect(2, 1); + if (!checkQueryContentResult(c, description + "rect for 'c'")) { + return; + } + + is(c.top, b.top, description + "'b' and 'c' should be at same top"); + isSimilarTo(c.left, b.left + b.width, 2, description + "left of 'c' should be at similar to right of 'b'"); + is(c.height, b.height, description + "'b' and 'c' should be same height"); + + // "abc" as array + abcAsArray = synthesizeQueryTextRectArray(0, 3); + if (!checkQueryContentResult(abcAsArray, description + "rect array for 'abc'") || + !checkRectArray(abcAsArray, [a, b, c], description + "query text rect array result of 'abc' should match with each query text rect result")) { + return; + } + + // <br> (can be computed with the rect of 'c') + br = synthesizeQueryTextRect(3, 1); + if (!checkQueryContentResult(br, description + "rect for <br>")) { + return; + } + + is(br.top, c.top, description + "'c' and a line breaker caused by <br> should be at same top"); + isSimilarTo(br.left, c.left + c.width, 2, description + "left of a line breaker caused by <br> should be at similar to right of 'c'"); + is(br.height, c.height, description + "'c' and a line breaker caused by <br> should be same height"); + + // <br> as array + brAsArray = synthesizeQueryTextRectArray(3, 1); + if (!checkQueryContentResult(brAsArray, description + "<br>'s line breaker as array") || + !checkRectArray(brAsArray, [br], description + "query text rect array result of <br> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br_2 = synthesizeQueryTextRect(4, 1); + if (!checkQueryContentResult(br_2, description + "rect for \n of \r\n caused by <br>")) { + return; + } + + is(br_2.top, br.top, description + "'\\r' and '\\n' should be at same top"); + is(br_2.left, br.left, description + "'\\r' and '\\n' should be at same top"); + is(br_2.height, br.height, description + "'\\r' and '\\n' should be same height"); + is(br_2.width, br.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br_2AsArray = synthesizeQueryTextRectArray(4, 1); + if (!checkQueryContentResult(br_2AsArray, description + "rect array for \\n of \\r\\n caused by <br>") || + !checkRectArray(br_2AsArray, [br_2], description + "query text rect array result of \\n of \\r\\n caused by <br> should match with each query text rect result")) { + return; + } + } + + // "d" + d = synthesizeQueryTextRect(kLFLen + 3, 1); + if (!checkQueryContentResult(d, description + "rect for 'd'")) { + return; + } + + isSimilarTo(d.top, a.top + a.height, 2, description + "top of 'd' should be at similar to bottom of 'a'"); + is(d.left, a.left, description + "'a' and 'd' should be same at same left"); + is(d.height, a.height, description + "'a' and 'd' should be same height"); + + // "e" + e = synthesizeQueryTextRect(kLFLen + 4, 1); + if (!checkQueryContentResult(e, description + "rect for 'e'")) { + return; + } + + is(e.top, d.top, description + "'d' and 'd' should be at same top"); + isSimilarTo(e.left, d.left + d.width, 2, description + "left of 'e' should be at similar to right of 'd'"); + is(e.height, d.height, description + "'d' and 'e' should be same height"); + + // "f" + f = synthesizeQueryTextRect(kLFLen + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + + // "def" as array + defAsArray = synthesizeQueryTextRectArray(kLFLen + 3, 3); + if (!checkQueryContentResult(defAsArray, description + "rect array for 'def'") || + !checkRectArray(defAsArray, [d, e, f], description + "query text rect array result of 'def' should match with each query text rect result")) { + return; + } + + // next of "f" (can be computed with rect of 'f') + next_f = synthesizeQueryTextRect(kLFLen + 6, 1); + if (!checkQueryContentResult(next_f, description + "rect for next of 'f'")) { + return; + } + + is(next_f.top, d.top, 2, description + "'f' and next of 'f' should be at same top"); + isSimilarTo(next_f.left, f.left + f.width, 2, description + "left of next of 'f' should be at similar to right of 'f'"); + is(next_f.height, d.height, description + "'f' and next of 'f' should be same height"); + + // next of "f" as array + next_fAsArray = synthesizeQueryTextRectArray(kLFLen + 6, 1); + if (!checkQueryContentResult(next_fAsArray, description + "rect array for next of 'f'") || + !checkRectArray(next_fAsArray, [next_f], description + "query text rect array result of next of 'f' should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_f.top, description + "too big offset and next of 'f' should be at same top"); + is(tooBigOffset.left, next_f.left, description + "too big offset and next of 'f' should be at same left"); + is(tooBigOffset.height, next_f.height, description + "too big offset and next of 'f' should be same height"); + is(tooBigOffset.width, next_f.width, description + "too big offset and next of 'f' should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + // Note that this case does not have an empty line at the end. + contenteditable.innerHTML = "abc<br>def<br>"; + // \n 0123 4567 + // \r\n 01234 56789 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "f" + f = synthesizeQueryTextRect(kLFLen + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + + // 2nd <br> (can be computed with rect of 'f') + let br2 = synthesizeQueryTextRect(kLFLen + 6, 1); + if (!checkQueryContentResult(br2, description + "rect for 2nd <br>")) { + return; + } + + is(br2.top, f.top, description + "'f' and a line breaker caused by 2nd <br> should be at same top"); + is(br2.height, f.height, description + "'f' and a line breaker caused by 2nd <br> should be same height"); + isSimilarTo(br2.left, f.left + f.width, 2, description + "left of a line breaker caused by 2nd <br> should be similar to right of 'f'"); + + // 2nd <br> as array + let br2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(br2AsArray, description + "2nd <br>'s line breaker as array") || + !checkRectArray(br2AsArray, [br2], description + "query text rect array result of 2nd <br> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br2_2 = synthesizeQueryTextRect(kLFLen + 7, 1); + if (!checkQueryContentResult(br2_2, description + "rect for \\n of \\r\\n caused by 2nd <br>")) { + return; + } + + is(br2_2.top, br2.top, description + "'\\r' and '\\n' should be at same top"); + is(br2_2.left, br2.left, description + "'\\r' and '\\n' should be at same top"); + is(br2_2.height, br2.height, description + "'\\r' and '\\n' should be same height"); + is(br2_2.width, br2.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br2_2AsArray = synthesizeQueryTextRectArray(kLFLen + 7, 1); + if (!checkQueryContentResult(br2_2AsArray, description + "rect array for \\n of \\r\\n caused by 2nd <br>") || + !checkRectArray(br2_2AsArray, [br2_2], description + "query text rect array result of \\n of \\r\\n caused by 2nd <br> should match with each query text rect result")) { + return; + } + } + + // next of 2nd <br> + let next_br2 = synthesizeQueryTextRect(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(next_br2, description + "rect for next of 2nd <br>")) { + return; + } + + is(next_br2.top, br2.top, description + "2nd <br> and next of 2nd <br> should be at same top"); + is(next_br2.left, br2.left, description + "2nd <br> and next of 2nd <br> should be at same top"); + is(next_br2.height, br2.height, description + "2nd <br> and next of 2nd <br> should be same height"); + is(next_br2.width, br2.width, description + "2nd <br> and next of 2nd <br> should be same width"); + + // next of 2nd <br> as array + let next_br2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 6, 1); + if (!checkQueryContentResult(next_br2AsArray, description + "rect array for next of 2nd <br>") || + !checkRectArray(next_br2AsArray, [next_br2], description + "query text rect array result of next of 2nd <br> should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_br2.top, description + "too big offset and next of 2nd <br> should be at same top"); + is(tooBigOffset.left, next_br2.left, description + "too big offset and next of 2nd <br> should be at same left"); + is(tooBigOffset.height, next_br2.height, description + "too big offset and next of 2nd <br> should be same height"); + is(tooBigOffset.width, next_br2.width, description + "too big offset and next of 2nd <br> should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset") || + !checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result")) { + return; + } + + contenteditable.innerHTML = "abc<br>def<br><br>"; + // \n 0123 4567 8 + // \r\n 01234 56789 01 + + description = "runTextRectInContentEditableTest: \"" + contenteditable.innerHTML + "\", "; + + // "f" + f = synthesizeQueryTextRect(kLFLen + 5, 1); + if (!checkQueryContentResult(f, description + "rect for 'f'")) { + return; + } + + is(f.top, e.top, description + "'e' and 'f' should be at same top"); + isSimilarTo(f.left, e.left + e.width, 2, description + "left of 'f' should be at similar to right of 'e'"); + is(f.height, e.height, description + "'e' and 'f' should be same height"); + + // 2nd <br> + br2 = synthesizeQueryTextRect(kLFLen + 6, 1); + if (!checkQueryContentResult(br2, description + "rect for 2nd <br>")) { + return; + } + + is(br2.top, f.top, description + "'f' and a line breaker caused by 2nd <br> should be at same top"); + is(br2.height, f.height, description + "'f' and a line breaker caused by 2nd <br> should be same height"); + ok(f.left < br2.left, description + "left of a line breaker caused by 2nd <br> should be bigger than left of 'f', f.left=" + f.left + ", br2.left=" + br2.left); + + // 2nd <br> as array + br2AsArray = synthesizeQueryTextRectArray(kLFLen + 6, 1); + if (!checkQueryContentResult(br2AsArray, description + "2nd <br>'s line breaker as array") || + !checkRectArray(br2AsArray, [br2], description + "query text rect array result of 2nd <br> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br2_2 = synthesizeQueryTextRect(kLFLen + 7, 1); + if (!checkQueryContentResult(br2_2, description + "rect for \\n of \\r\\n caused by 2nd <br>")) { + return; + } + + is(br2_2.top, br2.top, description + "'\\r' and '\\n' should be at same top"); + is(br2_2.left, br2.left, description + "'\\r' and '\\n' should be at same top"); + is(br2_2.height, br2.height, description + "'\\r' and '\\n' should be same height"); + is(br2_2.width, br2.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br2_2AsArray = synthesizeQueryTextRectArray(kLFLen + 7, 1); + if (!checkQueryContentResult(br2_2AsArray, description + "rect array for \\n of \\r\\n caused by 2nd <br>") || + !checkRectArray(br2_2AsArray, [br2_2], description + "query text rect array result of \\n of \\r\\n caused by 2nd <br> should match with each query text rect result")) { + return; + } + } + + // 3rd <br> + let br3 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(br3, description + "rect for next of 3rd <br>")) { + return; + } + + isSimilarTo(br3.top, d.top + d.height, 3, description + "top of next of 3rd <br> should at similar to bottom of 'd'"); + isSimilarTo(br3.left, d.left, 2, description + "left of next of 3rd <br> should be at similar to left of 'd'"); + isSimilarTo(br3.height, d.height, 2, description + "next of 3rd <br> and 'd' should be similar height"); + + // 3rd <br> as array + let br3AsArray = synthesizeQueryTextRectArray(kLFLen + 6, 1); + if (!checkQueryContentResult(br3AsArray, description + "3rd <br>'s line breaker as array") || + !checkRectArray(br3AsArray, [br3], description + "query text rect array result of 3rd <br> should match with each query text rect result")) { + return; + } + + if (kLFLen > 1) { + // \n of \r\n + let br3_2 = synthesizeQueryTextRect(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(br3_2, description + "rect for \\n of \\r\\n caused by 3rd <br>")) { + return; + } + + is(br3_2.top, br3.top, description + "'\\r' and '\\n' should be at same top"); + is(br3_2.left, br3.left, description + "'\\r' and '\\n' should be at same left"); + is(br3_2.height, br3.height, description + "'\\r' and '\\n' should be same height"); + is(br3_2.width, br3.width, description + "'\\r' and '\\n' should be same width"); + + // \n of \r\n as array + let br3_2AsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (!checkQueryContentResult(br3_2AsArray, description + "rect array for \\n of \\r\\n caused by 3rd <br>") || + !checkRectArray(br3_2AsArray, [br3_2], description + "query text rect array result of \\n of \\r\\n caused by 3rd <br> should match with each query text rect result")) { + return; + } + } + + // next of 3rd <br> + let next_br3 = synthesizeQueryTextRect(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(next_br3, description + "rect for next of 3rd <br>")) { + return; + } + + is(next_br3.top, br3.top, description + "3rd <br> and next of 3rd <br> should be at same top"); + is(next_br3.left, br3.left, description + "3rd <br> and next of 3rd <br> should be at same left"); + is(next_br3.height, br3.height, description + "3rd <br> and next of 3rd <br> should be same height"); + is(next_br3.width, br3.width, description + "3rd <br> and next of 3rd <br> should be same width"); + + // next of 3rd <br> as array + let next_br3AsArray = synthesizeQueryTextRectArray(kLFLen * 3 + 6, 1); + if (!checkQueryContentResult(next_br3AsArray, description + "rect array for next of 3rd <br>") || + !checkRectArray(next_br3AsArray, [next_br3], description + "query text rect array result of next of 3rd <br> should match with each query text rect result")) { + return; + } + + // too big offset for the editor + tooBigOffset = synthesizeQueryTextRect(kLFLen * 3 + 7, 1); + if (!checkQueryContentResult(tooBigOffset, description + "rect for too big offset")) { + return; + } + + is(tooBigOffset.top, next_br3.top, description + "too big offset and next of 3rd <br> should be at same top"); + is(tooBigOffset.left, next_br3.left, description + "too big offset and next of 3rd <br> should be at same left"); + is(tooBigOffset.height, next_br3.height, description + "too big offset and next of 3rd <br> should be same height"); + is(tooBigOffset.width, next_br3.width, description + "too big offset and next of 3rd <br> should be same width"); + + // too big offset for the editors as array + tooBigOffsetAsArray = synthesizeQueryTextRectArray(kLFLen * 2 + 7, 1); + if (checkQueryContentResult(tooBigOffsetAsArray, description + "rect array for too big offset")) { + checkRectArray(tooBigOffsetAsArray, [tooBigOffset], description + "query text rect array result with too big offset should match with each query text rect result"); + } + + if (!(function test_query_text_rects_across_invisible_text() { + contenteditable.innerHTML = "<div>\n<div>abc</div></div>"; + // \n 0 1 2 345 + // \r\n 01 2345 678 + description = `runQueryTextRectInContentEditableTest: test_query_text_rects_across_invisible_text: "${ + contenteditable.innerHTML.replace(/\n/g, "\\n") + }",`; + // rect of "a" + const rectA = synthesizeQueryTextRect(kLFLen * 3, 1); + if (!checkQueryContentResult(rectA, `${description} rect of "a"`)) { + return false; + } + const rectArrayFromStartToA = synthesizeQueryTextRectArray(0, kLFLen * 3 + 1); + if (!checkQueryContentResult(rectArrayFromStartToA, `${description} rect array from invisible text to "a"`)) { + return false; + } + const fromStartToARects = getRectArray(rectArrayFromStartToA); + if (!checkRect( + fromStartToARects[kLFLen * 3], + rectA, + `${description} rect for "a" in array should be same as the result of query only it` + )) { + return false; + } + return checkRect( + fromStartToARects[kLFLen * 2], + fromStartToARects[0], + `${description} rect for the linebreak in invisible text node should be same as first linebreak` + ); + })()) { + return; + } + + function test_query_text_rects_starting_from_invisible_text() { + contenteditable.innerHTML = "<div>\n<div>abc</div></div>"; + // \n 0 1 2 345 + // \r\n 01 2345 678 + description = `runQueryTextRectInContentEditableTest: test_query_text_rects_starting_from_invisible_text: "${ + contenteditable.innerHTML.replace(/\n/g, "\\n") + }",`; + // rect of "a" + const rectA = synthesizeQueryTextRect(kLFLen * 3, 1); + if (!checkQueryContentResult(rectA, `${description} rect of "a"`)) { + return false; + } + const rectArrayFromInvisibleToA = synthesizeQueryTextRectArray(kLFLen, kLFLen * 2 + 1); + if (!checkQueryContentResult(rectArrayFromInvisibleToA, `${description} rect array from invisible text to "a"`)) { + return false; + } + const fromInvisibleToARects = getRectArray(rectArrayFromInvisibleToA); + if (!checkRect( + fromInvisibleToARects[kLFLen * 2], + rectA, + `${description} rect for "a" in array should be same as the result of query only it` + )) { + return false; + } + // For now the rect of characters in invisible text node should be caret rect + // before the following line break. This is inconsistent from the result of + // the query text rect array event starting from the previous visible line + // break or character, but users anyway cannot insert text into the invisible + // text node only with user's operation. Therefore, this won't be problem + // in usual web apps. + const caretRectBeforeLineBreakBeforeA = fromInvisibleToARects[kLFLen]; + return checkRect( + fromInvisibleToARects[0], + caretRectBeforeLineBreakBeforeA, + `${description} rect for the linebreak in invisible text node should be same as caret rect before the following visible linebreak before "a"` + ); + } + if (!test_query_text_rects_starting_from_invisible_text()) { + return; + } + + if (kLFLen > 1) { + function test_query_text_rects_starting_from_middle_of_invisible_linebreak() { + contenteditable.innerHTML = "<div>\n<div>abc</div></div>"; + // \n 0 1 2 345 + // \r\n 01 2345 678 + description = `runQueryTextRectInContentEditableTest: test_query_text_rects_starting_from_middle_of_invisible_linebreak: "${ + contenteditable.innerHTML.replace(/\n/g, "\\n") + }",`; + // rect of "a" + const rectA = synthesizeQueryTextRect(kLFLen * 3, 1); + if (!checkQueryContentResult(rectA, `${description} rect of "a"`)) { + return false; + } + const rectArrayFromInvisibleToA = synthesizeQueryTextRectArray(kLFLen + 1, 1 + kLFLen + 1); + if (!checkQueryContentResult(rectArrayFromInvisibleToA, `${description} rect array from invisible text to "a"`)) { + return false; + } + const fromInvisibleToARects = getRectArray(rectArrayFromInvisibleToA); + if (!checkRect( + fromInvisibleToARects[1 + kLFLen], + rectA, + `${description} rect for "a" in array should be same as the result of query only it` + )) { + return false; + } + // For now the rect of characters in invisible text node should be caret rect + // before the following line break. This is inconsistent from the result of + // the query text rect array event starting from the previous visible line + // break or character, but users anyway cannot insert text into the invisible + // text node only with user's operation. Therefore, this won't be problem + // in usual web apps. + const caretRectBeforeLineBreakBeforeA = fromInvisibleToARects[1]; + return checkRect( + fromInvisibleToARects[0], + caretRectBeforeLineBreakBeforeA, + `${description} rect for the linebreak in invisible text node should be same as caret rect before the following visible linebreak before "a"` + ); + } + if (!test_query_text_rects_starting_from_middle_of_invisible_linebreak()) { + return; + } + } + + function test_query_text_rects_ending_with_invisible_text() { + contenteditable.innerHTML = "<div><div>abc</div>\n</div>"; + // \n 0 1 234 5 + // \r\n 01 23 456 78 + description = `runQueryTextRectInContentEditableTest: test_query_text_rects_ending_with_invisible_text: "${ + contenteditable.innerHTML.replace(/\n/g, "\\n") + }",`; + // rect of "c" + const rectC = synthesizeQueryTextRect(kLFLen * 2 + 2, 1); + if (!checkQueryContentResult(rectC, `${description} rect of "c"`)) { + return false; + } + const rectArrayFromCToInvisible = synthesizeQueryTextRectArray(kLFLen * 2 + 2, 1 + kLFLen); + if (!checkQueryContentResult(rectArrayFromCToInvisible, `${description} rect array from "c" to invisible linebreak`)) { + return false; + } + const fromCToInvisibleRects = getRectArray(rectArrayFromCToInvisible); + if (!checkRect( + fromCToInvisibleRects[0], + rectC, + `${description} rect for "c" in array should be same as the result of query only it` + )) { + return false; + } + const caretRectAfterC = { + left: rectC.left + rectC.width, + top: rectC.top, + width: 1, + height: rectC.height, + }; + return checkRectFuzzy( + fromCToInvisibleRects[1], + caretRectAfterC, + { + left: 1, + top: 0, + width: 0, + height: 0, + }, + `${description} rect for the linebreak in invisible text node should be same as caret rect after "c"` + ); + } + if (!test_query_text_rects_ending_with_invisible_text()) { + // eslint-disable-next-line no-useless-return + return; + } +} + +function runCharAtPointTest(aFocusedEditor, aTargetName) +{ + aFocusedEditor.value = "This is a test of the\nContent Events"; + // 012345678901234567890 12345678901234 + // 0 1 2 3 + + aFocusedEditor.focus(); + + const kNone = -1; + const kTestingOffset = [ 0, 10, 20, 21 + kLFLen, 34 + kLFLen]; + const kLeftSideOffset = [ kNone, 9, 19, kNone, 33 + kLFLen]; + const kRightSideOffset = [ 1, 11, kNone, 22 + kLFLen, kNone]; + const kLeftTentativeCaretOffset = [ 0, 10, 20, 21 + kLFLen, 34 + kLFLen]; + const kRightTentativeCaretOffset = [ 1, 11, 21, 22 + kLFLen, 35 + kLFLen]; + + let editorRect = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRect, + "runCharAtPointTest (" + aTargetName + "): editorRect")) { + return; + } + + for (let i = 0; i < kTestingOffset.length; i++) { + let textRect = synthesizeQueryTextRect(kTestingOffset[i], 1); + if (!checkQueryContentResult(textRect, + "runCharAtPointTest (" + aTargetName + "): textRect: i=" + i)) { + continue; + } + + checkRectContainsRect(textRect, editorRect, + "runCharAtPointTest (" + aTargetName + + "): the text rect isn't in the editor"); + + // Test #1, getting same character rect by the point near the top-left. + let charAtPt1 = synthesizeCharAtPoint(textRect.left + 1, + textRect.top + 1); + if (checkQueryContentResult(charAtPt1, + "runCharAtPointTest (" + aTargetName + "): charAtPt1: i=" + i)) { + ok(!charAtPt1.notFound, + "runCharAtPointTest (" + aTargetName + "): charAtPt1 isn't found: i=" + i); + if (!charAtPt1.notFound) { + is(charAtPt1.offset, kTestingOffset[i], + "runCharAtPointTest (" + aTargetName + "): charAtPt1 offset is wrong: i=" + i); + checkRect(charAtPt1, textRect, "runCharAtPointTest (" + aTargetName + + "): charAtPt1 left is wrong: i=" + i); + } + ok(!charAtPt1.tentativeCaretOffsetNotFound, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt1 isn't found: i=" + i); + if (!charAtPt1.tentativeCaretOffsetNotFound) { + is(charAtPt1.tentativeCaretOffset, kLeftTentativeCaretOffset[i], + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt1 is wrong: i=" + i); + } + } + + // Test #2, getting same character rect by the point near the bottom-right. + let charAtPt2 = synthesizeCharAtPoint(textRect.left + textRect.width - 2, + textRect.top + textRect.height - 2); + if (checkQueryContentResult(charAtPt2, + "runCharAtPointTest (" + aTargetName + "): charAtPt2: i=" + i)) { + ok(!charAtPt2.notFound, + "runCharAtPointTest (" + aTargetName + "): charAtPt2 isn't found: i=" + i); + if (!charAtPt2.notFound) { + is(charAtPt2.offset, kTestingOffset[i], + "runCharAtPointTest (" + aTargetName + "): charAtPt2 offset is wrong: i=" + i); + checkRect(charAtPt2, textRect, "runCharAtPointTest (" + aTargetName + + "): charAtPt1 left is wrong: i=" + i); + } + ok(!charAtPt2.tentativeCaretOffsetNotFound, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt2 isn't found: i=" + i); + if (!charAtPt2.tentativeCaretOffsetNotFound) { + is(charAtPt2.tentativeCaretOffset, kRightTentativeCaretOffset[i], + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt2 is wrong: i=" + i); + } + } + + // Test #3, getting left character offset. + let charAtPt3 = synthesizeCharAtPoint(textRect.left - 2, + textRect.top + 1); + if (checkQueryContentResult(charAtPt3, + "runCharAtPointTest (" + aTargetName + "): charAtPt3: i=" + i)) { + is(charAtPt3.notFound, kLeftSideOffset[i] == kNone, + kLeftSideOffset[i] == kNone ? + "runCharAtPointTest (" + aTargetName + "): charAtPt3 is found: i=" + i : + "runCharAtPointTest (" + aTargetName + "): charAtPt3 isn't found: i=" + i); + if (!charAtPt3.notFound) { + is(charAtPt3.offset, kLeftSideOffset[i], + "runCharAtPointTest (" + aTargetName + "): charAtPt3 offset is wrong: i=" + i); + } + if (kLeftSideOffset[i] == kNone) { + // There may be no enough padding-left (depends on platform) + todo(false, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt3 isn't tested: i=" + i); + } else { + ok(!charAtPt3.tentativeCaretOffsetNotFound, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt3 isn't found: i=" + i); + if (!charAtPt3.tentativeCaretOffsetNotFound) { + is(charAtPt3.tentativeCaretOffset, kLeftTentativeCaretOffset[i], + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt3 is wrong: i=" + i); + } + } + } + + // Test #4, getting right character offset. + let charAtPt4 = synthesizeCharAtPoint(textRect.left + textRect.width + 1, + textRect.top + textRect.height - 2); + if (checkQueryContentResult(charAtPt4, + "runCharAtPointTest (" + aTargetName + "): charAtPt4: i=" + i)) { + is(charAtPt4.notFound, kRightSideOffset[i] == kNone, + kRightSideOffset[i] == kNone ? + "runCharAtPointTest (" + aTargetName + "): charAtPt4 is found: i=" + i : + "runCharAtPointTest (" + aTargetName + "): charAtPt4 isn't found: i=" + i); + if (!charAtPt4.notFound) { + is(charAtPt4.offset, kRightSideOffset[i], + "runCharAtPointTest (" + aTargetName + "): charAtPt4 offset is wrong: i=" + i); + } + ok(!charAtPt4.tentativeCaretOffsetNotFound, + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt4 isn't found: i=" + i); + if (!charAtPt4.tentativeCaretOffsetNotFound) { + is(charAtPt4.tentativeCaretOffset, kRightTentativeCaretOffset[i], + "runCharAtPointTest (" + aTargetName + "): tentative caret offset for charAtPt4 is wrong: i=" + i); + } + } + } +} + +function runCharAtPointAtOutsideTest() +{ + textarea.focus(); + textarea.value = "some text"; + let editorRect = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRect, + "runCharAtPointAtOutsideTest: editorRect")) { + return; + } + // Check on a text node which is at the outside of editor. + let charAtPt = synthesizeCharAtPoint(editorRect.left + 20, + editorRect.top - 10); + if (checkQueryContentResult(charAtPt, + "runCharAtPointAtOutsideTest: charAtPt")) { + ok(charAtPt.notFound, + "runCharAtPointAtOutsideTest: charAtPt is found on outside of editor"); + ok(charAtPt.tentativeCaretOffsetNotFound, + "runCharAtPointAtOutsideTest: tentative caret offset for charAtPt is found on outside of editor"); + } +} + +async function runSetSelectionEventTest() +{ + contenteditable.focus(); + + const selection = windowOfContenteditable.getSelection(); + + // #1 + contenteditable.innerHTML = "abc<br>def"; + + await synthesizeSelectionSet(0, 100); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node of the editor"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of children"); + checkSelection(0, "abc" + kLF + "def", "runSetSelectionEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(2, 2 + kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 2, + "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 2"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(2, "c" + kLF + "d", "runSetSelectionEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1, 2); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node"); + is(selection.focusOffset, contenteditable.firstChild.wholeText.length, + "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node"); + checkSelection(1, "bc", "runSetSelectionEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, contenteditable.firstChild.wholeText.length, + "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the first text node"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 2, + "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the index of the last text node"); + checkSelection(3, kLF, "runSetSelectionEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen, 0); + is(selection.anchorNode, contenteditable.lastChild, + "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.lastChild.wholeText.length, + "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.anchorOffset, contenteditable.lastChild.wholeText.length, + "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(6 + kLFLen, "", "runSetSelectionEventTest #1 (6+kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(100, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node of the editor"); + is(selection.anchorOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node of the editor"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of children"); + checkSelection(6 + kLFLen, "", "runSetSelectionEventTest #1 (100, 0), \"" + contenteditable.innerHTML + "\""); + + // #2 + contenteditable.innerHTML = "<p>a<b>b</b>c</p><p>def</p>"; + + await synthesizeSelectionSet(kLFLen, 4+kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first <p> node"); + is(selection.focusNode, contenteditable.lastChild.firstChild, + "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the second <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen, "abc" + kLF + "d", "runSetSelectionEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 2); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first <p> node"); + is(selection.focusNode, contenteditable.firstChild.childNodes.item(1).firstChild, + "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <b> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.item(1).firstChild.wholeText.length, + "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node in the <b> node"); + checkSelection(kLFLen, "ab", "runSetSelectionEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1+kLFLen, 2); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node in the first <p> node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node in the first <p> node"); + checkSelection(1+kLFLen, "bc", "runSetSelectionEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(2+kLFLen, 2+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.childNodes.item(1).firstChild, + "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <b> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.item(1).firstChild.wholeText.length, + "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node in the <b> node"); + is(selection.focusNode, contenteditable.lastChild.firstChild, + "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the last <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(2+kLFLen, "c" + kLF + "d", "runSetSelectionEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen*2, 1); + is(selection.anchorNode, contenteditable.lastChild, + "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the second <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the second <p> node"); + is(selection.focusNode, contenteditable.lastChild.firstChild, + "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the second <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(3+kLFLen*2, "d", "runSetSelectionEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #2 (0, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first <p> node"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the first <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF, "runSetSelectionEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(2+kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.childNodes.item(1).firstChild, + "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node of the <b> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.item(1).firstChild.wholeText.length, + "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node of the first <b> node"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(2+kLFLen, "c" + kLF, "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node of the first <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node of the first <p> node"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(3+kLFLen, kLF, "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node of the first <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node of the first <p> node"); + is(selection.focusNode, contenteditable.lastChild.firstChild, + "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node of the second <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(3+kLFLen, kLF + "d", "runSetSelectionEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #3 + contenteditable.innerHTML = "<div>abc<p>def</p></div>"; + + await synthesizeSelectionSet(1+kLFLen, 2); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node"); + is(selection.focusOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the first text node"); + checkSelection(1+kLFLen, "bc", "runSetSelectionEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1+kLFLen, 3+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(1+kLFLen, "bc" + kLF + "d", "runSetSelectionEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, 0); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the first text node"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node"); + is(selection.focusOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the first text node"); + checkSelection(3+kLFLen, "", "runSetSelectionEventTest #3 (3+kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 6+kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(0, kLF + "abc" + kLF + "def", "runSetSelectionEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 100); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, kLF + "abc" + kLF + "def", "runSetSelectionEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(4+kLFLen*2, 2); + is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(4+kLFLen*2, 100); + is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #3 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen*2, 1); + is(selection.anchorNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the first text node"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <div> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF, "runSetSelectionEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 1+kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the first text node of the <div> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(0, kLF + "a", "runSetSelectionEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(2+kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node of the <div> node"); + is(selection.anchorOffset, 2, + "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 2"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(2+kLFLen, "c" + kLF, "runSetSelectionEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node of the <div> node"); + is(selection.anchorOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(3+kLFLen, kLF, "runSetSelectionEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node of the <div> node"); + is(selection.anchorOffset, contenteditable.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.lastChild.firstChild, + "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node of the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(3+kLFLen, kLF + "d", "runSetSelectionEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #4 + contenteditable.innerHTML = "<div><p>abc</p>def</div>"; + + await synthesizeSelectionSet(1+kLFLen*2, 2); + is(selection.anchorNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <p> node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node in the <p> node"); + checkSelection(1+kLFLen*2, "bc", "runSetSelectionEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1+kLFLen*2, 3); + is(selection.anchorNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <p> node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(1+kLFLen*2, "bcd", "runSetSelectionEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(3+kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node in the <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the text node in the <p> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.firstChild.firstChild.wholeText.length, + "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the text node in the <p> node"); + checkSelection(3+kLFLen*2, "", "runSetSelectionEventTest #4 (3+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 6+kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(0, kLF + kLF + "abcdef", "runSetSelectionEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 100); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, kLF + kLF + "abcdef", "runSetSelectionEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(4+kLFLen*2, 2); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(4+kLFLen*2, 100); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(4+kLFLen*2, "ef", "runSetSelectionEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #4 (6+kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(6+kLFLen*2, 1); + is(selection.anchorNode, contenteditable.firstChild.lastChild, + "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the last text node"); + is(selection.anchorOffset, contenteditable.firstChild.lastChild.wholeText.length, + "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the length of the last text node"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(6+kLFLen*2, "", "runSetSelectionEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <div> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF, "runSetSelectionEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF + kLF, "runSetSelectionEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 1+kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <div> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(0, kLF + kLF + "a", "runSetSelectionEventTest #4 (0, 1+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <div> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <div> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #4 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <div> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(kLFLen, kLF, "runSetSelectionEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <div> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node"); + is(selection.focusNode, contenteditable.firstChild.firstChild.firstChild, + "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node in the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen, kLF +"a", "runSetSelectionEventTest #4 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #5 + contenteditable.innerHTML = "<br>"; + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #5 (0, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, kLF, "runSetSelectionEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #5 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 1); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\""); + + // #6 + contenteditable.innerHTML = "<p><br></p>"; + + await synthesizeSelectionSet(kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen, kLF, "runSetSelectionEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the <p>'s children"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen*2, "", "runSetSelectionEventTest #6 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*2, 1); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen*2, "", "runSetSelectionEventTest #6 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, kLF, "runSetSelectionEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen*2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(0, kLF + kLF, "runSetSelectionEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #6 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + // #7 + contenteditable.innerHTML = "<br><br>"; + + await synthesizeSelectionSet(0, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(0, kLF, "runSetSelectionEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, kLFLen * 2); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, kLF + kLF, "runSetSelectionEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #7 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, kLFLen); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen, kLFLen) selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen, kLF, "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen * 2, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the root's children"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen * 2, "", "runSetSelectionEventTest #7 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + // #8 + contenteditable.innerHTML = "<p><br><br></p>"; + + await synthesizeSelectionSet(kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen, kLF, "runSetSelectionEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, kLFLen * 2); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen, kLF + kLF, "runSetSelectionEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*2, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 1"); + checkSelection(kLFLen*2, "", "runSetSelectionEventTest #8 (kLFLen*2, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*2, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*2, kLFLen) selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen*2, kLF, "runSetSelectionEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen*3, 0); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the <p> node"); + is(selection.anchorOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the count of the <p>'s children"); + is(selection.focusNode, contenteditable.firstChild, + "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, contenteditable.firstChild.childNodes.length, + "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the <p>'s children"); + checkSelection(kLFLen*3, "", "runSetSelectionEventTest #8 (kLFLen*3, 0), \"" + contenteditable.innerHTML + "\""); + + // #9 (ContentEventHandler cannot distinguish if <p> can have children, so, the result is same as case #5, "<br>") + contenteditable.innerHTML = "<p></p>"; + + await synthesizeSelectionSet(kLFLen, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node + 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the <p> node"); + is(selection.focusOffset, 1, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be the index of the <p> node + 1"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(kLFLen, 1); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be the index of the <p> node + 1"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #9 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #9 (kLFLen, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(kLFLen, "", "runSetSelectionEventTest #9 (kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + // #10 + contenteditable.innerHTML = ""; + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #10 (0, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 1); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #10 (0, 1), \"" + contenteditable.innerHTML + "\""); + + // #11 + contenteditable.innerHTML = "<span></span><i><u></u></i>"; + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #11 (0, 0), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(0, 1); + is(selection.anchorNode, contenteditable, + "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor node should be the root node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable, + "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus node should be the root node"); + is(selection.focusOffset, contenteditable.childNodes.length, + "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\": selection focus offset should be the count of the root's children"); + checkSelection(0, "", "runSetSelectionEventTest #11 (0, 1), \"" + contenteditable.innerHTML + "\""); + + // #12 + contenteditable.innerHTML = "<span>abc</span><i><u></u></i>"; + selection.selectAllChildren(contenteditable); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.firstChild.firstChild, + "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #12 (0, 0), \"" + contenteditable.innerHTML + "\""); + + // #13 + contenteditable.innerHTML = "<span></span><i>abc<u></u></i>"; + selection.selectAllChildren(contenteditable); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable.childNodes.item(1).firstChild, + "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.childNodes.item(1).firstChild, + "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #13 (0, 0), \"" + contenteditable.innerHTML + "\""); + + // #14 + contenteditable.innerHTML = "<span></span><i><u>abc</u></i>"; + selection.selectAllChildren(contenteditable); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable.childNodes.item(1).firstChild.firstChild, + "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.childNodes.item(1).firstChild.firstChild, + "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #14 (0, 0), \"" + contenteditable.innerHTML + "\""); + + // #15 + contenteditable.innerHTML = "<span></span><i><u></u>abc</i>"; + selection.selectAllChildren(contenteditable); + + await synthesizeSelectionSet(0, 0); + is(selection.anchorNode, contenteditable.childNodes.item(1).lastChild, + "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.childNodes.item(1).lastChild, + "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the text node"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(0, "", "runSetSelectionEventTest #15 (0, 0), \"" + contenteditable.innerHTML + "\""); + + // #16 + contenteditable.innerHTML = "a<blink>b</blink>c"; + await synthesizeSelectionSet(0, 3); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first text node"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection focus node should be the last text node"); + is(selection.focusOffset, contenteditable.lastChild.wholeText.length, + "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\": selection focus offset should be the length of the last text node"); + checkSelection(0, "abc", "runSetSelectionEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\""); + + // #17 (bug 1319660 - incorrect adjustment of content iterator last node) + contenteditable.innerHTML = "<div>a</div><div><br></div>"; + + await synthesizeSelectionSet(kLFLen, 1+kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <div> element"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <div> element"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(kLFLen, "a" + kLF, "runSetSelectionEventTest #17 (kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + await synthesizeSelectionSet(1+2*kLFLen, 0); + is(selection.anchorNode, contenteditable.lastChild, + "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor node should be the second <div> element"); + is(selection.anchorOffset, 0, + "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 0"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <div> element"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(1+2*kLFLen, "", "runSetSelectionEventTest #17 (1+2*kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + // #18 (bug 1319660 - content iterator start node regression) + contenteditable.innerHTML = "<div><br></div><div><br></div>"; + + await synthesizeSelectionSet(2*kLFLen, kLFLen); + is(selection.anchorNode, contenteditable.firstChild, + "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor node should be the first <div> element"); + is(selection.anchorOffset, 1, + "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection anchor offset should be 1"); + is(selection.focusNode, contenteditable.lastChild, + "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus node should be the second <div> element"); + is(selection.focusOffset, 0, + "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\": selection focus offset should be 0"); + checkSelection(2*kLFLen, kLF, "runSetSelectionEventTest #18 (2*kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); +} + +function runQueryTextContentEventTest() +{ + contenteditable.focus(); + + let result; + + // #1 + contenteditable.innerHTML = "abc<br>def"; + + result = synthesizeQueryTextContent(0, 6 + kLFLen); + is(result.text, "abc" + kLF + "def", "runQueryTextContentEventTest #1 (0, 6+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 100); + is(result.text, "abc" + kLF + "def", "runQueryTextContentEventTest #1 (0, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(2, 2 + kLFLen); + is(result.text, "c" + kLF + "d", "runQueryTextContentEventTest #1 (2, 2+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(1, 2); + is(result.text, "bc", "runQueryTextContentEventTest #1 (1, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #1 (3, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(6 + kLFLen, 1); + is(result.text, "", "runQueryTextContentEventTest #1 (6 + kLFLen, 0), \"" + contenteditable.innerHTML + "\""); + + // #2 + contenteditable.innerHTML = "<p>a<b>b</b>c</p><p>def</p>"; + + result = synthesizeQueryTextContent(kLFLen, 4+kLFLen); + is(result.text, "abc" + kLF + "d", "runQueryTextContentEventTest #2 (kLFLen, 4+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, 2); + is(result.text, "ab", "runQueryTextContentEventTest #2 (kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(1+kLFLen, 2); + is(result.text, "bc", "runQueryTextContentEventTest #2 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(2+kLFLen, 2+kLFLen); + is(result.text, "c" + kLF + "d", "runQueryTextContentEventTest #2 (2+kLFLen, 2+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen*2, 1); + is(result.text, "d", "runQueryTextContentEventTest #2 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #2 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(2+kLFLen, 1+kLFLen); + is(result.text, "c" + kLF, "runQueryTextContentEventTest #2 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #2 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen, 1+kLFLen); + is(result.text, kLF + "d", "runQueryTextContentEventTest #2 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #3 + contenteditable.innerHTML = "<div>abc<p>def</p></div>"; + + result = synthesizeQueryTextContent(1+kLFLen, 2); + is(result.text, "bc", "runQueryTextContentEventTest #3 (1+kLFLen, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(1+kLFLen, 3+kLFLen); + is(result.text, "bc" + kLF + "d", "runQueryTextContentEventTest #3 (1+kLFLen, 3+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen*2, 1); + is(result.text, "d", "runQueryTextContentEventTest #3 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 6+kLFLen*2); + is(result.text, kLF + "abc" + kLF + "def", "runQueryTextContentEventTest #3 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 100); + is(result.text, kLF + "abc" + kLF + "def", "runQueryTextContentEventTest #3 (0, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(4+kLFLen*2, 2); + is(result.text, "ef", "runQueryTextContentEventTest #3 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(4+kLFLen*2, 100); + is(result.text, "ef", "runQueryTextContentEventTest #3 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(6+kLFLen*2, 1); + is(result.text, "", "runQueryTextContentEventTest #3 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #3 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 1+kLFLen); + is(result.text, kLF + "a", "runQueryTextContentEventTest #3 (0, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(2+kLFLen, 1+kLFLen); + is(result.text, "c" + kLF, "runQueryTextContentEventTest #3 (2+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #3 (3+kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen, 1+kLFLen); + is(result.text, kLF + "d", "runQueryTextContentEventTest #3 (3+kLFLen, 1+kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #4 + contenteditable.innerHTML = "<div><p>abc</p>def</div>"; + + result = synthesizeQueryTextContent(1+kLFLen*2, 2); + is(result.text, "bc", "runQueryTextContentEventTest #4 (1+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(1+kLFLen*2, 3); + is(result.text, "bcd", "runQueryTextContentEventTest #4 (1+kLFLen*2, 3), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(3+kLFLen*2, 1); + is(result.text, "d", "runQueryTextContentEventTest #4 (3+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 6+kLFLen*2); + is(result.text, kLF + kLF + "abcdef", "runQueryTextContentEventTest #4 (0, 6+kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 100); + is(result.text, kLF + kLF + "abcdef", "runQueryTextContentEventTest #4 (0, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(4+kLFLen*2, 2); + is(result.text, "ef", "runQueryTextContentEventTest #4 (4+kLFLen*2, 2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(4+kLFLen*2, 100); + is(result.text, "ef", "runQueryTextContentEventTest #4 (4+kLFLen*2, 100), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(6+kLFLen*2, 1); + is(result.text, "", "runQueryTextContentEventTest #4 (6+kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #4 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen*2); + is(result.text, kLF + kLF, "runQueryTextContentEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, 1+kLFLen*2); + is(result.text, kLF + kLF + "a", "runQueryTextContentEventTest #4 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, 1+kLFLen); + is(result.text, kLF + "a", "runQueryTextContentEventTest #4 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + // #5 + contenteditable.innerHTML = "<br>"; + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #5 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, 1); + is(result.text, "", "runQueryTextContentEventTest #5 (kLFLen, 1), \"" + contenteditable.innerHTML + "\""); + + // #6 + contenteditable.innerHTML = "<p><br></p>"; + + result = synthesizeQueryTextContent(kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #6 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen*2, 1); + is(result.text, "", "runQueryTextContentEventTest #5 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #6 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen*2); + is(result.text, kLF + kLF, "runQueryTextContentEventTest #6 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + // #7 + contenteditable.innerHTML = "<br><br>"; + + result = synthesizeQueryTextContent(0, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #7 (0, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(0, kLFLen * 2); + is(result.text, kLF + kLF, "runQueryTextContentEventTest #7 (0, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #7 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen * 2, 1); + is(result.text, "", "runQueryTextContentEventTest #7 (kLFLen*2, 1), \"" + contenteditable.innerHTML + "\""); + + // #8 + contenteditable.innerHTML = "<p><br><br></p>"; + + result = synthesizeQueryTextContent(kLFLen, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #8 (kLFLen, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen, kLFLen * 2); + is(result.text, kLF + kLF, "runQueryTextContentEventTest #8 (kLFLen, kLFLen*2), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen*2, kLFLen); + is(result.text, kLF, "runQueryTextContentEventTest #8 (kLFLen*2, kLFLen), \"" + contenteditable.innerHTML + "\""); + + result = synthesizeQueryTextContent(kLFLen*3, 1); + is(result.text, "", "runQueryTextContentEventTest #8 (kLFLen*3, 1), \"" + contenteditable.innerHTML + "\""); + + // #16 + contenteditable.innerHTML = "a<blink>b</blink>c"; + + result = synthesizeQueryTextContent(0, 3); + is(result.text, "abc", "runQueryTextContentEventTest #16 (0, 3), \"" + contenteditable.innerHTML + "\""); +} + +function runQuerySelectionEventTest() +{ + contenteditable.focus(); + + let selection = windowOfContenteditable.getSelection(); + + // #1 + contenteditable.innerHTML = "<br/>a"; + selection.setBaseAndExtent( + contenteditable.firstChild, + 0, + contenteditable.lastChild, + 1 + ); + checkSelection( + 0, + `${kLF}a`, + `runQuerySelectionEventTest #1, "${contenteditable.innerHTML}"` + ); + + // #2 + contenteditable.innerHTML = "<p></p><p>abc</p>"; + selection.setBaseAndExtent( + contenteditable.firstChild, + 0, + contenteditable.lastChild.firstChild, + 1 + ); + checkSelection( + kLFLen, + `${kLF}a`, + `runQuerySelectionEventTest #2, "${contenteditable.innerHTML}"` + ); + + // #3 + contenteditable.innerHTML = "<p>abc</p><p>def</p>"; + selection.setBaseAndExtent( + contenteditable.firstChild, + 0, + contenteditable.lastChild.firstChild, + 1 + ); + checkSelection( + kLFLen, + `abc${kLF}d`, + `runQuerySelectionEventTest #3, "${contenteditable.innerHTML}"` + ); + + // #4 + contenteditable.innerHTML = "<p>abc</p>"; + selection.removeAllRanges(); + checkSelection( + null, + null, + `runQuerySelectionEventTest #4, "${contenteditable.innerHTML}"` + ); +} + +function runQueryIMESelectionTest() +{ + textarea.focus(); + textarea.value = "before after"; + let startoffset = textarea.selectionStart = textarea.selectionEnd = "before ".length; + + if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: before starting composition") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: before starting composition") || + !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: before starting composition") || + !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: before starting composition")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + if (!checkIMESelection("RawClause", true, startoffset, "a", "runQueryIMESelectionTest: inputting raw text") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: inputting raw text") || + !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: inputting raw text") || + !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: inputting raw text")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "abcdefgh", + "clauses": + [ + { "length": 8, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 8, "length": 0 } + }); + + if (!checkIMESelection("RawClause", true, startoffset, "abcdefgh", "runQueryIMESelectionTest: updating raw text") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: updating raw text") || + !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: updating raw text") || + !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: updating raw text")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "ABCDEFGH", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 3, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 3, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: starting to convert") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: starting to convert") || + !checkIMESelection("ConvertedClause", true, startoffset + 2, "CDE", "runQueryIMESelectionTest: starting to convert") || + !checkIMESelection("SelectedClause", true, startoffset, "AB", "runQueryIMESelectionTest: starting to convert")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "ABCDEFGH", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 3, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 3, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: changing selected clause") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: changing selected clause") || + !checkIMESelection("ConvertedClause", true, startoffset, "AB", "runQueryIMESelectionTest: changing selected clause") || + !checkIMESelection("SelectedClause", true, startoffset + 2, "CDE", "runQueryIMESelectionTest: changing selected clause")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeComposition({ type: "compositioncommitasis" }); + + if (!checkIMESelection("RawClause", false, 0, "", "runQueryIMESelectionTest: after committing composition") || + !checkIMESelection("SelectedRawClause", false, 0, "", "runQueryIMESelectionTest: after committing composition") || + !checkIMESelection("ConvertedClause", false, 0, "", "runQueryIMESelectionTest: after committing composition") || + !checkIMESelection("SelectedClause", false, 0, "", "runQueryIMESelectionTest: after committing composition")) { + return; + } + + startoffset = textarea.selectionStart; + + synthesizeCompositionChange( + { "composition": + { "string": "abcdefgh", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + ] + }, + "caret": { "start": 8, "length": 0 } + }); + + if (!checkIMESelection("RawClause", true, startoffset, "a", "runQueryIMESelectionTest: unrealistic testcase") || + !checkIMESelection("SelectedRawClause", true, startoffset + 1, "b", "runQueryIMESelectionTest: unrealistic testcase") || + !checkIMESelection("ConvertedClause", true, startoffset + 2, "c", "runQueryIMESelectionTest: unrealistic testcase") || + !checkIMESelection("SelectedClause", true, startoffset + 3, "d", "runQueryIMESelectionTest: unrealistic testcase")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeComposition({ type: "compositioncommitasis" }); +} + +function runQueryPasswordTest() { + function checkRange(aOffset, aLength, aExpectedResult, aDescription) { + password.focus(); + let result = synthesizeQueryTextContent(aOffset, aLength); + is(result.text, aExpectedResult, + `${aDescription}: synthesizeQueryTextContent(${aOffset}, ${aLength})`); + password.setSelectionRange(aOffset, aOffset + aLength); + result = synthesizeQuerySelectedText(); + is(result.text, aExpectedResult, + `${aDescription}: synthesizeQuerySelectedText(${aOffset}, ${aLength})`); + } + + let editor = password.editor; + const kMask = editor.passwordMask; + password.value = "abcdef"; + + editor.mask(); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range is not specified #1"); + checkRange(0, 3, `${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range is not specified #2"); + checkRange(3, 3, `${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range is not specified #3"); + checkRange(2, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range is not specified #4"); + + editor.unmask(0, 6); + checkRange(0, 6, "abcdef", + "runQueryPasswordTest: unmasked range 0-6 #1"); + checkRange(0, 3, "abc", + "runQueryPasswordTest: unmasked range 0-6 #2"); + checkRange(3, 3, "def", + "runQueryPasswordTest: unmasked range 0-6 #3"); + checkRange(2, 2, "cd", + "runQueryPasswordTest: unmasked range 0-6 #4"); + + editor.unmask(0, 3); + checkRange(0, 6, `abc${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range 0-3 #1"); + checkRange(0, 3, "abc", + "runQueryPasswordTest: unmasked range 0-3 #2"); + checkRange(3, 3, `${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range 0-3 #3"); + checkRange(2, 2, `c${kMask}`, + "runQueryPasswordTest: unmasked range 0-3 #4"); + + editor.unmask(3, 6); + checkRange(0, 6, `${kMask}${kMask}${kMask}def`, + "runQueryPasswordTest: unmasked range 3-6 #1"); + checkRange(0, 3, `${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range 3-6 #2"); + checkRange(3, 3, `def`, + "runQueryPasswordTest: unmasked range 3-6 #3"); + checkRange(2, 2, `${kMask}d`, + "runQueryPasswordTest: unmasked range 3-6 #4"); + + editor.unmask(2, 4); + checkRange(0, 6, `${kMask}${kMask}cd${kMask}${kMask}`, + "runQueryPasswordTest: unmasked range 3-4 #1"); + checkRange(1, 2, `${kMask}c`, + "runQueryPasswordTest: unmasked range 3-4 #2"); + checkRange(1, 3, `${kMask}cd`, + "runQueryPasswordTest: unmasked range 3-4 #3"); + checkRange(1, 4, `${kMask}cd${kMask}`, + "runQueryPasswordTest: unmasked range 3-4 #4"); + checkRange(2, 2, "cd", + "runQueryPasswordTest: unmasked range 3-4 #5"); + checkRange(2, 3, `cd${kMask}`, + "runQueryPasswordTest: unmasked range 3-4 #6"); + + + const kEmoji = String.fromCodePoint(0x1f914); + password.value = `${kEmoji}${kEmoji}${kEmoji}` + + editor.mask(); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range is not specified"); + + editor.unmask(0, 2); + checkRange(0, 6, `${kEmoji}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #1"); + checkRange(0, 2, `${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #2"); + checkRange(2, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #3"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-2 #4"); + + editor.unmask(2, 4); + checkRange(0, 6, `${kMask}${kMask}${kEmoji}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #1"); + checkRange(0, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #2"); + checkRange(2, 2, `${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #3"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-4 #4"); + + editor.unmask(4, 6); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #1"); + checkRange(0, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #2"); + checkRange(2, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #3"); + checkRange(4, 2, `${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-6 #4"); + + editor.unmask(0, 1); + checkRange(0, 6, `${kEmoji}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 0-1"); + + editor.unmask(1, 2); + checkRange(0, 6, `${kEmoji}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 1-2"); + + editor.unmask(2, 3); + checkRange(0, 6, `${kMask}${kMask}${kEmoji}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 2-3"); + + editor.unmask(3, 4); + checkRange(0, 6, `${kMask}${kMask}${kEmoji}${kMask}${kMask}`, + "runQueryPasswordTest: Emojis in password, unmasked range 3-4"); + + editor.unmask(4, 5); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 4-5"); + + editor.unmask(5, 6); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kEmoji}`, + "runQueryPasswordTest: Emojis in password, unmasked range 5-6"); + + + const kEmojiSuperhero = String.fromCodePoint(0x1f9b8); + const kEmojiMediumSkinTone = String.fromCodePoint(0x1f3fd); + const kZeroWidthJoiner = "\u200d"; + const kFemaleSign = "\u2640"; + const kVariationSelector16 = "\ufe0f"; + const kComplicatedEmoji = `${kEmojiSuperhero}${kEmojiMediumSkinTone}${kZeroWidthJoiner}${kFemaleSign}${kVariationSelector16}`; + password.value = `${kComplicatedEmoji}${kComplicatedEmoji}${kComplicatedEmoji}` + editor.mask(); + checkRange(0, 21, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range is not specified"); + + editor.unmask(0, 7); + checkRange(0, 21, `${kComplicatedEmoji}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #1"); + checkRange(0, 7, `${kComplicatedEmoji}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #2"); + checkRange(7, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #3"); + checkRange(14, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 0-7 #4"); + + editor.unmask(7, 14); + checkRange(0, 21, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kComplicatedEmoji}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #1"); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #2"); + checkRange(7, 7, `${kComplicatedEmoji}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #3"); + checkRange(14, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 7-14 #4"); + + editor.unmask(14, 21); + checkRange(0, 21, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kComplicatedEmoji}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #1"); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #2"); + checkRange(7, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #3"); + checkRange(14, 7, `${kComplicatedEmoji}`, + "runQueryPasswordTest: Complicated emojis in password, unmasked range 14-21 #4"); + + password.value = `${kComplicatedEmoji}` + editor.unmask(0, 1); + checkRange(0, 7, `${kEmojiSuperhero}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 0-1"); + + editor.unmask(1, 2); + checkRange(0, 7, `${kEmojiSuperhero}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 1-2"); + + editor.unmask(2, 3); + checkRange(0, 7, `${kMask}${kMask}${kEmojiMediumSkinTone}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 2-3"); + + editor.unmask(3, 4); + checkRange(0, 7, `${kMask}${kMask}${kEmojiMediumSkinTone}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 3-4"); + + editor.unmask(4, 5); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kZeroWidthJoiner}${kMask}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 4-5"); + + editor.unmask(5, 6); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kFemaleSign}${kMask}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 5-6"); + + editor.unmask(6, 7); + checkRange(0, 7, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kVariationSelector16}`, + "runQueryPasswordTest: Complicated emoji in password, unmasked range 6-7"); + + + const kKanji = "\u8fba"; + const kIVS = String.fromCodePoint(0xe0101); + const kKanjiWithIVS = `${kKanji}${kIVS}`; + password.value = `${kKanjiWithIVS}${kKanjiWithIVS}${kKanjiWithIVS}` + + editor.mask(); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range is not specified"); + + editor.unmask(0, 3); + checkRange(0, 9, `${kKanjiWithIVS}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #1"); + checkRange(0, 3, `${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #2"); + checkRange(1, 3, `${kIVS}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #3"); + checkRange(0, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #4"); + checkRange(1, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #5"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #6"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #7"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #8"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #9"); + + editor.unmask(0, 1); + checkRange(0, 9, `${kKanji}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #1"); + checkRange(0, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-1 #7"); + + editor.unmask(1, 3); + checkRange(0, 9, `${kMask}${kIVS}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #2"); + checkRange(1, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-3 #7"); + + editor.unmask(3, 6); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kKanjiWithIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #1"); + checkRange(3, 3, `${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #2"); + checkRange(4, 3, `${kIVS}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #3"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #4"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #5"); + checkRange(3, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #6"); + checkRange(4, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #7"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #8"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-6 #9"); + + editor.unmask(3, 4); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kKanji}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #3"); + checkRange(3, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-4 #7"); + + editor.unmask(4, 6); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #4"); + checkRange(4, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-6 #7"); + + editor.unmask(6, 9); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #1"); + checkRange(6, 3, `${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #2"); + checkRange(4, 3, `${kMask}${kMask}${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #3"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #4"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #5"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #6"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #7"); + checkRange(6, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #8"); + checkRange(7, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-9 #9"); + + editor.unmask(6, 7); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kKanji}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #5"); + checkRange(6, 1, `${kKanji}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #6"); + checkRange(7, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 6-7 #7"); + + editor.unmask(7, 9); + checkRange(0, 9, `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #1"); + checkRange(0, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #2"); + checkRange(1, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #3"); + checkRange(3, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #4"); + checkRange(4, 2, `${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #5"); + checkRange(6, 1, `${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #6"); + checkRange(7, 2, `${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 7-9 #7"); + + password.value = `${kKanjiWithIVS}${kKanjiWithIVS}`; + editor.unmask(0, 2); + checkRange(0, 6, `${kKanjiWithIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 0-2"); + + editor.unmask(1, 2); + checkRange(0, 6, `${kMask}${kIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 1-2"); + + editor.unmask(2, 3); + checkRange(0, 6, `${kMask}${kIVS}${kMask}${kMask}${kMask}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 2-3"); + + editor.unmask(3, 5); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kKanjiWithIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 3-5"); + + editor.unmask(4, 5); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 4-5"); + + editor.unmask(5, 6); + checkRange(0, 6, `${kMask}${kMask}${kMask}${kMask}${kIVS}`, + "runQueryPasswordTest: Pairs of Kanji and IVS in password, unmasked range 5-6"); + + editor.mask(); +} + +function runQueryContentEventRelativeToInsertionPoint() +{ + textarea.focus(); + textarea.value = "0123456789"; + + // "[]0123456789" + let startOffset = textarea.selectionStart = textarea.selectionEnd = 0; + if (!checkContentRelativeToSelection(0, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#1") || + !checkContentRelativeToSelection(-1, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#2") || + !checkContentRelativeToSelection(1, 1, 1, "1", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#3") || + !checkContentRelativeToSelection(5, 10, 5, "56789", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#4") || + !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[0-0]", "#5")) { + return; + } + + // "[01234]56789" + textarea.selectionEnd = 5; + if (!checkContentRelativeToSelection(0, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#1") || + !checkContentRelativeToSelection(-1, 1, 0, "0", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#2") || + !checkContentRelativeToSelection(1, 1, 1, "1", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#3") || + !checkContentRelativeToSelection(5, 10, 5, "56789", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#4") || + !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[0-5]", "#5")) { + return; + } + + // "0123[]456789" + startOffset = textarea.selectionStart = textarea.selectionEnd = 4; + if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "4", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#1") || + !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#2") || + !checkContentRelativeToSelection(1, 1, startOffset + 1, "5", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#3") || + !checkContentRelativeToSelection(5, 10, startOffset + 5, "9", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#4") || + !checkContentRelativeToSelection(10, 1, 10, "", "runQueryContentEventRelativeToInsertionPoint[4-4]", "#5")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + // "0123[a]456789" + if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "a", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#1") || + !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#2") || + !checkContentRelativeToSelection(1, 1, startOffset + 1, "4", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#3") || + !checkContentRelativeToSelection(5, 10, startOffset + 5, "89", "runQueryContentEventRelativeToInsertionPoint[composition at 4]", "#4") || + !checkContentRelativeToSelection(11, 1, 11, "", "runQueryContentEventRelativeToInsertionPoint[composition at 4]")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeComposition({ type: "compositioncommitasis" }); + + // Move start of composition at first compositionupdate event. + function onCompositionUpdate(aEvent) + { + startOffset = textarea.selectionStart = textarea.selectionEnd = textarea.selectionStart - 1; + textarea.removeEventListener("compositionupdate", onCompositionUpdate); + } + textarea.addEventListener("compositionupdate", onCompositionUpdate); + + synthesizeCompositionChange( + { "composition": + { "string": "b", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + // "0123[b]a456789" + if (!checkContentRelativeToSelection(0, 1, startOffset + 0, "b", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#1") || + !checkContentRelativeToSelection(-1, 1, startOffset - 1, "3", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#2") || + !checkContentRelativeToSelection(1, 1, startOffset + 1, "a", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#3") || + !checkContentRelativeToSelection(5, 10, startOffset + 5, "789", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#4") || + !checkContentRelativeToSelection(12, 1, 12, "", "runQueryContentEventRelativeToInsertionPoint[composition at 3]", "#5")) { + synthesizeComposition({ type: "compositioncommitasis" }); + return; + } + + synthesizeComposition({ type: "compositioncommitasis" }); +} + +function runBug1375825Test() +{ + contenteditable.focus(); + + // #1 + contenteditable.innerHTML = "abc<span contenteditable=\"false\">defgh</span>"; + + let ret = synthesizeQueryTextRect(2, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (2, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "c", "runBug1375825Test #1 (2, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'c'"); + + ret = synthesizeQueryTextRect(3, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (3, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "d", "runBug1375825Test #1 (3, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'd'"); + + ret = synthesizeQueryTextRect(4, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (4, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "e", "runBug1375825Test #1 (4, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'e'"); + + ret = synthesizeQueryTextRect(5, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (5, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "f", "runBug1375825Test #1 (5, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'f'"); + + ret = synthesizeQueryTextRect(6, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (6, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "g", "runBug1375825Test #1 (6, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'g'"); + + ret = synthesizeQueryTextRect(7, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #1 (7, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "h", "runBug1375825Test #1 (7, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'h'"); + + // #2 + contenteditable.innerHTML = "abc<span style=\"user-select: all;\">defgh</span>"; + + ret = synthesizeQueryTextRect(2, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (2, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "c", "runBug1375825Test #2 (2, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'c'"); + + ret = synthesizeQueryTextRect(3, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (3, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "d", "runBug1375825Test #2 (3, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'd'"); + + ret = synthesizeQueryTextRect(4, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (4, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "e", "runBug1375825Test #2 (4, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'e'"); + + ret = synthesizeQueryTextRect(5, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (5, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "f", "runBug1375825Test #2 (5, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'f'"); + + ret = synthesizeQueryTextRect(6, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (6, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "g", "runBug1375825Test #2 (6, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'g'"); + + ret = synthesizeQueryTextRect(7, 1); + if (!checkQueryContentResult(ret, "runBug1375825Test #2 (7, 1), \"" + contenteditable.innerHTML + "\"")) { + return; + } + is(ret.text, "h", "runBug1375825Test #2 (7, 1), \"" + contenteditable.innerHTML + "\": should have queried a rect for 'h'"); +} + +function runBug1530649Test() +{ + // Vietnamese IME on macOS commits composition with typing space key. + // Then, typing new word shouldn't trim the trailing whitespace. + contenteditable.focus(); + contenteditable.innerHTML = ""; + synthesizeCompositionChange( + {composition: {string: "abc", clauses: [{length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: {start: 3, length: 0}}); + synthesizeComposition({type: "compositioncommit", data: "abc ", key: " "}); + + is(contenteditable.innerHTML, "abc <br>", + "runBug1530649Test: The trailing space shouldn't be removed"); + + synthesizeCompositionChange( + {composition: {string: "d", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: {start: 1, length: 0}}); + + is(contenteditable.innerHTML, "abc d<br>", + "runBug1530649Test: The new composition string shouldn't remove the last space"); + + synthesizeComposition({type: "compositioncommitasis", key: "KEY_Enter"}); + + is(contenteditable.innerHTML, "abc d<br>", + "runBug1530649Test: Committing the new composition string shouldn't remove the last space"); +} + +function runBug1571375Test() +{ + let selection = windowOfContenteditableBySpan.getSelection(); + let doc = document.getElementById("iframe7").contentDocument; + + contenteditableBySpan.focus(); + + contenteditableBySpan.innerHTML = "hello world"; + let range = doc.createRange(); + range.setStart(contenteditableBySpan.firstChild, 6); + range.setEnd(contenteditableBySpan.firstChild, 11); + selection.removeAllRanges(); + selection.addRange(range); + + synthesizeCompositionChange({ + composition: {string: "world", clauses: [{length: 5, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: { start: 5, length: 0 }, + }); + synthesizeComposition({type: "compositioncommit", data: "world", key: " "}); + is(contenteditableBySpan.innerHTML, "hello world", + "runBug1571375Test: space must not be removed by commit"); + + contenteditableBySpan.innerHTML = "hello world"; + range = doc.createRange(); + range.setStart(contenteditableBySpan.firstChild, 0); + range.setEnd(contenteditableBySpan.firstChild, 5); + selection.removeAllRanges(); + selection.addRange(range); + + synthesizeCompositionChange({ + composition: {string: "hello", clauses: [{length: 5, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: { start: 5, length: 0 }, + }); + synthesizeComposition({type: "compositioncommit", data: "hello", key: " "}); + is(contenteditableBySpan.innerHTML, "hello world", + "runBug1571375Test: space must not be removed by commit"); + + contenteditableBySpan.innerHTML = "hello world<div>.</div>"; + range = doc.createRange(); + range.setStart(contenteditableBySpan.firstChild, 6); + range.setEnd(contenteditableBySpan.firstChild, 11); + selection.removeAllRanges(); + selection.addRange(range); + + synthesizeCompositionChange({ + composition: {string: "world", clauses: [{length: 5, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: {start: 0, length: 0}} + ); + synthesizeComposition({type: "compositioncommit", data: "world", key: " "}); + is(contenteditableBySpan.innerHTML, "hello world<div>.</div>", + "runBug1571375Test: space must not be removed by commit"); +} + +async function runBug1584901Test() +{ + contenteditableBySpan.focus(); + contenteditableBySpan.innerHTML = ""; + + // XXX synthesizeCompositionChange won't work without wait. + await waitForTick(); + + synthesizeCompositionChange({ + composition: {string: "a ", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommitasis", key: " "}); + + is(contenteditableBySpan.innerHTML, "a ", + "runBug1584901Test: space must not be removed by composition change"); + + synthesizeCompositionChange({ + composition: {string: "b ", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommitasis", key: " "}); + + is(contenteditableBySpan.innerHTML, "a b ", + "runBug1584901Test: space must not be removed by composition change"); +} + +function runBug1675313Test() +{ + input.value = ""; + input.focus(); + let count = 0; + + function handler() { + input.focus(); + count++; + } + + input.addEventListener("keydown", handler); + input.addEventListener("keyup", handler); + + synthesizeCompositionChange({ + composition: { + string: "a", + clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + key: { key: "a", type: "keyup" }, + }, + }); + synthesizeCompositionChange({ + composition: { + string: "b", + clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + key: { key: "b", type: "keyup" }, + }, + }); + synthesizeComposition({type: "compositioncommitasis"}); + + is(count, 6, "runBug1675313Test: keydown event and keyup event are fired correctly"); + is(input.value, "b", + "runBug1675313Test: re-focus element doesn't commit composition if re-focus isn't click by user"); + + input.removeEventListener("keyup", handler); +} + +function runCommitCompositionWithSpaceKey() +{ + contenteditable.focus(); + contenteditable.innerHTML = ""; + + // Last white space might be if last child is no <br> + // Actually, our implementation will insert <br> element at last child, so + // white space will be ASCII space. + + synthesizeCompositionChange({ + composition: {string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommit", data: "a"}); + synthesizeKey(" "); + + is(contenteditable.innerHTML, "a <br>", + "runCommitCompositionWithSpaceKey: last single space should be kept"); + + synthesizeCompositionChange({ + composition: {string: "b", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommit", data: "b"}); + synthesizeKey(" "); + + is(contenteditable.innerHTML, "a b <br>", + "runCommitCompositionWithSpaceKey: inserting composition shouldn't remove last single space."); + + synthesizeCompositionChange({ + composition: {string: "c", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommit", data: "c"}); + synthesizeKey(" "); + + is(contenteditable.innerHTML, "a b c <br>", + "runCommitCompositionWithSpaceKey: inserting composition shouldn't remove last single space."); + + contenteditable.innerHTML = "a"; + windowOfContenteditable.getSelection().collapse(contenteditable.firstChild, contenteditable.firstChild.length); + is(contenteditable.innerHTML, "a", + "runCommitCompositionWithSpaceKey: contenteditable should be initialized with text ending with a space and without following <br> element"); + + synthesizeCompositionChange({ + composition: {string: "b", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + synthesizeComposition({type: "compositioncommit", data: "b ", key: { key: " ", code: "Space" }}); + + is(contenteditable.innerHTML, "ab <br>", + "runCommitCompositionWithSpaceKey: contenteditable should end with a padding <br> element after inserting commit string ending with a space"); +} + +function runCSSTransformTest() +{ + textarea.focus(); + textarea.value = "some text"; + textarea.selectionStart = textarea.selectionEnd = textarea.value.length; + let editorRect = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRect, + "runCSSTransformTest: editorRect")) { + return; + } + let firstCharRect = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstCharRect, + "runCSSTransformTest: firstCharRect")) { + return; + } + let lastCharRect = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRect, + "runCSSTransformTest: lastCharRect")) { + return; + } + let caretRect = synthesizeQueryCaretRect(textarea.selectionStart); + if (!checkQueryContentResult(caretRect, + "runCSSTransformTest: caretRect")) { + return; + } + let caretRectBeforeFirstChar = synthesizeQueryCaretRect(0); + if (!checkQueryContentResult(caretRectBeforeFirstChar, + "runCSSTransformTest: caretRectBeforeFirstChar")) { + return; + } + + try { + textarea.style.transform = "translate(10px, 15px)"; + function movedRect(aRect, aCSS_CX, aCSS_CY) + { + return { + left: aRect.left + Math.round(aCSS_CX * window.devicePixelRatio), + top: aRect.top + Math.round(aCSS_CY * window.devicePixelRatio), + width: aRect.width, + height: aRect.height + }; + } + + let editorRectTranslated = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRectTranslated, + "runCSSTransformTest: editorRectTranslated, " + textarea.style.transform) || + !checkRectFuzzy(editorRectTranslated, movedRect(editorRect, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: editorRectTranslated, " + textarea.style.transform)) { + return; + } + let firstCharRectTranslated = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstCharRectTranslated, + "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform) || + !checkRectFuzzy(firstCharRectTranslated, movedRect(firstCharRect, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform)) { + return; + } + let lastCharRectTranslated = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslated, + "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform) || + !checkRectFuzzy(lastCharRectTranslated, movedRect(lastCharRect, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform)) { + return; + } + let caretRectTranslated = synthesizeQueryCaretRect(textarea.selectionStart); + if (!checkQueryContentResult(caretRectTranslated, + "runCSSTransformTest: caretRectTranslated, " + textarea.style.transform) || + !checkRectFuzzy(caretRectTranslated, movedRect(caretRect, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: caretRectTranslated, " + textarea.style.transform)) { + return; + } + let caretRectBeforeFirstCharTranslated = synthesizeQueryCaretRect(0); + if (!checkQueryContentResult(caretRectBeforeFirstCharTranslated, + "runCSSTransformTest: caretRectBeforeFirstCharTranslated, " + textarea.style.transform) || + !checkRectFuzzy(caretRectBeforeFirstCharTranslated, movedRect(caretRectBeforeFirstChar, 10, 15), {left: 1, top: 1, width: 1, height: 1}, + "runCSSTransformTest: caretRectBeforeFirstCharTranslated, " + textarea.style.transform)) { + return; + } + let firstCharRectTranslatedAsArray = synthesizeQueryTextRectArray(0, 1); + if (!checkQueryContentResult(firstCharRectTranslatedAsArray, "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(firstCharRectTranslatedAsArray, [firstCharRectTranslated], "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + let lastCharRectTranslatedAsArray = synthesizeQueryTextRectArray(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslatedAsArray, "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(lastCharRectTranslatedAsArray, [lastCharRectTranslated], "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + + // XXX It's too difficult to check the result with scale and rotate... + // For now, let's check if query text rect and query text rect array returns same rect. + textarea.style.transform = "scale(1.5)"; + firstCharRectTranslated = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstCharRectTranslated, + "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform)) { + return; + } + lastCharRectTranslated = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslated, + "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform)) { + return; + } + firstCharRectTranslatedAsArray = synthesizeQueryTextRectArray(0, 1); + if (!checkQueryContentResult(firstCharRectTranslatedAsArray, "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(firstCharRectTranslatedAsArray, [firstCharRectTranslated], "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + lastCharRectTranslatedAsArray = synthesizeQueryTextRectArray(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslatedAsArray, "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(lastCharRectTranslatedAsArray, [lastCharRectTranslated], "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + + textarea.style.transform = "rotate(30deg)"; + firstCharRectTranslated = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstCharRectTranslated, + "runCSSTransformTest: firstCharRectTranslated, " + textarea.style.transform)) { + return; + } + lastCharRectTranslated = synthesizeQueryTextRect(textarea.value.length - 1, textarea.value.length); + if (!checkQueryContentResult(lastCharRectTranslated, + "runCSSTransformTest: lastCharRectTranslated, " + textarea.style.transform)) { + return; + } + firstCharRectTranslatedAsArray = synthesizeQueryTextRectArray(0, 1); + if (!checkQueryContentResult(firstCharRectTranslatedAsArray, "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform) || + !checkRectArray(firstCharRectTranslatedAsArray, [firstCharRectTranslated], "runCSSTransformTest: firstCharRectTranslatedAsArray, " + textarea.style.transform)) { + return; + } + lastCharRectTranslatedAsArray = synthesizeQueryTextRectArray(textarea.value.length - 1, textarea.value.length); + checkQueryContentResult(lastCharRectTranslatedAsArray, "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform); + checkRectArray(lastCharRectTranslatedAsArray, [lastCharRectTranslated], "runCSSTransformTest: lastCharRectTranslatedAsArray, " + textarea.style.transform); + } finally { + textarea.style.transform = ""; + } +} + +function runBug722639Test() +{ + textarea.focus(); + textarea.value = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; + textarea.value += textarea.value; + textarea.value += textarea.value; // 80 characters + + let firstLine = synthesizeQueryTextRect(0, 1); + if (!checkQueryContentResult(firstLine, + "runBug722639Test: firstLine")) { + return; + } + ok(true, "runBug722639Test: 1st line, top=" + firstLine.top + ", left=" + firstLine.left); + let firstLineAsArray = synthesizeQueryTextRectArray(0, 1); + if (!checkQueryContentResult(firstLineAsArray, "runBug722639Test: 1st line as array") || + !checkRectArray(firstLineAsArray, [firstLine], "runBug722639Test: 1st line as array should match with text rect result")) { + return; + } + if (kLFLen > 1) { + let firstLineLF = synthesizeQueryTextRect(1, 1); + if (!checkQueryContentResult(firstLineLF, + "runBug722639Test: firstLineLF")) { + return; + } + is(firstLineLF.top, firstLine.top, "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect"); + is(firstLineLF.left, firstLine.left, "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect"); + isfuzzy(firstLineLF.height, firstLine.height, 1, + "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect"); + is(firstLineLF.width, firstLine.width, "runBug722639Test: 1st line's \\n rect should be same as 1st line's \\r rect"); + let firstLineLFAsArray = synthesizeQueryTextRectArray(1, 1); + if (!checkQueryContentResult(firstLineLFAsArray, "runBug722639Test: 1st line's \\n rect as array") || + !checkRectArray(firstLineLFAsArray, [firstLineLF], "runBug722639Test: 1st line's rect as array should match with text rect result")) { + return; + } + } + let secondLine = synthesizeQueryTextRect(kLFLen, 1); + if (!checkQueryContentResult(secondLine, + "runBug722639Test: secondLine")) { + return; + } + ok(true, "runBug722639Test: 2nd line, top=" + secondLine.top + ", left=" + secondLine.left); + let secondLineAsArray = synthesizeQueryTextRectArray(kLFLen, 1); + if (!checkQueryContentResult(secondLineAsArray, "runBug722639Test: 2nd line as array") || + !checkRectArray(secondLineAsArray, [secondLine], "runBug722639Test: 2nd line as array should match with text rect result")) { + return; + } + if (kLFLen > 1) { + let secondLineLF = synthesizeQueryTextRect(kLFLen + 1, 1); + if (!checkQueryContentResult(secondLineLF, + "runBug722639Test: secondLineLF")) { + return; + } + is(secondLineLF.top, secondLine.top, "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect"); + is(secondLineLF.left, secondLine.left, "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect"); + isfuzzy(secondLineLF.height, secondLine.height, 1, + "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect"); + is(secondLineLF.width, secondLine.width, "runBug722639Test: 2nd line's \\n rect should be same as 2nd line's \\r rect"); + let secondLineLFAsArray = synthesizeQueryTextRectArray(kLFLen + 1, 1); + if (!checkQueryContentResult(secondLineLFAsArray, "runBug722639Test: 2nd line's \\n rect as array") || + !checkRectArray(secondLineLFAsArray, [secondLineLF], "runBug722639Test: 2nd line's rect as array should match with text rect result")) { + return; + } + } + let lineHeight = secondLine.top - firstLine.top; + ok(lineHeight > 0, + "runBug722639Test: lineHeight must be positive"); + is(secondLine.left, firstLine.left, + "runBug722639Test: the left value must be always same value"); + isfuzzy(secondLine.height, firstLine.height, 1, + "runBug722639Test: the height must be always same value"); + let previousTop = secondLine.top; + for (let i = 3; i <= textarea.value.length + 1; i++) { + let currentLine = synthesizeQueryTextRect(kLFLen * (i - 1), 1); + if (!checkQueryContentResult(currentLine, + "runBug722639Test: " + i + "th currentLine")) { + return; + } + ok(true, "runBug722639Test: " + i + "th line, top=" + currentLine.top + ", left=" + currentLine.left); + let currentLineAsArray = synthesizeQueryTextRectArray(kLFLen * (i - 1), 1); + if (!checkQueryContentResult(currentLineAsArray, "runBug722639Test: " + i + "th line as array") || + !checkRectArray(currentLineAsArray, [currentLine], "runBug722639Test: " + i + "th line as array should match with text rect result")) { + return; + } + // NOTE: the top position may be 1px larger or smaller than other lines + // due to sub pixel positioning. + if (Math.abs(currentLine.top - (previousTop + lineHeight)) <= 1) { + ok(true, "runBug722639Test: " + i + "th line's top is expected"); + } else { + is(currentLine.top, previousTop + lineHeight, + "runBug722639Test: " + i + "th line's top is unexpected"); + } + is(currentLine.left, firstLine.left, + "runBug722639Test: " + i + "th line's left is unexpected"); + isfuzzy(currentLine.height, firstLine.height, 1, + `runBug722639Test: ${i}th line's height is unexpected`); + if (kLFLen > 1) { + let currentLineLF = synthesizeQueryTextRect(kLFLen * (i - 1) + 1, 1); + if (!checkQueryContentResult(currentLineLF, + "runBug722639Test: " + i + "th currentLineLF")) { + return; + } + is(currentLineLF.top, currentLine.top, "runBug722639Test: " + i + "th line's \\n rect should be same as same line's \\r rect"); + is(currentLineLF.left, currentLine.left, "runBug722639Test: " + i + "th line's \\n rect should be same as same line's \\r rect"); + isfuzzy(currentLineLF.height, currentLine.height, 1, + `runBug722639Test: ${i}th line's \\n rect should be same as same line's \\r rect`); + is(currentLineLF.width, currentLine.width, "runBug722639Test: " + i + "th line's \\n rect should be same as same line's \\r rect"); + let currentLineLFAsArray = synthesizeQueryTextRectArray(kLFLen * (i - 1) + 1, 1); + if (!checkQueryContentResult(currentLineLFAsArray, "runBug722639Test: " + i + "th line's \\n rect as array") || + !checkRectArray(currentLineLFAsArray, [currentLineLF], "runBug722639Test: " + i + "th line's rect as array should match with text rect result")) { + return; + } + } + previousTop = currentLine.top; + } +} + +function runCompositionWithSelectionChange() { + function doTest(aEditor, aDescription) { + aEditor.focus(); + const isHTMLEditor = + aEditor.nodeName.toLowerCase() != "input" && aEditor.nodeName.toLowerCase() != "textarea"; + const win = isHTMLEditor ? windowOfContenteditable : window; + function getValue() { + return isHTMLEditor ? aEditor.innerHTML : aEditor.value; + } + function setSelection(aStart, aLength) { + if (isHTMLEditor) { + win.getSelection().setBaseAndExtent(aEditor.firstChild, aStart, aEditor.firstChild, aStart + aLength); + } else { + aEditor.setSelectionRange(aStart, aStart + aLength); + } + } + + if (isHTMLEditor) { + aEditor.innerHTML = "abcxyz"; + } else { + aEditor.value = "abcxyz"; + } + setSelection("abc".length, 0); + + synthesizeCompositionChange({ + composition: { + string: "1", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + caret: { start: 1, length: 0 }, + } + }); + + is(getValue(), "abc1xyz", + `${aDescription}: First composing character should be inserted middle of the text`); + + aEditor.addEventListener("compositionupdate", () => { + setSelection("abc".length, "1".length); + }, {once: true}); + + synthesizeCompositionChange({ + composition: { + string: "12", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + caret: { start: 2, length: 0 }, + } + }); + + is(getValue(), "abc12xyz", + `${aDescription}: Only composition string should be updated even if selection range is updated by "compositionupdate" event listener`); + + aEditor.addEventListener("compositionupdate", () => { + setSelection("abc1".length, "2d".length); + }, {once: true}); + + synthesizeCompositionChange({ + composition: { + string: "123", + clauses: [{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + caret: { start: 3, length: 0 }, + } + }); + + is(getValue(), "abc123xyz", + `${aDescription}: Only composition string should be updated even if selection range wider than composition string is updated by "compositionupdate" event listener`); + + aEditor.addEventListener("compositionupdate", () => { + setSelection("ab".length, "c123d".length); + }, {once: true}); + + synthesizeCompositionChange({ + composition: { + string: "456", + clauses: [{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + caret: { start: 3, length: 0 }, + } + }); + + is(getValue(), "abc456xyz", + `${aDescription}: Only composition string should be updated even if selection range which covers all over the composition string is updated by "compositionupdate" event listener`); + + aEditor.addEventListener("beforeinput", () => { + setSelection("abc456d".length, 0); + }, {once: true}); + + synthesizeComposition({ type: "compositioncommitasis" }); + + is(getValue(), "abc456xyz", + `${aDescription}: Only composition string should be updated when committing composition but selection is updated by "beforeinput" event listener`); + if (isHTMLEditor) { + is(win.getSelection().focusNode, aEditor.firstChild, + `${aDescription}: The focus node after composition should be the text node`); + is(win.getSelection().focusOffset, "abc456".length, + `${aDescription}: The focus offset after composition should be end of the composition string`); + is(win.getSelection().anchorNode, aEditor.firstChild, + `${aDescription}: The anchor node after composition should be the text node`); + is(win.getSelection().anchorOffset, "abc456".length, + `${aDescription}: The anchor offset after composition should be end of the composition string`); + } else { + is(aEditor.selectionStart, "abc456".length, + `${aDescription}: The selectionStart after composition should be end of the composition string`); + is(aEditor.selectionEnd, "abc456".length, + `${aDescription}: The selectionEnd after composition should be end of the composition string`); + } + } + doTest(textarea, "runCompositionWithSelectionChange(textarea)"); + doTest(input, "runCompositionWithSelectionChange(input)"); + doTest(contenteditable, "runCompositionWithSelectionChange(contenteditable)"); +} + +function runForceCommitTest() +{ + let events; + function eventHandler(aEvent) + { + events.push(aEvent); + } + window.addEventListener("compositionstart", eventHandler, true); + window.addEventListener("compositionupdate", eventHandler, true); + window.addEventListener("compositionend", eventHandler, true); + window.addEventListener("beforeinput", eventHandler, true); + window.addEventListener("input", eventHandler, true); + window.addEventListener("text", eventHandler, true); + + // Make the composition in textarea commit by click in the textarea + textarea.focus(); + textarea.value = ""; + + events = []; + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + is(events.length, 5, + "runForceCommitTest: wrong event count #1"); + is(events[0].type, "compositionstart", + "runForceCommitTest: the 1st event must be compositionstart #1"); + is(events[1].type, "compositionupdate", + "runForceCommitTest: the 2nd event must be compositionupdate #1"); + is(events[2].type, "text", + "runForceCommitTest: the 3rd event must be text #1"); + is(events[3].type, "beforeinput", + "runForceCommitTest: the 4th event must be beforeinput #1"); + checkInputEvent(events[3], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #1"); + is(events[4].type, "input", + "runForceCommitTest: the 5th event must be input #1"); + checkInputEvent(events[4], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #1"); + + events = []; + synthesizeMouseAtCenter(textarea, {}); + + is(events.length, 4, + "runForceCommitTest: wrong event count #2"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #2"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #2`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #2"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #2`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #2"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #2"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #2`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #2"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #2"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #2`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #2"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #2"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #2"); + + // Make the composition in textarea commit by click in another editor (input) + textarea.focus(); + textarea.value = ""; + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + synthesizeMouseAtCenter(input, {}); + + is(events.length, 4, + "runForceCommitTest: wrong event count #3"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #3"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #3`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #3"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #3`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #3"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #3"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #3`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #3"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #3"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #3`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #3"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #3"); + ok(!getEditor(input).isComposing, + "runForceCommitTest: the input has composition #3"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #3"); + is(input.value, "", + "runForceCommitTest: the input has the committed text? #3"); + + // Make the composition in textarea commit by blur() + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + textarea.blur(); + + is(events.length, 4, + "runForceCommitTest: wrong event count #4"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #4"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #4`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #4"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #4`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #4"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #4"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #4`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #4"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #4"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #4`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #4"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #4"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #4"); + + // Make the composition in textarea commit by input.focus() + textarea.focus(); + textarea.value = ""; + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + input.focus(); + + is(events.length, 4, + "runForceCommitTest: wrong event count #5"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #5"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #5`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #5"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #5`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #5"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #5"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #5`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #5"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #5"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #5`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #5"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #5"); + ok(!getEditor(input).isComposing, + "runForceCommitTest: the input has composition #5"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #5"); + is(input.value, "", + "runForceCommitTest: the input has the committed text? #5"); + + // Make the composition in textarea commit by click in another document's editor + textarea.focus(); + textarea.value = ""; + textareaInFrame.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + synthesizeMouseAtCenter(textareaInFrame, {}, iframe.contentWindow); + + is(events.length, 4, + "runForceCommitTest: wrong event count #6"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #6"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #6`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #6"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #6`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #6"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #6"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #6`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #6"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #6"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #6`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #6"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #6"); + ok(!getEditor(textareaInFrame).isComposing, + "runForceCommitTest: the textarea in frame has composition #6"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #6"); + is(textareaInFrame.value, "", + "runForceCommitTest: the textarea in frame has the committed text? #6"); + + // Make the composition in textarea commit by another document's editor's focus() + textarea.focus(); + textarea.value = ""; + textareaInFrame.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + textareaInFrame.focus(); + + is(events.length, 4, + "runForceCommitTest: wrong event count #7"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #7"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #7`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #7"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #7`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #7"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #7"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #7`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #7"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #7"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #7`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #7"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #7"); + ok(!getEditor(textareaInFrame).isComposing, + "runForceCommitTest: the textarea in frame has composition #7"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #7"); + is(textareaInFrame.value, "", + "runForceCommitTest: the textarea in frame has the committed text? #7"); + + // Make the composition in a textarea commit by click in another editable document + textarea.focus(); + textarea.value = ""; + iframe2.contentDocument.body.innerHTML = "Text in the Body"; + let iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + synthesizeMouseAtCenter(iframe2.contentDocument.body, {}, iframe2.contentWindow); + + is(events.length, 4, + "runForceCommitTest: wrong event count #8"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #8"); + is(events[0].target, textarea, + `runForceCommitTest: The ${events[0].type} event was fired on wrong event target #8`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #8"); + is(events[1].target, textarea, + `runForceCommitTest: The ${events[1].type} event was fired on wrong event target #8`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #8"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #8"); + is(events[2].target, textarea, + `runForceCommitTest: The ${events[2].type} event was fired on wrong event target #8`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #8"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #8"); + is(events[3].target, textarea, + `runForceCommitTest: The ${events[3].type} event was fired on wrong event target #8`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #8"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #8"); + ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing, + "runForceCommitTest: the editable document has composition #8"); + is(textarea.value, "\u306E", + "runForceCommitTest: the textarea doesn't have the committed text #8"); + is(iframe2.contentDocument.body.innerHTML, iframe2BodyInnerHTML, + "runForceCommitTest: the editable document has the committed text? #8"); + + // Make the composition in an editable document commit by click in it + iframe2.contentWindow.focus(); + iframe2.contentDocument.body.innerHTML = "Text in the Body"; + iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }, iframe2.contentWindow); + + events = []; + synthesizeMouseAtCenter(iframe2.contentDocument.body, {}, iframe2.contentWindow); + + is(events.length, 4, + "runForceCommitTest: wrong event count #9"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #9"); + is(events[0].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #9`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #9"); + is(events[1].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #9`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", + [{startContainer: iframe2.contentDocument.body.firstChild, + startOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E"), + endContainer: iframe2.contentDocument.body.firstChild, + endOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E") + 1}], + "runForceCommitTest #9"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #9"); + is(events[2].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #9`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #9"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #9"); + is(events[3].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #9`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #9"); + ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing, + "runForceCommitTest: the editable document still has composition #9"); + ok(iframe2.contentDocument.body.innerHTML != iframe2BodyInnerHTML && + iframe2.contentDocument.body.innerHTML.includes("\u306E"), + "runForceCommitTest: the editable document doesn't have the committed text #9"); + + // Make the composition in an editable document commit by click in another document's editor + textarea.value = ""; + iframe2.contentWindow.focus(); + iframe2.contentDocument.body.innerHTML = "Text in the Body"; + iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }, iframe2.contentWindow); + + events = []; + synthesizeMouseAtCenter(textarea, {}); + + is(events.length, 4, + "runForceCommitTest: wrong event count #10"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #10"); + is(events[0].target, iframe2.contentDocument.body, + `runForceCommitTest: The ${events[0].type} event was fired on wrong event target #10`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #10"); + is(events[1].target, iframe2.contentDocument.body, + `runForceCommitTest: The ${events[1].type} event was fired on wrong event target #10`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", + [{startContainer: iframe2.contentDocument.body.firstChild, + startOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E"), + endContainer: iframe2.contentDocument.body.firstChild, + endOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E") + 1}], + "runForceCommitTest #10"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #10"); + is(events[2].target, iframe2.contentDocument.body, + `runForceCommitTest: The ${events[2].type} event was fired on wrong event target #10`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #10"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #10"); + is(events[3].target, iframe2.contentDocument.body, + `runForceCommitTest: The ${events[3].type} event was fired on wrong event target #10`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #10"); + ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing, + "runForceCommitTest: the editable document still has composition #10"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea has composition #10"); + ok(iframe2.contentDocument.body.innerHTML != iframe2BodyInnerHTML && + iframe2.contentDocument.body.innerHTML.includes("\u306E"), + "runForceCommitTest: the editable document doesn't have the committed text #10"); + is(textarea.value, "", + "runForceCommitTest: the textarea has the committed text? #10"); + + // Make the composition in an editable document commit by click in the another editable document + iframe2.contentWindow.focus(); + iframe2.contentDocument.body.innerHTML = "Text in the Body"; + iframe2BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + iframe3.contentDocument.body.innerHTML = "Text in the Body"; + let iframe3BodyInnerHTML = iframe2.contentDocument.body.innerHTML; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }, iframe2.contentWindow); + + events = []; + synthesizeMouseAtCenter(iframe3.contentDocument.body, {}, iframe3.contentWindow); + + is(events.length, 4, + "runForceCommitTest: wrong event count #11"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #11"); + is(events[0].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #11`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #11"); + is(events[1].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #11`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", + [{startContainer: iframe2.contentDocument.body.firstChild, + startOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E"), + endContainer: iframe2.contentDocument.body.firstChild, + endOffset: iframe2.contentDocument.body.firstChild.wholeText.indexOf("\u306E") + 1}], + "runForceCommitTest #11"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #11"); + is(events[2].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #11`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #11"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #11"); + is(events[3].target, iframe2.contentDocument.body, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #11`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #11"); + ok(!getHTMLEditorIMESupport(iframe2.contentWindow).isComposing, + "runForceCommitTest: the editable document still has composition #11"); + ok(!getHTMLEditorIMESupport(iframe3.contentWindow).isComposing, + "runForceCommitTest: the other editable document has composition #11"); + ok(iframe2.contentDocument.body.innerHTML != iframe2BodyInnerHTML && + iframe2.contentDocument.body.innerHTML.includes("\u306E"), + "runForceCommitTest: the editable document doesn't have the committed text #11"); + is(iframe3.contentDocument.body.innerHTML, iframe3BodyInnerHTML, + "runForceCommitTest: the other editable document has the committed text? #11"); + + input.focus(); + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + input.value = "set value"; + + is(events.length, 4, + "runForceCommitTest: wrong event count #12"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #12"); + is(events[0].target, input, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #12`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #12"); + is(events[1].target, input, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #12`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #12"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #12"); + is(events[2].target, input, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #12`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #12"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #12"); + is(events[3].target, input, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #12`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #12"); + ok(!getEditor(input).isComposing, + "runForceCommitTest: the input still has composition #12"); + is(input.value, "set value", + "runForceCommitTest: the input doesn't have the set text #12"); + + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + textarea.value = "set value"; + + is(events.length, 4, + "runForceCommitTest: wrong event count #13"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #13"); + is(events[0].target, textarea, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #13`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #13"); + is(events[1].target, textarea, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #13`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #13"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #13"); + is(events[2].target, textarea, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #13`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #13"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #13"); + is(events[3].target, textarea, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #13`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #13"); + ok(!getEditor(textarea).isComposing, + "runForceCommitTest: the textarea still has composition #13"); + is(textarea.value, "set value", + "runForceCommitTest: the textarea doesn't have the set text #13"); + + input.focus(); + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + input.value += " appended value"; + + is(events.length, 4, + "runForceCommitTest: wrong event count #14"); + is(events[0].type, "text", + "runForceCommitTest: the 1st event must be text #14"); + is(events[0].target, input, + `runForceCommitTest: The "${events[0].type}" event was fired on wrong event target #14`); + is(events[1].type, "beforeinput", + "runForceCommitTest: the 2nd event must be beforeinput #14"); + is(events[1].target, input, + `runForceCommitTest: The "${events[1].type}" event was fired on wrong event target #14`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runForceCommitTest #14"); + is(events[2].type, "compositionend", + "runForceCommitTest: the 3rd event must be compositionend #14"); + is(events[2].target, input, + `runForceCommitTest: The "${events[2].type}" event was fired on wrong event target #14`); + is(events[2].data, "\u306E", + "runForceCommitTest: compositionend has wrong data #14"); + is(events[3].type, "input", + "runForceCommitTest: the 4th event must be input #14"); + is(events[3].target, input, + `runForceCommitTest: The "${events[3].type}" event was fired on wrong event target #14`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runForceCommitTest #14"); + ok(!getEditor(input).isComposing, + "runForceCommitTest: the input still has composition #14"); + is(input.value, "\u306E appended value", + "runForceCommitTest: the input should have both composed text and appended text #14"); + + input.focus(); + input.value = "abcd"; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + input.value = "abcd\u306E"; + + is(events.length, 0, + "runForceCommitTest: setting same value to input with composition shouldn't cause any events #15"); + is(input.value, "abcd\u306E", + "runForceCommitTest: the input has unexpected value #15"); + + input.blur(); // commit composition + + textarea.focus(); + textarea.value = "abcd"; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + textarea.value = "abcd\u306E"; + + is(events.length, 0, + "runForceCommitTest: setting same value to textarea with composition shouldn't cause any events #16"); + is(textarea.value, "abcd\u306E", + "runForceCommitTest: the input has unexpected value #16"); + + textarea.blur(); // commit composition + + window.removeEventListener("compositionstart", eventHandler, true); + window.removeEventListener("compositionupdate", eventHandler, true); + window.removeEventListener("compositionend", eventHandler, true); + window.removeEventListener("beforeinput", eventHandler, true); + window.removeEventListener("input", eventHandler, true); + window.removeEventListener("text", eventHandler, true); +} + +function runNestedSettingValue() +{ + let isTesting = false; + let events = []; + function eventHandler(aEvent) + { + events.push(aEvent); + if (isTesting) { + aEvent.target.value += aEvent.type + ", "; + } + } + window.addEventListener("compositionstart", eventHandler, true); + window.addEventListener("compositionupdate", eventHandler, true); + window.addEventListener("compositionend", eventHandler, true); + window.addEventListener("beforeinput", eventHandler, true); + window.addEventListener("input", eventHandler, true); + window.addEventListener("text", eventHandler, true); + + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + isTesting = true; + textarea.value = "first setting value, "; + isTesting = false; + + is(events.length, 4, + "runNestedSettingValue: wrong event count #1"); + is(events[0].type, "text", + "runNestedSettingValue: the 1st event must be text #1"); + is(events[0].target, textarea, + `runNestedSettingValue: The "${events[0].type}" event was fired on wrong event target #1`); + is(events[1].type, "beforeinput", + "runNestedSettingValue: the 2nd event must be beforeinput #1"); + is(events[1].target, textarea, + `runNestedSettingValue: The "${events[1].type}" event was fired on wrong event target #1`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #1"); + is(events[2].type, "compositionend", + "runNestedSettingValue: the 3rd event must be compositionend #1"); + is(events[2].target, textarea, + `runNestedSettingValue: The "${events[2].type}" event was fired on wrong event target #1`); + is(events[2].data, "\u306E", + "runNestedSettingValue: compositionend has wrong data #1"); + is(events[3].type, "input", + "runNestedSettingValue: the 4th event must be input #1"); + is(events[3].target, textarea, + `runNestedSettingValue: The "${events[3].type}" event was fired on wrong event target #1`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #1"); + ok(!getEditor(textarea).isComposing, + "runNestedSettingValue: the textarea still has composition #1"); + is(textarea.value, "first setting value, text, beforeinput, compositionend, input, ", + "runNestedSettingValue: the textarea should have all string set to value attribute"); + + input.focus(); + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + isTesting = true; + input.value = "first setting value, "; + isTesting = false; + + is(events.length, 4, + "runNestedSettingValue: wrong event count #2"); + is(events[0].type, "text", + "runNestedSettingValue: the 1st event must be text #2"); + is(events[0].target, input, + `runNestedSettingValue: The "${events[0].type}" event was fired on wrong event target #2`); + is(events[1].type, "beforeinput", + "runNestedSettingValue: the 2nd event must be beforeinput #2"); + is(events[1].target, input, + `runNestedSettingValue: The "${events[1].type}" event was fired on wrong event target #2`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #2"); + is(events[2].type, "compositionend", + "runNestedSettingValue: the 3rd event must be compositionend #2"); + is(events[2].target, input, + `runNestedSettingValue: The "${events[2].type}" event was fired on wrong event target #2`); + is(events[2].data, "\u306E", + "runNestedSettingValue: compositionend has wrong data #2"); + is(events[3].type, "input", + "runNestedSettingValue: the 4th event must be input #2"); + is(events[3].target, input, + `runNestedSettingValue: The "${events[3].type}" event was fired on wrong event target #2`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #2"); + ok(!getEditor(input).isComposing, + "runNestedSettingValue: the input still has composition #2"); + is(textarea.value, "first setting value, text, beforeinput, compositionend, input, ", + "runNestedSettingValue: the input should have all string set to value attribute #2"); + + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + isTesting = true; + textarea.setRangeText("first setting value, "); + isTesting = false; + + is(events.length, 4, + "runNestedSettingValue: wrong event count #3"); + is(events[0].type, "text", + "runNestedSettingValue: the 1st event must be text #3"); + is(events[0].target, textarea, + `runNestedSettingValue: The ${events[0].type} event was fired on wrong event target #3`); + is(events[1].type, "beforeinput", + "runNestedSettingValue: the 2nd event must be beforeinput #3"); + is(events[1].target, textarea, + `runNestedSettingValue: The ${events[1].type} event was fired on wrong event target #3`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #3"); + is(events[2].type, "compositionend", + "runNestedSettingValue: the 3rd event must be compositionend #3"); + is(events[2].target, textarea, + `runNestedSettingValue: The ${events[2].type} event was fired on wrong event target #3`); + is(events[2].data, "\u306E", + "runNestedSettingValue: compositionend has wrong data #3"); + is(events[3].type, "input", + "runNestedSettingValue: the 4th event must be input #3"); + is(events[3].target, textarea, + `runNestedSettingValue: The ${events[3].type} event was fired on wrong event target #3`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #3"); + ok(!getEditor(textarea).isComposing, + "runNestedSettingValue: the textarea still has composition #3"); + is(textarea.value, "\u306Efirst setting value, text, beforeinput, compositionend, input, ", + "runNestedSettingValue: the textarea should have appended by setRangeText() and all string set to value attribute #3"); + + input.focus(); + input.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + events = []; + isTesting = true; + input.setRangeText("first setting value, "); + isTesting = false; + + is(events.length, 4, + "runNestedSettingValue: wrong event count #4"); + is(events[0].type, "text", + "runNestedSettingValue: the 1st event must be text #4"); + is(events[0].target, input, + `runNestedSettingValue: The "${events[0].type}" event was fired on wrong event target #4`); + is(events[1].type, "beforeinput", + "runNestedSettingValue: the 2nd event must be beforeinput #4"); + is(events[1].target, input, + `runNestedSettingValue: The "${events[1].type}" event was fired on wrong event target #4`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #4"); + is(events[2].type, "compositionend", + "runNestedSettingValue: the 3rd event must be compositionend #4"); + is(events[2].target, input, + `runNestedSettingValue: The "${events[2].type}" event was fired on wrong event target #4`); + is(events[2].data, "\u306E", + "runNestedSettingValue: compositionend has wrong data #4"); + is(events[3].type, "input", + "runNestedSettingValue: the 4th event must be input #4"); + is(events[3].target, input, + `runNestedSettingValue: The "${events[3].type}" event was fired on wrong event target #4`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runNestedSettingValue #4"); + ok(!getEditor(input).isComposing, + "runNestedSettingValue: the input still has composition #4"); + is(textarea.value, "\u306Efirst setting value, text, beforeinput, compositionend, input, ", + "runNestedSettingValue: the input should have all string appended by setRangeText() and set to value attribute #4"); + + window.removeEventListener("compositionstart", eventHandler, true); + window.removeEventListener("compositionupdate", eventHandler, true); + window.removeEventListener("compositionend", eventHandler, true); + window.removeEventListener("beforeinput", eventHandler, true); + window.removeEventListener("input", eventHandler, true); + window.removeEventListener("text", eventHandler, true); + +} + +async function runAsyncForceCommitTest() +{ + let events; + function eventHandler(aEvent) + { + events.push(aEvent); + }; + + // If IME commits composition for a request, TextComposition commits + // composition automatically because most web apps must expect that active + // composition should be committed synchronously. Therefore, in this case, + // a click during composition should cause committing composition + // synchronously and delayed commit shouldn't cause composition events. + let commitRequested = false; + let onFinishTest = null; + function callback(aTIP, aNotification) + { + ok(true, aNotification.type); + if (aNotification.type != "request-to-commit") { + return true; + } + commitRequested = true; + if (onFinishTest) { + let resolve = onFinishTest; + onFinishTest = null; + + SimpleTest.executeSoon(() => { + events = []; + aTIP.commitComposition(); + + is(events.length, 0, + "runAsyncForceCommitTest: composition events shouldn't been fired by asynchronous call of nsITextInputProcessor.commitComposition()"); + + SimpleTest.executeSoon(resolve); + }); + } + return true; + }; + + function promiseCleanUp() { + return new Promise(resolve => { onFinishTest = resolve; }); + } + + window.addEventListener("compositionstart", eventHandler, true); + window.addEventListener("compositionupdate", eventHandler, true); + window.addEventListener("compositionend", eventHandler, true); + window.addEventListener("beforeinput", eventHandler, true); + window.addEventListener("input", eventHandler, true); + window.addEventListener("text", eventHandler, true); + + // Make the composition in textarea commit by click in the textarea + textarea.focus(); + textarea.value = ""; + + events = []; + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }, window, callback); + + is(events.length, 5, + "runAsyncForceCommitTest: wrong event count #1"); + is(events[0].type, "compositionstart", + "runAsyncForceCommitTest: the 1st event must be compositionstart #1"); + is(events[1].type, "compositionupdate", + "runAsyncForceCommitTest: the 2nd event must be compositionupdate #1"); + is(events[2].type, "text", + "runAsyncForceCommitTest: the 3rd event must be text #1"); + is(events[3].type, "beforeinput", + "runAsyncForceCommitTest: the 4th event must be beforeinput #1"); + checkInputEvent(events[3], true, "insertCompositionText", "\u306E", [], + "runAsyncForceCommitTest #1"); + is(events[4].type, "input", + "runAsyncForceCommitTest: the 5th event must be input #1"); + checkInputEvent(events[4], true, "insertCompositionText", "\u306E", [], + "runAsyncForceCommitTest #1"); + + events = []; + let waitCleanState = promiseCleanUp(); + + synthesizeMouseAtCenter(textarea, {}); + + ok(commitRequested, + "runAsyncForceCommitTest: \"request-to-commit\" should've been notified"); + is(events.length, 4, + "runAsyncForceCommitTest: wrong event count #2"); + is(events[0].type, "text", + "runAsyncForceCommitTest: the 1st event must be text #2"); + is(events[0].target, textarea, + `runAsyncForceCommitTest: The "${events[0].type}" event was fired on wrong event target #2`); + is(events[1].type, "beforeinput", + "runAsyncForceCommitTest: the 2nd event must be beforeinput #2"); + is(events[1].target, textarea, + `runAsyncForceCommitTest: The "${events[1].type}" event was fired on wrong event target #2`); + checkInputEvent(events[1], true, "insertCompositionText", "\u306E", [], + "runAsyncForceCommitTest #2"); + is(events[2].type, "compositionend", + "runAsyncForceCommitTest: the 3rd event must be compositionend #2"); + is(events[2].target, textarea, + `runAsyncForceCommitTest: The "${events[2].type}" event was fired on wrong event target #2`); + is(events[2].data, "\u306E", + "runAsyncForceCommitTest: compositionend has wrong data #2"); + is(events[3].type, "input", + "runAsyncForceCommitTest: the 4th event must be input #2"); + is(events[3].target, textarea, + `runAsyncForceCommitTest: The "${events[3].type}" event was fired on wrong event target #2`); + checkInputEvent(events[3], false, "insertCompositionText", "\u306E", [], + "runAsyncForceCommitTest #2"); + ok(!getEditor(textarea).isComposing, + "runAsyncForceCommitTest: the textarea still has composition #2"); + is(textarea.value, "\u306E", + "runAsyncForceCommitTest: the textarea doesn't have the committed text #2"); + + await waitCleanState; + + window.removeEventListener("compositionstart", eventHandler, true); + window.removeEventListener("compositionupdate", eventHandler, true); + window.removeEventListener("compositionend", eventHandler, true); + window.removeEventListener("beforeinput", eventHandler, true); + window.removeEventListener("input", eventHandler, true); + window.removeEventListener("text", eventHandler, true); +} + +function runBug811755Test() +{ + iframe2.contentDocument.body.innerHTML = "<div>content<br/></div>"; + iframe2.contentWindow.focus(); + // Query everything + let textContent = synthesizeQueryTextContent(0, 10); + if (!checkQueryContentResult(textContent, "runBug811755Test: synthesizeQueryTextContent #1")) { + return; + } + // Query everything but specify exact end offset, which should be immediately after the <br> node + // If PreContentIterator is used, the next node after <br> is the node after </div>. + // If ContentIterator is used, the next node is the <div> node itself. In this case, the end + // node ends up being before the start node, and an empty string is returned. + let queryContent = synthesizeQueryTextContent(0, textContent.text.length); + if (!checkQueryContentResult(queryContent, "runBug811755Test: synthesizeQueryTextContent #2")) { + return; + } + is(queryContent.text, textContent.text, "runBug811755Test: two queried texts don't match"); +} + +function runIsComposingTest() +{ + let expectedIsComposing = false; + let description = ""; + + function eventHandler(aEvent) + { + if (aEvent.type == "keydown" || aEvent.type == "keyup") { + is(aEvent.isComposing, expectedIsComposing, + "runIsComposingTest: " + description + " (type=" + aEvent.type + ", key=" + aEvent.key + ")"); + } else if (aEvent.type == "keypress") { + // keypress event shouldn't be fired during composition so that isComposing should be always false. + is(aEvent.isComposing, false, + "runIsComposingTest: " + description + " (type=" + aEvent.type + ")"); + } else { + checkInputEvent(aEvent, expectedIsComposing, "insertCompositionText", "\u3042", [], + `runIsComposingTest: ${description}`); + } + } + + function onComposition(aEvent) + { + if (aEvent.type == "compositionstart") { + expectedIsComposing = true; + } else if (aEvent.type == "compositionend") { + expectedIsComposing = false; + } + } + + textarea.addEventListener("keydown", eventHandler, true); + textarea.addEventListener("keypress", eventHandler, true); + textarea.addEventListener("keyup", eventHandler, true); + textarea.addEventListener("beforeinput", eventHandler, true); + textarea.addEventListener("input", eventHandler, true); + textarea.addEventListener("compositionstart", onComposition, true); + textarea.addEventListener("compositionend", onComposition, true); + + textarea.focus(); + textarea.value = ""; + + // XXX These cases shouldn't occur in actual native key events because we + // don't dispatch key events while composition (bug 354358). + description = "events before dispatching compositionstart"; + synthesizeKey("KEY_ArrowLeft"); + + description = "events after dispatching compositionchange"; + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }, + }); + + // Although, firing keypress event during composition is a bug. + synthesizeKey("KEY_Insert"); + + description = "events for committing composition string"; + + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Enter", code: "Enter", type: "keydown" } }); + + // input event will be fired by synthesizing compositionend event. + // Then, its isComposing should be false. + description = "events after dispatching compositioncommitasis"; + synthesizeKey("KEY_Enter", {type: "keyup"}); + + textarea.removeEventListener("keydown", eventHandler, true); + textarea.removeEventListener("keypress", eventHandler, true); + textarea.removeEventListener("keyup", eventHandler, true); + textarea.removeEventListener("beforeinput", eventHandler, true); + textarea.removeEventListener("input", eventHandler, true); + textarea.removeEventListener("compositionstart", onComposition, true); + textarea.removeEventListener("compositionend", onComposition, true); + + textarea.value = ""; +} + +function runRedundantChangeTest() +{ + textarea.focus(); + + let result = []; + function clearResult() + { + result = []; + } + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + textarea.addEventListener("beforeinput", handler, true); + textarea.addEventListener("input", handler, true); + textarea.addEventListener("text", handler, true); + + textarea.value = ""; + + // synthesize change event + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + is(result.length, 4, + "runRedundantChangeTest: 4 events should be fired after synthesizing composition change #1"); + is(result[0].type, "compositionupdate", + "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #1"); + is(result[1].type, "text", + "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #1"); + is(result[2].type, "beforeinput", + "runRedundantChangeTest: beforeinput should be fired after synthesizing composition change #1"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3042", [], + "runRedundantChangeTest: after synthesizing composition change #1"); + is(result[3].type, "input", + "runRedundantChangeTest: input should be fired after synthesizing composition change #1"); + checkInputEvent(result[3], true, "insertCompositionText", "\u3042", [], + "runRedundantChangeTest: after synthesizing composition change #1"); + is(textarea.value, "\u3042", "runRedundantChangeTest: textarea has uncommitted string #1"); + + // synthesize another change event + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042\u3044", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + is(result.length, 4, + "runRedundantChangeTest: 4 events should be fired after synthesizing composition change #2"); + is(result[0].type, "compositionupdate", + "runRedundantChangeTest: compositionupdate should be fired after synthesizing composition change #2"); + is(result[1].type, "text", + "runRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string #2"); + is(result[2].type, "beforeinput", + "runRedundantChangeTest: beforeinput should be fired after synthesizing composition change #2"); + checkInputEvent(result[2], true, "insertCompositionText", "\u3042\u3044", [], + "runRedundantChangeTest: after synthesizing composition change #2"); + is(result[3].type, "input", + "runRedundantChangeTest: input should be fired after synthesizing composition change #2"); + checkInputEvent(result[3], true, "insertCompositionText", "\u3042\u3044", [], + "runRedundantChangeTest: after synthesizing composition change #2"); + is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #2"); + + // synthesize same change event again + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "\u3042\u3044", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + is(result.length, 0, "runRedundantChangeTest: no events should be fired after synthesizing composition change again"); + is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has uncommitted string #3"); + + // synthesize commit-as-is + clearResult(); + synthesizeComposition({ type: "compositioncommitasis" }); + is(result.length, 4, + "runRedundantChangeTest: 4 events should be fired after synthesizing composition commit-as-is"); + is(result[0].type, "text", + "runRedundantChangeTest: text should be fired after synthesizing composition commit-as-is for removing the ranges"); + is(result[1].type, "beforeinput", + "runRedundantChangeTest: beforeinput should be fired after synthesizing composition commit-as-is for removing the ranges"); + checkInputEvent(result[1], true, "insertCompositionText", "\u3042\u3044", [], + "runRedundantChangeTest: at synthesizing commit-as-is"); + is(result[2].type, "compositionend", + "runRedundantChangeTest: compositionend should be fired after synthesizing composition commit-as-is"); + is(result[3].type, "input", + "runRedundantChangeTest: input should be fired before compositionend at synthesizing commit-as-is"); + checkInputEvent(result[3], false, "insertCompositionText", "\u3042\u3044", [], + "runRedundantChangeTest: at synthesizing commit-as-is"); + is(textarea.value, "\u3042\u3044", "runRedundantChangeTest: textarea has the commit string"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + textarea.removeEventListener("beforeinput", handler, true); + textarea.removeEventListener("input", handler, true); + textarea.removeEventListener("text", handler, true); +} + +function runNotRedundantChangeTest() +{ + textarea.focus(); + + let result = []; + function clearResult() + { + result = []; + } + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + textarea.addEventListener("beforeinput", handler, true); + textarea.addEventListener("input", handler, true); + textarea.addEventListener("text", handler, true); + + textarea.value = "abcde"; + + // synthesize change event with non-null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "ABCDE", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + is(result.length, 4, + "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition change with non-null ranges"); + is(result[0].type, "compositionupdate", + "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges"); + is(result[1].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges"); + is(result[2].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change with non-null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges"); + is(result[3].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges"); + checkInputEvent(result[3], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges"); + is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #1"); + + // synthesize change event with null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "ABCDE", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + }); + is(result.length, 3, + "runNotRedundantChangeTest: 3 events should be fired after synthesizing composition change with null ranges after non-null ranges"); + is(result[0].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with null ranges after non-null ranges"); + is(result[1].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change because it's dispatched when there is composing string with null ranges after non-null ranges"); + checkInputEvent(result[1], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with null ranges after non-null ranges"); + is(result[2].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with null ranges after non-null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with null ranges after non-null ranges"); + is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #2"); + + // synthesize change event with non-null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "ABCDE", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + is(result.length, 3, + "runNotRedundantChangeTest: 3 events should be fired after synthesizing composition change with null ranges after non-null ranges"); + is(result[0].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after null ranges"); + is(result[1].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after null ranges"); + checkInputEvent(result[1], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after null ranges"); + is(result[2].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after null ranges"); + is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #3"); + + // synthesize change event with empty data and null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + }); + is(result.length, 4, + "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition change with empty data and null ranges after non-null ranges"); + is(result[0].type, "compositionupdate", + "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with empty data and null ranges after non-null ranges"); + is(result[1].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data and null ranges after non-null ranges"); + is(result[2].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change with empty data and null ranges after non-null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "", [], + "runNotRedundantChangeTest: after synthesizing composition change with empty data and null ranges after non-null ranges"); + is(result[3].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with empty data and null ranges after non-null ranges"); + checkInputEvent(result[3], true, "insertCompositionText", "", [], + "runNotRedundantChangeTest: after synthesizing composition change with empty data and null ranges after non-null ranges"); + is(textarea.value, "abcde", "runNotRedundantChangeTest: textarea doesn't have uncommitted string #1"); + + // synthesize change event with non-null ranges + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": "ABCDE", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + is(result.length, 4, + "runNotRedundantChangeTest: 4 events should be fired after synthesizing composition change with non-null ranges after empty data and null ranges"); + is(result[0].type, "compositionupdate", + "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition change with non-null ranges after empty data and null ranges"); + is(result[1].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with non-null ranges after empty data and null ranges"); + is(result[2].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition change with non-null ranges after empty data and null ranges"); + checkInputEvent(result[2], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after empty data and null ranges"); + is(result[3].type, "input", + "runNotRedundantChangeTest: input should be fired after synthesizing composition change with non-null ranges after empty data and null ranges"); + checkInputEvent(result[3], true, "insertCompositionText", "ABCDE", [], + "runNotRedundantChangeTest: after synthesizing composition change with non-null ranges after empty data and null ranges"); + is(textarea.value, "abcdeABCDE", "runNotRedundantChangeTest: textarea has uncommitted string #4"); + + clearResult(); + synthesizeComposition({ type: "compositioncommit", data: "" }); + + is(result.length, 5, + "runNotRedundantChangeTest: 5 events should be fired after synthesizing composition commit with empty data after non-empty data"); + is(result[0].type, "compositionupdate", + "runNotRedundantChangeTest: compositionupdate should be fired after synthesizing composition commit with empty data after non-empty data"); + is(result[1].type, "text", + "runNotRedundantChangeTest: text should be fired after synthesizing composition change because it's dispatched when there is composing string with empty data after non-empty data"); + is(result[2].type, "beforeinput", + "runNotRedundantChangeTest: beforeinput should be fired after synthesizing composition commit with empty data after non-empty data"); + checkInputEvent(result[2], true, "insertCompositionText", "", [], + "runNotRedundantChangeTest: after synthesizing composition change with empty data after non-empty data"); + is(result[3].type, "compositionend", + "runNotRedundantChangeTest: compositionend should be fired after synthesizing composition commit with empty data after non-empty data"); + is(result[4].type, "input", + "runNotRedundantChangeTest: input should be fired after compositionend after synthesizing composition change with empty data after non-empty data"); + checkInputEvent(result[4], false, "insertCompositionText", "", [], + "runNotRedundantChangeTest: after synthesizing composition change with empty data after non-empty data"); + is(textarea.value, "abcde", "runNotRedundantChangeTest: textarea doesn't have uncommitted string #2"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + textarea.removeEventListener("beforeinput", handler, true); + textarea.removeEventListener("input", handler, true); + textarea.removeEventListener("text", handler, true); +} + +function runNativeLineBreakerTest() +{ + textarea.focus(); + + let result = {}; + function clearResult() + { + result = { compositionupdate: null, compositionend: null }; + } + + function handler(aEvent) + { + result[aEvent.type] = aEvent.data; + } + + Services.prefs.setBoolPref("dom.compositionevent.allow_control_characters", false); + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + + // '\n' in composition string shouldn't be changed. + clearResult(); + textarea.value = ""; + let clauses = [ "abc\n", "def\n\ng", "hi\n", "\njkl" ]; + let caret = clauses[0] + clauses[1] + clauses[2]; + synthesizeCompositionChange( + { "composition": + { "string": clauses.join(''), + "clauses": + [ + { "length": clauses[0].length, + "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": clauses[1].length, + "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": clauses[2].length, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": clauses[3].length, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + ] + }, + "caret": { "start": caret.length, "length": 0 } + }); + + checkSelection(caret.replace(/\n/g, "\n").length + (kLFLen - 1) * 4, "", "runNativeLineBreakerTest", "#1"); + checkIMESelection("RawClause", true, 0, clauses[0].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1"); + checkIMESelection("SelectedRawClause", true, clauses[0].replace(/\n/g, kLF).length, clauses[1].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1"); + checkIMESelection("ConvertedClause", true, (clauses[0] + clauses[1]).replace(/\n/g, kLF).length, clauses[2].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1"); + checkIMESelection("SelectedClause", true, (clauses[0] + clauses[1] + clauses[2]).replace(/\n/g, kLF).length, clauses[3].replace(/\n/g, kLF), "runNativeLineBreakerTest: \\n shouldn't be replaced with any character #1"); + is(result.compositionupdate, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in compositionupdate.data shouldn't be removed nor replaced with other characters #1"); + is(textarea.value, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in textarea.value shouldn't be removed nor replaced with other characters #1"); + + synthesizeComposition({ type: "compositioncommit", data: clauses.join('') }); + checkSelection(clauses.join('').replace(/\n/g, "\n").length + (kLFLen - 1) * 5, "", "runNativeLineBreakerTest", "#2"); + is(result.compositionend, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in compositionend.data shouldn't be removed nor replaced with other characters #2"); + is(textarea.value, clauses.join('').replace(/\n/g, "\n"), "runNativeLineBreakerTest: \\n in textarea.value shouldn't be removed nor replaced with other characters #2"); + + // \r\n in composition string should be replaced with \n. + clearResult(); + textarea.value = ""; + clauses = [ "abc\r\n", "def\r\n\r\ng", "hi\r\n", "\r\njkl" ]; + caret = clauses[0] + clauses[1] + clauses[2]; + synthesizeCompositionChange( + { "composition": + { "string": clauses.join(''), + "clauses": + [ + { "length": clauses[0].length, + "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": clauses[1].length, + "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": clauses[2].length, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": clauses[3].length, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + ] + }, + "caret": { "start": caret.length, "length": 0 } + }); + + checkSelection(caret.replace(/\r\n/g, "\n").length + (kLFLen - 1) * 4, "", "runNativeLineBreakerTest", "#3"); + checkIMESelection("RawClause", true, 0, clauses[0].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3"); + checkIMESelection("SelectedRawClause", true, clauses[0].replace(/\r\n/g, kLF).length, clauses[1].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3"); + checkIMESelection("ConvertedClause", true, (clauses[0] + clauses[1]).replace(/\r\n/g, kLF).length, clauses[2].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3"); + checkIMESelection("SelectedClause", true, (clauses[0] + clauses[1] + clauses[2]).replace(/\r\n/g, kLF).length, clauses[3].replace(/\r\n/g, kLF), "runNativeLineBreakerTest: \\r\\n should be replaced with \\n #3"); + is(result.compositionupdate, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in compositionudpate.data should be replaced with \\n #3"); + is(textarea.value, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in textarea.value should be replaced with \\n #3"); + + synthesizeComposition({ type: "compositioncommit", data: clauses.join('') }); + checkSelection(clauses.join('').replace(/\r\n/g, "\n").length + (kLFLen - 1) * 5, "", "runNativeLineBreakerTest", "#4"); + is(result.compositionend, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in compositionend.data should be replaced with \\n #4"); + is(textarea.value, clauses.join('').replace(/\r\n/g, "\n"), "runNativeLineBreakerTest: \\r\\n in textarea.value should be replaced with \\n #4"); + + // \r (not followed by \n) in composition string should be replaced with \n. + clearResult(); + textarea.value = ""; + clauses = [ "abc\r", "def\r\rg", "hi\r", "\rjkl" ]; + caret = clauses[0] + clauses[1] + clauses[2]; + synthesizeCompositionChange( + { "composition": + { "string": clauses.join(''), + "clauses": + [ + { "length": clauses[0].length, + "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + { "length": clauses[1].length, + "attr": COMPOSITION_ATTR_SELECTED_RAW_CLAUSE }, + { "length": clauses[2].length, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": clauses[3].length, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + ] + }, + "caret": { "start": caret.length, "length": 0 } + }); + + checkSelection(caret.replace(/\r/g, "\n").length + (kLFLen - 1) * 4, "", "runNativeLineBreakerTest", "#5"); + checkIMESelection("RawClause", true, 0, clauses[0].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5"); + checkIMESelection("SelectedRawClause", true, clauses[0].replace(/\r/g, kLF).length, clauses[1].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5"); + checkIMESelection("ConvertedClause", true, (clauses[0] + clauses[1]).replace(/\r/g, kLF).length, clauses[2].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5"); + checkIMESelection("SelectedClause", true, (clauses[0] + clauses[1] + clauses[2]).replace(/\r/g, kLF).length, clauses[3].replace(/\r/g, kLF), "runNativeLineBreakerTest: \\r should be replaced with \\n #5"); + is(result.compositionupdate, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in compositionupdate.data should be replaced with \\n #5"); + is(textarea.value, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in textarea.value should be replaced with \\n #5"); + + synthesizeComposition({ type: "compositioncommit", data: clauses.join('') }); + checkSelection(clauses.join('').replace(/\r/g, "\n").length + (kLFLen - 1) * 5, "", "runNativeLineBreakerTest", "#6"); + is(result.compositionend, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in compositionend.data should be replaced with \\n #6"); + is(textarea.value, clauses.join('').replace(/\r/g, "\n"), "runNativeLineBreakerTest: \\r in textarea.value should be replaced with \\n #6"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); + + Services.prefs.clearUserPref("dom.compositionevent.allow_control_characters"); +} + +function runControlCharTest() +{ + textarea.focus(); + + let result = {}; + function clearResult() + { + result = { compositionupdate: null, compositionend: null }; + } + + function handler(aEvent) + { + result[aEvent.type] = aEvent.data; + } + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + + textarea.value = ""; + + let controlChars = String.fromCharCode.apply(null, Object.keys(Array.from({length:0x20}))) + "\x7F"; + let allowedChars = "\t\n\n"; + + let data = "AB" + controlChars + "CD" + controlChars + "EF"; + let removedData = "AB" + allowedChars + "CD" + allowedChars + "EF"; + + let DIndex = data.indexOf("D"); + let removedDIndex = removedData.indexOf("D"); + + // input string contains control characters + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": data, + "clauses": + [ + { "length": DIndex, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": data.length - DIndex, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": DIndex, "length": 0 } + }); + + checkSelection(removedDIndex + (kLFLen - 1) * 2, "", "runControlCharTest", "#1") + + is(result.compositionupdate, removedData, "runControlCharTest: control characters in event.data should be removed in compositionupdate event #1"); + is(textarea.value, removedData, "runControlCharTest: control characters should not appear in textarea #1"); + + synthesizeComposition({ type: "compositioncommit", data }); + + is(result.compositionend, removedData, "runControlCharTest: control characters in event.data should be removed in compositionend event #2"); + is(textarea.value, removedData, "runControlCharTest: control characters should not appear in textarea #2"); + + textarea.value = ""; + + clearResult(); + + Services.prefs.setBoolPref("dom.compositionevent.allow_control_characters", true); + + // input string contains control characters, allowing control characters + clearResult(); + synthesizeCompositionChange( + { "composition": + { "string": data, + "clauses": + [ + { "length": DIndex, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": data.length - DIndex, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": DIndex, "length": 0 } + }); + + checkSelection(DIndex + (kLFLen - 1) * 2, "", "runControlCharTest", "#3") + + is(result.compositionupdate, data.replace(/\r/g, "\n"), "runControlCharTest: control characters in event.data should not be removed in compositionupdate event #3"); + is(textarea.value, data.replace(/\r/g, "\n"), "runControlCharTest: control characters should appear in textarea #3"); + + synthesizeComposition({ type: "compositioncommit", data }); + + is(result.compositionend, data.replace(/\r/g, "\n"), "runControlCharTest: control characters in event.data should not be removed in compositionend event #4"); + is(textarea.value, data.replace(/\r/g, "\n"), "runControlCharTest: control characters should appear in textarea #4"); + + Services.prefs.clearUserPref("dom.compositionevent.allow_control_characters"); + + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); +} + +async function runRemoveContentTest() +{ + let events = []; + function eventHandler(aEvent) + { + events.push(aEvent); + } + textarea.addEventListener("compositionstart", eventHandler, true); + textarea.addEventListener("compositionupdate", eventHandler, true); + textarea.addEventListener("compositionend", eventHandler, true); + textarea.addEventListener("beforeinput", eventHandler, true); + textarea.addEventListener("input", eventHandler, true); + textarea.addEventListener("text", eventHandler, true); + + textarea.focus(); + textarea.value = ""; + + synthesizeCompositionChange( + { "composition": + { "string": "\u306E", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + let nextSibling = textarea.nextSibling; + let parent = textarea.parentElement; + + events = []; + parent.removeChild(textarea); + + await waitForEventLoops(50); + + // XXX Currently, "input" event and "beforeinput" event are not fired on removed content. + is(events.length, 3, + "runRemoveContentTest: wrong event count #1"); + is(events[0].type, "compositionupdate", + "runRemoveContentTest: the 1st event must be compositionupdate #1"); + is(events[0].target, textarea, + `runRemoveContentTest: The "${events[0].type}" event was fired on wrong event target #1`); + is(events[0].data, "", + "runRemoveContentTest: compositionupdate has wrong data #1"); + is(events[1].type, "text", + "runRemoveContentTest: the 2nd event must be text #1"); + is(events[1].target, textarea, + `runRemoveContentTest: The "${events[1].type}" event was fired on wrong event target #1`); + todo_is(events[2].type, "beforeinput", + "runRemoveContentTest: the 3rd event must be beforeinput #1"); + // is(events[2].target, textarea, + // `runRemoveContentTest: The "${events[2].type}" event was fired on wrong event target #1`); + // checkInputEvent(events[2], true, "insertCompositionText", "", [], + // "runRemoveContentTest: #1"); + is(events[2].type, "compositionend", + "runRemoveContentTest: the 4th event must be compositionend #1"); + is(events[2].target, textarea, + `runRemoveContentTest: The "${events[2].type}" event was fired on wrong event target #1`); + is(events[2].data, "", + "runRemoveContentTest: compositionend has wrong data #1"); + ok(!getEditor(textarea).isComposing, + "runRemoveContentTest: the textarea still has composition #1"); + todo_is(textarea.value, "", + "runRemoveContentTest: the textarea has the committed text? #1"); + + parent.insertBefore(textarea, nextSibling); + + textarea.focus(); + textarea.value = ""; + + synthesizeComposition({ type: "compositionstart" }); + + events = []; + parent.removeChild(textarea); + + await waitForEventLoops(50); + + // XXX Currently, "input" event and "beforeinput" event are not fired on removed content. + is(events.length, 2, + "runRemoveContentTest: wrong event count #2"); + is(events[0].type, "text", + "runRemoveContentTest: the 1st event must be text #2"); + is(events[0].target, textarea, + `runRemoveContentTest: The ${events[0].type} event was fired on wrong event target #2`); + todo_is(events[1].type, "beforeinput", + "runRemoveContentTest: the 2nd event must be beforeinput #2"); + // is(events[1].target, textarea, + // `runRemoveContentTest: The ${events[1].type} event was fired on wrong event target #2`); + // checkInputEvent(events[1], true, "insertCompositionText", "", [], + // "runRemoveContentTest: #2"); + is(events[1].type, "compositionend", + "runRemoveContentTest: the 3rd event must be compositionend #2"); + is(events[1].target, textarea, + `runRemoveContentTest: The ${events[1].type} event was fired on wrong event target #2`); + is(events[1].data, "", + "runRemoveContentTest: compositionupdate has wrong data #2"); + ok(!getEditor(textarea).isComposing, + "runRemoveContentTest: the textarea still has composition #2"); + is(textarea.value, "", + "runRemoveContentTest: the textarea has the committed text? #2"); + + parent.insertBefore(textarea, nextSibling); + + textarea.removeEventListener("compositionstart", eventHandler, true); + textarea.removeEventListener("compositionupdate", eventHandler, true); + textarea.removeEventListener("compositionend", eventHandler, true); + textarea.removeEventListener("beforeinput", eventHandler, true); + textarea.removeEventListener("input", eventHandler, true); + textarea.removeEventListener("text", eventHandler, true); + + await waitForTick(); +} + +function runTestOnAnotherContext(aPanelOrFrame, aFocusedEditor, aTestName) +{ + aFocusedEditor.value = ""; + + // The frames and panel are cross-origin, and we no longer + // propagate flushes to parent cross-origin iframes explicitly, + // so flush our own layout here so the positions are correct. + document.documentElement.getBoundingClientRect(); + + let editorRect = synthesizeQueryEditorRect(); + if (!checkQueryContentResult(editorRect, aTestName + ": editorRect")) { + return; + } + + let r = aPanelOrFrame.getBoundingClientRect(); + let parentRect = { + left: r.left * window.devicePixelRatio, + top: r.top * window.devicePixelRatio, + width: (r.right - r.left) * window.devicePixelRatio, + height: (r.bottom - r.top) * window.devicePixelRatio, + }; + checkRectContainsRect(editorRect, parentRect, aTestName + + ": the editor rect coordinates are wrong"); + + // input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3078\u3093\u3057\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 } + }); + + if (!checkContent("\u3078\u3093\u3057\u3093", aTestName, "#1-1") || + !checkSelection(4, "", aTestName, "#1-1")) { + return; + } + + // convert them #1 + synthesizeCompositionChange( + { "composition": + { "string": "\u8FD4\u4FE1", + "clauses": + [ + { "length": 2, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkContent("\u8FD4\u4FE1", aTestName, "#1-2") || + !checkSelection(2, "", aTestName, "#1-2")) { + return; + } + + // convert them #2 + synthesizeCompositionChange( + { "composition": + { "string": "\u5909\u8EAB", + "clauses": + [ + { "length": 2, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkContent("\u5909\u8EAB", aTestName, "#1-3") || + !checkSelection(2, "", aTestName, "#1-3")) { + return; + } + + // commit them + synthesizeComposition({ type: "compositioncommitasis" }); + if (!checkContent("\u5909\u8EAB", aTestName, "#1-4") || + !checkSelection(2, "", aTestName, "#1-4")) { + return; + } + + is(aFocusedEditor.value, "\u5909\u8EAB", + aTestName + ": composition isn't in the focused editor"); + if (aFocusedEditor.value != "\u5909\u8EAB") { + return; + } + + let textRect = synthesizeQueryTextRect(0, 1); + let caretRect = synthesizeQueryCaretRect(2); + if (!checkQueryContentResult(textRect, + aTestName + ": synthesizeQueryTextRect") || + !checkQueryContentResult(caretRect, + aTestName + ": synthesizeQueryCaretRect")) { + return; + } + checkRectContainsRect(textRect, editorRect, aTestName + ":testRect"); + checkRectContainsRect(caretRect, editorRect, aTestName + ":caretRect"); +} + +function runFrameTest() +{ + textareaInFrame.focus(); + runTestOnAnotherContext(iframe, textareaInFrame, "runFrameTest"); + runCharAtPointTest(textareaInFrame, "textarea in the iframe"); +} + +async function runPanelTest() +{ + panel.hidden = false; + let waitOpenPopup = new Promise(resolve => { + panel.addEventListener("popupshown", resolve, {once: true}); + }); + let waitFocusTextBox = new Promise(resolve => { + textbox.addEventListener("focus", resolve, {once: true}); + }); + panel.openPopupAtScreen(window.screenX + window.outerWidth, 0, false); + await waitOpenPopup; + textbox.focus(); + await waitFocusTextBox; + is(panel.state, "open", "The panel should be open (after textbox.focus())"); + await waitForTick(); + is(panel.state, "open", "The panel should be open (after waitForTick())"); + runTestOnAnotherContext(panel, textbox, "runPanelTest"); + is(panel.state, "open", "The panel should be open (after runTestOnAnotherContext())"); + runCharAtPointTest(textbox, "textbox in the panel"); + is(panel.state, "open", "The panel should be open (after runCharAtPointTest())"); + let waitClosePopup = new Promise(resolve => { + panel.addEventListener("popuphidden", resolve, {once: true}); + }); + panel.hidePopup(); + await waitClosePopup; + await waitForTick(); +} + +// eslint-disable-next-line complexity +function runMaxLengthTest() +{ + input.maxLength = 1; + input.value = ""; + input.focus(); + + let kDesc ="runMaxLengthTest"; + + // input first character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + if (!checkContent("\u3089", kDesc, "#1-1") || + !checkSelection(1, "", kDesc, "#1-1")) { + return; + } + + // input second character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC", kDesc, "#1-2") || + !checkSelection(2, "", kDesc, "#1-2")) { + return; + } + + // input third character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081", kDesc, "#1-3") || + !checkSelection(3, "", kDesc, "#1-3")) { + return; + } + + // input fourth character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093", kDesc, "#1-4") || + !checkSelection(4, "", kDesc, "#1-4")) { + return; + } + + + // backspace + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081", kDesc, "#1-5") || + !checkSelection(3, "", kDesc, "#1-5")) { + return; + } + + // re-input + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093", + "clauses": + [ + { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093", kDesc, "#1-6") || + !checkSelection(4, "", kDesc, "#1-6")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055", + "clauses": + [ + { "length": 5, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 5, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055", kDesc, "#1-7") || + !checkSelection(5, "", kDesc, "#1-7")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044", + "clauses": + [ + { "length": 6, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 6, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044", kDesc, "#1-8") || + !checkSelection(6, "", kDesc, "#1-8")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053", + "clauses": + [ + { "length": 7, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 7, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053", + kDesc, "#1-8") || + !checkSelection(7, "", kDesc, "#1-8")) { + return; + } + + synthesizeCompositionChange( + { "composition": + { "string": "\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046", + "clauses": + [ + { "length": 8, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 8, "length": 0 } + }); + + if (!checkContent("\u3089\u30FC\u3081\u3093\u3055\u3044\u3053\u3046", + kDesc, "#1-9") || + !checkSelection(8, "", kDesc, "#1-9")) { + return; + } + + // convert + synthesizeCompositionChange( + { "composition": + { "string": "\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", + "clauses": + [ + { "length": 4, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 2, + "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 4, "length": 0 } + }); + + if (!checkContent("\u30E9\u30FC\u30E1\u30F3\u6700\u9AD8", kDesc, "#1-10") || + !checkSelection(4, "", kDesc, "#1-10")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + if (!checkContent("\u30E9", kDesc, "#1-11") || + !checkSelection(1, "", kDesc, "#1-11")) { + return; + } + + // input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u3057", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + if (!checkContent("\u30E9\u3057", kDesc, "#2-1") || + !checkSelection(1 + 1, "", kDesc, "#2-1")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommit", data: "\u3058" }); + if (!checkContent("\u30E9", kDesc, "#2-2") || + !checkSelection(1 + 0, "", kDesc, "#2-2")) { + return; + } + + // Undo + synthesizeKey("Z", {accelKey: true}); + + // XXX this is unexpected behavior, see bug 258291 + if (!checkContent("\u30E9", kDesc, "#3-1") || + !checkSelection(1 + 0, "", kDesc, "#3-1")) { + return; + } + + // Undo + synthesizeKey("Z", {accelKey: true}); + if (!checkContent("", kDesc, "#3-2") || + !checkSelection(0, "", kDesc, "#3-2")) { + return; + } + + // Redo + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + if (!checkContent("\u30E9", kDesc, "#3-3") || + !checkSelection(1, "", kDesc, "#3-3")) { + return; + } + + // Redo + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + if (!checkContent("\u30E9", kDesc, "#3-4") || + !checkSelection(1 + 0, "", kDesc, "#3-4")) { + return; + } + + // The input element whose content length is already maxlength and + // the carest is at start of the content. + input.value = "X"; + input.selectionStart = input.selectionEnd = 0; + + // input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u9B54", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + + if (!checkContent("\u9B54X", kDesc, "#4-1") || + !checkSelection(1, "", kDesc, "#4-1")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + + // The input text must be discarded. Then, the caret position shouldn't be + // updated from its position at compositionstart. + if (!checkContent("X", kDesc, "#4-2") || + !checkSelection(0, "", kDesc, "#4-2")) { + return; + } + + // input characters + synthesizeCompositionChange( + { "composition": + { "string": "\u9B54\u6CD5", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + + if (!checkContent("\u9B54\u6CD5X", kDesc, "#5-1") || + !checkSelection(2, "", kDesc, "#5-1")) { + return; + } + + // commit the composition string + synthesizeComposition({ type: "compositioncommitasis" }); + + if (checkContent("X", kDesc, "#5-2")) { + checkSelection(0, "", kDesc, "#5-2"); + } +} + +async function runEditorReframeTests() +{ + async function runEditorReframeTest(aEditor, aWindow, aEventType) + { + function getValue() + { + return aEditor == contenteditable ? + aEditor.innerHTML.replace("<br>", "") : aEditor.value; + } + + let description = "runEditorReframeTest(" + aEditor.id + ", \"" + aEventType + "\"): "; + + let tests = [ + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "a", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "a", description + "Typing 'a'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "ab", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ab", description + "Typing 'b' next to 'a'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "abc", description + "Typing 'c' next to 'ab'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "abc", description + "Starting to convert 'ab][c'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "ABc", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABc", description + "Starting to convert 'AB][c'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "ABC", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }, + { "length": 1, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABC", description + "Starting to convert 'AB][C'"); + }, + }, + { test () { + // Commit composition + synthesizeComposition({ type: "compositioncommitasis" }); + }, + check () { + is(getValue(aEditor), "ABC", description + "Committed as 'ABC'"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "d", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABCd", description + "Typing 'd' next to ABC"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "de", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABCde", description + "Typing 'e' next to ABCd"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "def", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABCdef", description + "Typing 'f' next to ABCde"); + }, + }, + { test () { + // Commit composition + synthesizeComposition({ type: "compositioncommitasis" }); + }, + check () { + is(getValue(aEditor), "ABCdef", description + "Commit 'def' without convert"); + }, + }, + { test () { + // Select "Cd" + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_Shift", {type: "keydown", shiftKey: true}); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + synthesizeKey("KEY_Shift", {type: "keyup"}); + }, + check () { + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "g", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABgef", description + "Typing 'g' next to AB"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "gh", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABghef", description + "Typing 'h' next to ABg"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "ghi", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABghief", description + "Typing 'i' next to ABgh"); + }, + }, + { test () { + synthesizeCompositionChange( + { "composition": + { "string": "GHI", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 } + }); + }, + check () { + is(getValue(aEditor), "ABGHIef", description + "Convert 'ghi' to 'GHI'"); + }, + }, + { test () { + // Commit composition + synthesizeComposition({ type: "compositioncommitasis" }); + }, + check () { + is(getValue(aEditor), "ABGHIef", description + "Commit 'GHI'"); + }, + }, + ]; + + function doReframe(aEvent) + { + aEvent.target.style.overflow = + aEvent.target.style.overflow != "hidden" ? "hidden" : "auto"; + } + aEditor.focus(); + aEditor.addEventListener(aEventType, doReframe); + + for (const currentTest of tests) { + currentTest.test(); + await waitForEventLoops(20); + currentTest.check(); + await waitForTick(); + } + + await new Promise(resolve => { + aEditor.style.overflow = "auto"; + aEditor.removeEventListener(aEventType, doReframe); + requestAnimationFrame(() => { SimpleTest.executeSoon(resolve); }); + }); + } + + // TODO: Add "beforeinput" case. + input.value = ""; + await runEditorReframeTest(input, window, "input"); + input.value = ""; + await runEditorReframeTest(input, window, "compositionupdate"); + textarea.value = ""; + await runEditorReframeTest(textarea, window, "input"); + textarea.value = ""; + await runEditorReframeTest(textarea, window, "compositionupdate"); + contenteditable.innerHTML = ""; + await runEditorReframeTest(contenteditable, windowOfContenteditable, "input"); + contenteditable.innerHTML = ""; + await runEditorReframeTest(contenteditable, windowOfContenteditable, "compositionupdate"); +} + +async function runIMEContentObserverTest() +{ + let notifications = []; + let onReceiveNotifications = null; + function callback(aTIP, aNotification) + { + if (aNotification.type != "notify-end-input-transaction") { + notifications.push(aNotification); + } + switch (aNotification.type) { + case "request-to-commit": + aTIP.commitComposition(); + break; + case "request-to-cancel": + aTIP.cancelComposition(); + break; + } + if (onReceiveNotifications) { + let resolve = onReceiveNotifications; + onReceiveNotifications = null; + SimpleTest.executeSoon(() => { + resolve(); + }); + } + return true; + } + + function dumpUnexpectedNotifications(aDescription, aExpectedCount) + { + if (notifications.length <= aExpectedCount) { + return; + } + for (let i = aExpectedCount; i < notifications.length; i++) { + ok(false, + aDescription + " caused unexpected notification: " + notifications[i].type); + } + } + + function promiseReceiveNotifications() + { + notifications = []; + return new Promise(resolve => { + onReceiveNotifications = resolve; + }); + } + + function flushNotifications() + { + return new Promise(resolve => { + // FYI: Dispatching non-op keyboard events causes forcibly flushing pending + // notifications. + synthesizeKey("KEY_Unidentified", { code: "" }); + SimpleTest.executeSoon(()=>{ + notifications = []; + resolve(); + }); + }); + } + + function ensureToRemovePrecedingPositionChangeNotification(aDescription) + { + if (!notifications.length) { + return; + } + if (notifications[0].type != "notify-position-change") { + return; + } + // Sometimes, notify-position-change is notified first separately if + // the operation causes scroll or something. Tests can ignore this. + ok(true, "notify-position-change", aDescription + "Unnecessary notify-position-change occurred, ignoring it"); + notifications.shift(); + } + + // Bug 1374057 - On ubuntu 16.04 there are notify-position-change events that are + // recorded after all the other events so we remove them through this function. + function ensureToRemovePostPositionChangeNotification(aDescription, expectedCount) + { + if (!notifications.length) { + return; + } + if (notifications.length <= expectedCount) { + return; + } + if (notifications[notifications.length-1].type != "notify-position-change") { + return; + } + ok(true, "notify-position-change", aDescription + "Unnecessary notify-position-change occurred, ignoring it"); + notifications.pop(); + } + + function getNativeText(aXPText) + { + if (kLF == "\n") { + return aXPText; + } + return aXPText.replace(/\n/g, kLF); + } + + function checkPositionChangeNotification(aNotification, aDescription) + { + is(!aNotification || aNotification.type, "notify-position-change", + aDescription + " should cause position change notification"); + } + + function checkSelectionChangeNotification(aNotification, aDescription, aExpected) + { + is(!aNotification || aNotification.type, "notify-selection-change", + aDescription + " should cause selection change notification"); + if (!aNotification || (aNotification.type != "notify-selection-change")) { + return; + } + is(aNotification.offset, aExpected.offset, + aDescription + " should cause selection change notification whose offset is " + aExpected.offset); + is(aNotification.text, aExpected.text, + aDescription + " should cause selection change notification whose text is '" + aExpected.text + "'"); + is(aNotification.collapsed, !aExpected.text.length, + aDescription + " should cause selection change notification whose collapsed is " + (!aExpected.text.length)); + is(aNotification.length, aExpected.text.length, + aDescription + " should cause selection change notification whose length is " + aExpected.text.length); + is(aNotification.reversed, aExpected.reversed || false, + aDescription + " should cause selection change notification whose reversed is " + (aExpected.reversed || false)); + is(aNotification.writingMode, aExpected.writingMode || "horizontal-tb", + aDescription + " should cause selection change notification whose writingMode is '" + (aExpected.writingMode || "horizontal-tb")); + } + + function checkTextChangeNotification(aNotification, aDescription, aExpected) + { + is(!aNotification || aNotification.type, "notify-text-change", + aDescription + " should cause text change notification"); + if (!aNotification || aNotification.type != "notify-text-change") { + return; + } + is(aNotification.offset, aExpected.offset, + aDescription + " should cause text change notification whose offset is " + aExpected.offset); + is(aNotification.removedLength, aExpected.removedLength, + aDescription + " should cause text change notification whose removedLength is " + aExpected.removedLength); + is(aNotification.addedLength, aExpected.addedLength, + aDescription + " should cause text change notification whose addedLength is " + aExpected.addedLength); + } + + async function testWithPlaintextEditor(aDescription, aElement, aTestLineBreaker) + { + aElement.value = ""; + aElement.blur(); + let doc = aElement.ownerDocument; + let win = doc.defaultView; + aElement.focus(); + await flushNotifications(); + + // "a[]" + let description = aDescription + "typing 'a'"; + let waitNotifications = promiseReceiveNotifications(); + synthesizeKey("a", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "ab[]" + description = aDescription + "typing 'b'"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("b", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "abc[]" + description = aDescription + "typing 'c'"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("c", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 3, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "ab[c]" + description = aDescription + "selecting 'c' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 2, text: "c", reversed: true }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "a[bc]" + description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 1, text: "bc", reversed: true }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[abc]" + description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "abc", reversed: true }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[]abc" + description = aDescription + "collapsing selection to the left-most with pressing ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "" }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[a]bc" + description = aDescription + "selecting 'a' with pressing Shift+ArrowRight"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "a" }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[ab]c" + description = aDescription + "selecting 'ab' with pressing Shift+ArrowRight"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0, text: "ab" }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "[]c" + description = aDescription + "deleting 'ab' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "[]" + description = aDescription + "deleting following 'c' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 1, addedLength: 0 }); + checkPositionChangeNotification(notifications[1], description); + ensureToRemovePostPositionChangeNotification(description, 2); + dumpUnexpectedNotifications(description, 2); + + // "abc[]" + synthesizeKey("a", {}, win, callback); + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + await flushNotifications(); + + // "ab[]" + description = aDescription + "deleting 'c' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 1, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "[ab]" + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "[]" + description = aDescription + "deleting 'ab' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "abcd[]" + synthesizeKey("a", {}, win, callback); + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + synthesizeKey("d", {}, win, callback); + await flushNotifications(); + + // "a[bc]d" + synthesizeKey("KEY_ArrowLeft", {}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "a[]d" + description = aDescription + "deleting 'bc' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "a[bc]d" + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "aB[]d" + description = aDescription + "replacing 'bc' with 'B' with pressing Shift+B"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("B", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 2, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + if (!aTestLineBreaker) { + return; + } + + // "aB\n[]d" + description = aDescription + "inserting a line break after 'B' with pressing Enter"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Enter", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: 0, addedLength: kLFLen }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("aB\n").length, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "aB[]d" + description = aDescription + "removing a line break after 'B' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2, removedLength: kLFLen, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "a[B]d" + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "a\n[]d" + description = aDescription + "replacing 'B' with a line break with pressing Enter"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Enter", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: 1, addedLength: kLFLen }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("a\n").length, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // "a[\n]d" + description = aDescription + "selecting '\n' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 1, text: kLF, reversed: true }); + ensureToRemovePostPositionChangeNotification(description, 1); + dumpUnexpectedNotifications(description, 1); + + // "a[]d" + description = aDescription + "removing selected '\n' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1, removedLength: kLFLen, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // ab\ncd\nef\ngh\n[] + description = aDescription + "setting the value property to 'ab\ncd\nef\ngh\n'"; + waitNotifications = promiseReceiveNotifications(); + aElement.value = "ab\ncd\nef\ngh\n"; + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: 2, addedLength: getNativeText("ab\ncd\nef\ngh\n").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("ab\ncd\nef\ngh\n").length, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + + // [] + description = aDescription + "setting the value property to ''"; + waitNotifications = promiseReceiveNotifications(); + aElement.value = ""; + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0, removedLength: getNativeText("ab\ncd\nef\ngh\n").length, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" }); + checkPositionChangeNotification(notifications[2], description); + ensureToRemovePostPositionChangeNotification(description, 3); + dumpUnexpectedNotifications(description, 3); + } + + async function testWithHTMLEditor(aDescription, aElement, aDefaultParagraphSeparator) + { + let doc = aElement.ownerDocument; + let win = doc.defaultView; + let sel = doc.getSelection(); + let inDesignMode = doc.designMode == "on"; + let offsetAtStart = 0; + let offsetAtContainer = 0; + let isDefaultParagraphSeparatorBlock = aDefaultParagraphSeparator != "br"; + doc.execCommand("defaultParagraphSeparator", false, aDefaultParagraphSeparator); + + // "[]", "<p>[]</p>" or "<div>[]</div>" + switch (aDefaultParagraphSeparator) { + case "br": + aElement.innerHTML = ""; + break; + case "p": + case "div": + aElement.innerHTML = "<" + aDefaultParagraphSeparator + "></" + aDefaultParagraphSeparator + ">"; + sel.collapse(aElement.firstChild, 0); + offsetAtContainer = offsetAtStart + kLFLen; + break; + default: + ok(false, aDescription + "aDefaultParagraphSeparator is illegal value"); + await flushNotifications(); + return; + } + + if (inDesignMode) { + win.focus(); + } else { + aElement.focus(); + } + await flushNotifications(); + + // "a[]" + let description = aDescription + "typing 'a'"; + let waitNotifications = promiseReceiveNotifications(); + synthesizeKey("a", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "ab[]" + description = aDescription + "typing 'b'"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("b", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "abc[]" + description = aDescription + "typing 'c'"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("c", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, removedLength: 0, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 3 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "ab[c]" + description = aDescription + "selecting 'c' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, text: "c", reversed: true }); + dumpUnexpectedNotifications(description, 1); + + // "a[bc]" + description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, text: "bc", reversed: true }); + dumpUnexpectedNotifications(description, 1); + + // "[abc]" + description = aDescription + "selecting 'bc' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "abc", reversed: true }); + dumpUnexpectedNotifications(description, 1); + + // "[]abc" + description = aDescription + "collapsing selection to the left-most with pressing ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "" }); + dumpUnexpectedNotifications(description, 1); + + // "[a]bc" + description = aDescription + "selecting 'a' with pressing Shift+ArrowRight"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "a" }); + dumpUnexpectedNotifications(description, 1); + + // "[ab]c" + description = aDescription + "selecting 'ab' with pressing Shift+ArrowRight"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, text: "ab" }); + dumpUnexpectedNotifications(description, 1); + + // "[]c" + description = aDescription + "deleting 'ab' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "[]" + description = aDescription + "deleting following 'c' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 1, addedLength: kLFLen }); + checkPositionChangeNotification(notifications[1], description); + dumpUnexpectedNotifications(description, 2); + + // "abc[]" + synthesizeKey("a", {}, win, callback); + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + await flushNotifications(); + + // "ab[]" + description = aDescription + "deleting 'c' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 2 + offsetAtContainer, removedLength: 1, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "[ab]" + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "[]" + description = aDescription + "deleting 'ab' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "abcd[]" + synthesizeKey("a", {}, win, callback); + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + synthesizeKey("d", {}, win, callback); + await flushNotifications(); + + // "a[bc]d" + synthesizeKey("KEY_ArrowLeft", {}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "a[]d" + description = aDescription + "deleting 'bc' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 2, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "a[bc]d" + synthesizeKey("b", {}, win, callback); + synthesizeKey("c", {}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "aB[]d" + description = aDescription + "replacing 'bc' with 'B' with pressing Shift+B"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("B", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, removedLength: 2, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "aB<br>[]d" or "<block>aB</block><block>[]d</block>" + description = aDescription + "inserting a line break after 'B' with pressing Enter"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Enter", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + if (isDefaultParagraphSeparatorBlock) { + // Splitting current block causes removing "d</block>" and inserting "</block><block>d</block>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "aB".length, + removedLength: getNativeText("d\n").length, + addedLength: getNativeText("\nd\n").length, + }); + } else { + // Inserting <br> causes removing "d" and inserting "<br>d" + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "aB".length, + removedLength: "d".length, + addedLength: getNativeText("\nd").length, + }); + } + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("aB\n").length + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "aB[]d" + description = aDescription + "removing a line break after 'B' with pressing Backspace"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Backspace", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + if (isDefaultParagraphSeparatorBlock) { + // Joining two blocks causes removing "aB</block><block>d</block>" and inserting "aBd</block>" + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer, + removedLength: getNativeText("aB\nd\n").length, + addedLength: getNativeText("aBd\n").length, + }); + checkSelectionChangeNotification(notifications[1], description, { offset: 2 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + } else { + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "aB".length, + removedLength: kLFLen, + addedLength: 0, + }); + is(notifications.length, 3, description + " should cause 3 notifications"); + is(notifications[1] && notifications[1].type, "notify-selection-change", description + " should cause selection change notification"); + is(notifications[2] && notifications[2].type, "notify-position-change", description + " should cause position change notification"); + } + + // "a[B]d" + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await flushNotifications(); + + // "a<br>[]d" or "<block>a</block><block>[]d</block>" + description = aDescription + "replacing 'B' with a line break with pressing Enter"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Enter", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + if (isDefaultParagraphSeparatorBlock) { + // Splitting current block causes removing "Bd</block>" and inserting "</block><block>d</block>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "a".length, + removedLength: getNativeText("Bd\n").length, + addedLength: getNativeText("\nd\n").length, + }); + } else { + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "a".length, + removedLength: "B".length, + addedLength: kLFLen, + }); + } + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("a\n").length + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "a[<br>]d" or "<block>a[</block><block>]d</block>" + description = aDescription + "selecting '\\n' with pressing Shift+ArrowLeft"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkSelectionChangeNotification(notifications[0], description, { offset: 1 + offsetAtContainer, text: kLF, reversed: true }); + dumpUnexpectedNotifications(description, 1); + + // "a[]d" + description = aDescription + "removing selected '\\n' with pressing Delete"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + if (isDefaultParagraphSeparatorBlock) { + // Joining the blocks causes removing "a</block><block>d</block>" and inserting "<block>ad</block>". + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer, + removedLength: getNativeText("a\nd\n").length, + addedLength: getNativeText("ad\n").length, + }); + } else { + checkTextChangeNotification(notifications[0], description, { + offset: offsetAtContainer + "a".length, + removedLength: kLFLen, + addedLength: 0, + }); + } + checkSelectionChangeNotification(notifications[1], description, { offset: 1 + offsetAtContainer, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>" + description = aDescription + "inserting HTML which has nested block elements"; + waitNotifications = promiseReceiveNotifications(); + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"; + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + // There is <br> after the end of the line. Therefore, removed length includes a line breaker length. + if (isDefaultParagraphSeparatorBlock) { + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer - kLFLen, removedLength: getNativeText("\nad\n").length, addedLength: getNativeText("\n1\n2\n345").length }); + } else { + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtContainer, removedLength: 2 + kLFLen, addedLength: getNativeText("\n1\n2\n345").length }); + } + checkSelectionChangeNotification(notifications[1], description, { offset: 0, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1[<div>2<div>3</div>4</div>]5</div>" and removing selection + sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(2), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>15</div>", description + " should remove '<div>2<div>3</div>4</div>'"); + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n34").length, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1[<div>2<div>3</div>]4</div>5</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"; + sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(1).childNodes.item(2), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (partially #1) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>145</div>", description + " should remove '<div>2<div>3</div></div>'"); + // It causes removing '<div>2<div>3</div>4</div>' and inserting '4'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n34").length, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1[<div>2<div>]3</div>4</div>5</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"; + sel.setBaseAndExtent(aElement.firstChild.firstChild, 1, aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (partially #2) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>13<div>4</div>5</div>", description + " should remove '<div>2</div>'"); + // It causes removing '1<div>2<div>3</div></div>' and inserting '13<div>'. + checkTextChangeNotification(notifications[0], description, { offset: kLFLen + offsetAtStart, removedLength: getNativeText("1\n2\n3").length, addedLength: getNativeText("13\n").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2<div>3[</div>4</div>]5</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(2), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (partially #3) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>2<div>35</div></div></div>", description + " should remove '4'"); + // It causes removing '45' and inserting '5'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, removedLength: 2, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>" + description = aDescription + "inserting HTML which has a pair of nested block elements"; + waitNotifications = promiseReceiveNotifications(); + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + checkTextChangeNotification(notifications[0], description, { offset: 0 + offsetAtStart, removedLength: getNativeText("\n1\n2\n35").length, addedLength: getNativeText("\n1\n2\n345\n6\n789").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: 0 + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2<div>3[</div>4</div>5<div>6<div>]7</div>8</div>9</div>" and removing selection + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).childNodes.item(1).firstChild, 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between same level descendants) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>2<div>37</div></div><div>8</div>9</div>", description + " should remove '456<div>7'"); + // It causes removing '<div>3</div>4</div>5<div>6<div>7</div>' and inserting '<div>37</div><div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n7").length, addedLength: getNativeText("\n37\n").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2[<div>3</div>4</div>5<div>6<div>]7</div>8</div>9</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).childNodes.item(1).firstChild, 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between different level descendants #1) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>27</div><div>8</div>9</div>", description + " should remove '<div>2<div>3</div>4</div>5<div>6<div>7</div>'"); + // It causes removing '<div>2<div>3</div>4</div>5<div>6<div>7</div>' and inserting '<div>27</div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1").length + offsetAtStart, removedLength: getNativeText("\n2\n345\n6\n7").length, addedLength: getNativeText("\n27\n").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2[<div>3</div>4</div>5<div>6<div>7</div>8</div>]9</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(4), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between different level descendants #2) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>29</div></div>", description + " should remove '<div>3</div>4</div>5<div>6<div>7</div>8</div>'"); + // It causes removing '<div>3</div>4</div>5</div>6<div>7</div>8</div>9' and inserting '9</div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n789").length, addedLength: 1 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2<div>3[</div>4</div>5<div>]6<div>7</div>8</div>9</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(3).firstChild, 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between different level descendants #3) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>2<div>36<div>7</div>8</div></div>9</div>", description + " should remove '<div>36<div>7</div>8</div>'"); + // It causes removing '<div>3</div>4</div>5<div>6<div>7</div>8</div>' and inserting '<div>36<div>7</div>8</div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2").length + offsetAtStart, removedLength: getNativeText("\n345\n6\n78").length, addedLength: getNativeText("\n36\n78").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<div>1<div>2<div>3[</div>4</div>5<div>6<div>7</div>8</div>]9</div>" and removing selection + aElement.innerHTML = "<div>1<div>2<div>3</div>4</div>5<div>6<div>7</div>8</div>9</div>"; + sel.setBaseAndExtent(aElement.firstChild.childNodes.item(1).childNodes.item(1).firstChild, 1, aElement.firstChild.childNodes.item(4), 0); + await flushNotifications(); + description = aDescription + "deleting child nodes (between different level descendants #4) with pressing Delete key"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Delete", {}, win, callback); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<div>1<div>2<div>39</div></div></div>", description + " should remove '</div>4</div>5<div>6<div>7</div>8</div>'"); + // It causes removing '</div>4</div>5<div>6<div>7</div>8</div>' and inserting '<div>36<div>7</div>8</div>'. + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, removedLength: getNativeText("45\n6\n789").length, addedLength: getNativeText("9").length }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\n1\n2\n3").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<p>abc</p><p><br></p><p>{<br>}</p>" and removing second paragraph with DOM API + aElement.innerHTML = "<p>abc</p><p><br></p><p><br></p>"; + sel.collapse(aElement.firstChild.nextSibling.nextSibling, 0); + await flushNotifications(); + description = aDescription + "deleting previous paragraph with DOM API"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Unidentified", { code: "" }, win, callback); // For setting the callback to recode notifications + aElement.firstChild.nextSibling.remove(); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<p>abc</p><p><br></p>", description + " the second paragraph should've been removed"); + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\nabc").length + offsetAtStart, removedLength: getNativeText("\n\n").length, addedLength: 0 }); + checkSelectionChangeNotification(notifications[1], description, { offset: getNativeText("\nabc\n").length + offsetAtStart, text: "" }); + checkPositionChangeNotification(notifications[2], description); + dumpUnexpectedNotifications(description, 3); + + // "<p>abc</p><p>{<br>}</p><p><br></p>" and removing last paragraph with DOM API + aElement.innerHTML = "<p>abc</p><p><br></p><p><br></p>"; + sel.collapse(aElement.firstChild.nextSibling, 0); + await flushNotifications(); + description = aDescription + "deleting next paragraph with DOM API"; + waitNotifications = promiseReceiveNotifications(); + synthesizeKey("KEY_Unidentified", { code: "" }, win, callback); // For setting the callback to recode notifications + aElement.firstChild.nextSibling.nextSibling.remove(); + await waitNotifications; + ensureToRemovePrecedingPositionChangeNotification(); + is(aElement.innerHTML, "<p>abc</p><p><br></p>", description + " the last paragraph should've been removed"); + checkTextChangeNotification(notifications[0], description, { offset: getNativeText("\nabc\n\n").length + offsetAtStart, removedLength: getNativeText("\n\n").length, addedLength: 0 }); + checkPositionChangeNotification(notifications[1], description); + dumpUnexpectedNotifications(description, 2); + } + + await testWithPlaintextEditor("runIMEContentObserverTest with input element: ", input, false); + await testWithPlaintextEditor("runIMEContentObserverTest with textarea element: ", textarea, true); + await testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is br): ", contenteditable, "br"); + await testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is p): ", contenteditable, "p"); + await testWithHTMLEditor("runIMEContentObserverTest with contenteditable (defaultParagraphSeparator is div): ", contenteditable, "div"); + // XXX Due to the difference of HTML editor behavior between designMode and contenteditable, + // testWithHTMLEditor() gets some unexpected behavior. However, IMEContentObserver is + // not depend on editor's detail. So, we should investigate this issue later. It's not + // so important for now. + // await testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is br): ", iframe2.contentDocument.body, "br"); + // await testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is p): ", iframe2.contentDocument.body, "p"); + // await testWithHTMLEditor("runIMEContentObserverTest in designMode (defaultParagraphSeparator is div): ", iframe2.contentDocument.body, "div"); +} + +async function runPasswordMaskDelayTest() { + await SpecialPowers.pushPrefEnv({ + set: [["editor.password.mask_delay", 600], + ["editor.password.testing.mask_delay", true], + ], + }); + + let iframe5 = document.getElementById("iframe5"); + let iframe6 = document.getElementById("iframe6"); + let inputWindow = iframe5.contentWindow; + let passwordWindow = iframe6.contentWindow; + + let inputElement = iframe5.contentDocument.getElementById("input"); + let passwordElement = iframe6.contentDocument.getElementById("password"); + + const kMask = passwordElement.editor.passwordMask; + + function promiseAllPasswordMasked() { + return new Promise(resolve => { + passwordElement.addEventListener("MozLastInputMasked", resolve, {once: true}); + }); + } + + function checkSnapshots(aResult, aReference, aMatch, aDescription) { + let [correct, data1, data2] = compareSnapshots(aResult, aReference, true); + is(correct, aMatch, `${aDescription}\nREFTEST IMAGE 1 (TEST): ${data1}\nREFTEST IMAGE 2 (REFERENCE): ${data2}`); + } + + // First character input + passwordElement.value = ""; + passwordElement.focus(); + let waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeKey("a"); + let unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + let maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = "a"; + inputElement.focus(); + inputElement.setSelectionRange(1, 1); + let unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = kMask; + inputElement.setSelectionRange(1, 1); + let maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): first inputted character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): first inputted character should be masked after a while"); + + // Second character input + passwordElement.value = "a"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeKey("b"); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = `${kMask}b`; + inputElement.focus(); + inputElement.setSelectionRange(2, 2); + unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = `${kMask}${kMask}`; + inputElement.setSelectionRange(2, 2); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): second inputted character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): second inputted character should be masked after a while"); + + // Typing new character should mask the previous unmasked characters + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(2, 2); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeKey("c"); + synthesizeKey("d"); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = `${kMask}${kMask}${kMask}d`; + inputElement.focus(); + inputElement.setSelectionRange(4, 4); + unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = `${kMask}${kMask}${kMask}${kMask}`; + inputElement.setSelectionRange(4, 4); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): forth character input should mask the third character"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): forth inputted character should be masked after a while"); + + // Typing middle of password should unmask the last input character + passwordElement.value = "abcd"; + passwordElement.focus(); + passwordElement.setSelectionRange(2, 2); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeKey("e"); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = `${kMask}${kMask}e${kMask}${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(3, 3); + unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = `${kMask}${kMask}${kMask}${kMask}${kMask}`; + inputElement.setSelectionRange(3, 3); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): inserted character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): inserted character should be masked after a while"); + + // Composition string should be unmasked for a while, and shouldn't be committed at masking + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "c", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + is(getEditor(passwordElement).composing, true, + "runPasswordMaskDelayTest(): composition shouldn't be commited at masking the composing string #1"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + inputElement.value = `${kMask}${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(1, 1); + synthesizeCompositionChange( + { composition: + { string: "c", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + unmaskedReference = await snapshotWindow(inputWindow, true); + synthesizeCompositionChange( + { composition: + { string: kMask, + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): composing character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): composing character should be masked after a while"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + // Updating composition string should unmask the composition string for a while + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "c", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + await waitForMaskingLastInput; + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "d", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + is(getEditor(passwordElement).composing, true, + "runPasswordMaskDelayTest(): composition shouldn't be commited at masking the composing string #2"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + inputElement.value = `${kMask}${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(1, 1); + synthesizeCompositionChange( + { composition: + { string: "d", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + unmaskedReference = await snapshotWindow(inputWindow, true); + synthesizeCompositionChange( + { composition: + { string: kMask, + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): updated composing character should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): updated composing character should be masked after a while"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + // Composing multi-characters should be unmasked for a while. + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "c", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + }); + await waitForMaskingLastInput; + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "cd", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + }); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + is(getEditor(passwordElement).composing, true, + "runPasswordMaskDelayTest(): composition shouldn't be commited at masking the composing string #3"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + inputElement.value = `${kMask}${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(1, 1); + synthesizeCompositionChange( + { composition: + { string: "cd", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + }); + unmaskedReference = await snapshotWindow(inputWindow, true); + synthesizeCompositionChange( + { composition: + { string: `${kMask}${kMask}`, + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + }); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): all of composing string should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): all of composing string should be masked after a while"); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + + // Committing composition should make the commit string unmasked. + passwordElement.value = "ab"; + passwordElement.focus(); + passwordElement.setSelectionRange(1, 1); + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeCompositionChange( + { composition: + { string: "cd", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + }); + await waitForMaskingLastInput; + waitForMaskingLastInput = promiseAllPasswordMasked(); + synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } }); + unmaskedResult = await snapshotWindow(passwordWindow, true); + await waitForMaskingLastInput; + maskedResult = await snapshotWindow(passwordWindow, true); + + inputElement.value = `${kMask}cd${kMask}`; + inputElement.focus(); + inputElement.setSelectionRange(3, 3); + unmaskedReference = await snapshotWindow(inputWindow, true); + inputElement.value = `${kMask}${kMask}${kMask}${kMask}`; + inputElement.setSelectionRange(3, 3); + maskedReference = await snapshotWindow(inputWindow, true); + checkSnapshots(unmaskedResult, unmaskedReference, true, + "runPasswordMaskDelayTest(): committed string should be unmasked for a while"); + checkSnapshots(maskedResult, maskedReference, true, + "runPasswordMaskDelayTest(): committed string should be masked after a while"); +} + +async function runInputModeTest() +{ + let result = []; + + function handler(aEvent) + { + result.push(aEvent); + } + + textarea.inputMode = "text"; + textarea.value = ""; + textarea.focus(); + + textarea.addEventListener("compositionupdate", handler, true); + textarea.addEventListener("compositionend", handler, true); + + synthesizeCompositionChange({ + composition: {string: "a ", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + }); + + is(result[0].type, "compositionupdate", "Set initial composition for inputmode test"); + result = []; + + textarea.inputMode = "tel"; + is(result.length, 0, "No compositonend event even if inputmode is updated"); + + // Clean up + synthesizeComposition({ type: "compositioncommitasis" }); + textarea.inputMode = ""; + textarea.value = ""; + textarea.removeEventListener("compositionupdate", handler, true); + textarea.removeEventListener("compositionend", handler, true); +} + + +async function runTest() +{ + window.addEventListener("unload", window.arguments[0].SimpleTest.finish, {once: true, capture: true}); + + contenteditable = document.getElementById("iframe4").contentDocument.getElementById("contenteditable"); + windowOfContenteditable = document.getElementById("iframe4").contentWindow; + textareaInFrame = iframe.contentDocument.getElementById("textarea"); + + contenteditableBySpan = document.getElementById("iframe7").contentDocument.getElementById("contenteditable"); + windowOfContenteditableBySpan = document.getElementById("iframe7").contentWindow; + + await runIMEContentObserverTest(); + await runEditorReframeTests(); + await runAsyncForceCommitTest(); + await runRemoveContentTest(); + await runPanelTest(); + await runPasswordMaskDelayTest(); + await runBug1584901Test(); + await runInputModeTest(); + await runCompositionTest(); + await runCompositionCommitTest(); + await runSetSelectionEventTest(); + + runUndoRedoTest(); + runCompositionCommitAsIsTest(); + runCompositionEventTest(); + runCompositionTestWhoseTextNodeModified(); + runQueryTextRectInContentEditableTest(); + runCharAtPointTest(textarea, "textarea in the document"); + runCharAtPointAtOutsideTest(); + runQueryTextContentEventTest(); + runQuerySelectionEventTest(); + runQueryIMESelectionTest(); + runQueryContentEventRelativeToInsertionPoint(); + runQueryPasswordTest(); + runCSSTransformTest(); + runBug722639Test(); + runBug1375825Test(); + runBug1530649Test(); + runBug1571375Test(); + runBug1675313Test(); + runCommitCompositionWithSpaceKey(); + runCompositionWithSelectionChange(); + runForceCommitTest(); + runNestedSettingValue(); + runBug811755Test(); + runIsComposingTest(); + runRedundantChangeTest(); + runNotRedundantChangeTest(); + runNativeLineBreakerTest(); + runControlCharTest(); + runFrameTest(); + runMaxLengthTest(); + + window.close(); +} + +window.arguments[0].SimpleTest.waitForFocus(runTest, window); + +]]> +</script> + +</window> diff --git a/widget/tests/window_imestate_iframes.html b/widget/tests/window_imestate_iframes.html new file mode 100644 index 0000000000..c8b182977f --- /dev/null +++ b/widget/tests/window_imestate_iframes.html @@ -0,0 +1,358 @@ +<html> +<head> + <title>Test for IME state controling and focus moving for iframes</title> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <style type="text/css"> + iframe { + border: none; + height: 100px; + } + </style> +</head> +<body onunload="onUnload();"> +<p id="display"> + <!-- Use input[readonly] because it isn't affected by the partial focus + movement on Mac --> + <input id="prev" readonly><br> + <iframe id="iframe_not_editable" + src="data:text/html,<html><body><input id='editor'></body></html>"></iframe><br> + + <!-- Testing IME state and focus movement, the anchor elements cannot get focus --> + <iframe id="iframe_html" + src="data:text/html,<html id='editor' contenteditable='true'><body><a href='about:blank'>about:blank;</a></body></html>"></iframe><br> + <iframe id="iframe_designMode" + src="data:text/html,<body id='editor' onload='document.designMode="on";'><a href='about:blank'>about:blank;</a></body>"></iframe><br> + <iframe id="iframe_body" + src="data:text/html,<body id='editor' contenteditable='true'><a href='about:blank'>about:blank;</a></body>"></iframe><br> + <iframe id="iframe_p" + src="data:text/html,<body><p id='editor' contenteditable='true'><a href='about:blank'>about:blank;</a></p></body>"></iframe><br> + + <input id="next" readonly><br> +</p> +<script class="testbody" type="application/javascript"> + +window.opener.SimpleTest.waitForFocus(runTests, window); + +function ok(aCondition, aMessage) { + window.opener.SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) { + window.opener.SimpleTest.is(aLeft, aRight, aMessage); +} + +function onUnload() { + window.opener.onFinish(); +} + +var gFocusObservingElement = null; +var gBlurObservingElement = null; + +function onFocus(aEvent) { + if (aEvent.target != gFocusObservingElement) { + return; + } + ok(gFocusObservingElement.willFocus, + "focus event is fired on unexpected element"); + gFocusObservingElement.willFocus = false; +} + +function onBlur(aEvent) { + if (aEvent.target != gBlurObservingElement) { + return; + } + ok(gBlurObservingElement.willBlur, + "blur event is fired on unexpected element"); + gBlurObservingElement.willBlur = false; +} + +function observeFocusBlur(aNextFocusedNode, aWillFireFocusEvent, + aNextBlurredNode, aWillFireBlurEvent) { + if (gFocusObservingElement) { + if (gFocusObservingElement.willFocus) { + ok(false, "focus event was never fired on " + gFocusObservingElement); + } + gFocusObservingElement.removeEventListener("focus", onFocus, true); + gFocusObservingElement.willFocus = NaN; + gFocusObservingElement = null; + } + if (gBlurObservingElement) { + if (gBlurObservingElement.willBlur) { + ok(false, "blur event was never fired on " + gBlurObservingElement); + } + gBlurObservingElement.removeEventListener("blur", onBlur, true); + gBlurObservingElement.willBlur = NaN; + gBlurObservingElement = null; + } + if (aNextFocusedNode) { + gFocusObservingElement = aNextFocusedNode; + gFocusObservingElement.willFocus = aWillFireFocusEvent; + gFocusObservingElement.addEventListener("focus", onFocus, true); + } + if (aNextBlurredNode) { + gBlurObservingElement = aNextBlurredNode; + gBlurObservingElement.willBlur = aWillFireBlurEvent; + gBlurObservingElement.addEventListener("blur", onBlur, true); + } +} + +function runTests() { + var utils = window.windowUtils; + var fm = Services.focus; + + var iframe, editor, root; + var prev = document.getElementById("prev"); + var next = document.getElementById("next"); + var html = document.documentElement; + + function resetFocusToInput(aDescription) { + observeFocusBlur(null, false, null, false); + prev.focus(); + is(fm.focusedElement, prev, + "input#prev[readonly] element didn't get focus: " + aDescription); + is(utils.IMEStatus, utils.IME_STATUS_DISABLED, + "IME enabled on input#prev[readonly]: " + aDescription); + } + + function resetFocusToParentHTML(aDescription) { + observeFocusBlur(null, false, null, false); + html.focus(); + is(fm.focusedElement, html, + "Parent html element didn't get focus: " + aDescription); + is(utils.IMEStatus, utils.IME_STATUS_DISABLED, + "IME enabled on parent html element: " + aDescription); + } + + function testTabKey(aForward, + aNextFocusedNode, aWillFireFocusEvent, + aNextBlurredNode, aWillFireBlurEvent, + aIMEShouldBeEnabled, aTestingCaseDescription) { + observeFocusBlur(aNextFocusedNode, aWillFireFocusEvent, + aNextBlurredNode, aWillFireBlurEvent); + synthesizeKey("VK_TAB", { shiftKey: !aForward }); + var description = "Tab key test: "; + if (!aForward) { + description = "Shift-" + description; + } + description += aTestingCaseDescription + ": "; + is(fm.focusedElement, aNextFocusedNode, + description + "didn't move focus as expected"); + is(utils.IMEStatus, + aIMEShouldBeEnabled ? + utils.IME_STATUS_ENABLED : utils.IME_STATUS_DISABLED, + description + "didn't set IME state as expected"); + } + + function testMouseClick(aNextFocusedNode, aWillFireFocusEvent, + aWillAllNodeLostFocus, + aNextBlurredNode, aWillFireBlurEvent, + aIMEShouldBeEnabled, aTestingCaseDescription) { + observeFocusBlur(aNextFocusedNode, aWillFireFocusEvent, + aNextBlurredNode, aWillFireBlurEvent); + // We're relying on layout inside the iframe being up to date, so make it so + iframe.contentDocument.documentElement.getBoundingClientRect(); + synthesizeMouse(iframe, 10, 80, { }); + var description = "Click test: " + aTestingCaseDescription + ": "; + is(fm.focusedElement, !aWillAllNodeLostFocus ? aNextFocusedNode : null, + description + "didn't move focus as expected"); + is(utils.IMEStatus, + aIMEShouldBeEnabled ? + utils.IME_STATUS_ENABLED : utils.IME_STATUS_DISABLED, + description + "didn't set IME state as expected"); + } + + function testOnEditorFlagChange(aDescription, aIsInDesignMode) { + const kReadonly = Ci.nsIEditor.eEditorReadonlyMask; + var description = "testOnEditorFlagChange: " + aDescription; + resetFocusToParentHTML(description); + var htmlEditor = iframe.contentWindow.docShell.editor; + var e = aIsInDesignMode ? root : editor; + e.focus(); + is(fm.focusedElement, e, + description + ": focus() of editor didn't move focus as expected"); + is(utils.IMEStatus, utils.IME_STATUS_ENABLED, + description + ": IME isn't enabled when the editor gets focus"); + var flags = htmlEditor.flags; + htmlEditor.flags |= kReadonly; + is(fm.focusedElement, e, + description + ": when editor becomes readonly, focus moved unexpectedly"); + is(utils.IMEStatus, utils.IME_STATUS_DISABLED, + description + ": when editor becomes readonly, IME is still enabled"); + htmlEditor.flags = flags; + is(fm.focusedElement, e, + description + ": when editor becomes read-write, focus moved unexpectedly"); + is(utils.IMEStatus, utils.IME_STATUS_ENABLED, + description + ": when editor becomes read-write, IME is still disabled"); + } + + // hide all iframes + document.getElementById("iframe_not_editable").style.display = "none"; + document.getElementById("iframe_html").style.display = "none"; + document.getElementById("iframe_designMode").style.display = "none"; + document.getElementById("iframe_body").style.display = "none"; + document.getElementById("iframe_p").style.display = "none"; + + // non editable HTML element and input element can get focus. + iframe = document.getElementById("iframe_not_editable"); + iframe.style.display = "inline"; + editor = iframe.contentDocument.getElementById("editor"); + root = iframe.contentDocument.documentElement; + resetFocusToInput("initializing for iframe_not_editable"); + + testTabKey(true, root, false, prev, true, + false, "input#prev[readonly] -> html"); + testTabKey(true, editor, true, root, false, + true, "html -> input in the subdoc"); + testTabKey(true, next, true, editor, true, + false, "input in the subdoc -> input#next[readonly]"); + testTabKey(false, editor, true, next, true, + true, "input#next[readonly] -> input in the subdoc"); + testTabKey(false, root, false, editor, true, + false, "input in the subdoc -> html"); + testTabKey(false, prev, true, root, false, + false, "html -> input#next[readonly]"); + + iframe.style.display = "none"; + + // HTML element (of course, it's root) must enables IME. + iframe = document.getElementById("iframe_html"); + iframe.style.display = "inline"; + editor = iframe.contentDocument.getElementById("editor"); + root = iframe.contentDocument.documentElement; + resetFocusToInput("initializing for iframe_html"); + + testTabKey(true, editor, true, prev, true, + true, "input#prev[readonly] -> html[contentediable=true]"); + testTabKey(true, next, true, editor, true, + false, "html[contentediable=true] -> input#next[readonly]"); + testTabKey(false, editor, true, next, true, + true, "input#next[readonly] -> html[contentediable=true]"); + testTabKey(false, prev, true, editor, true, + false, "html[contenteditable=true] -> input[readonly]"); + + prev.style.display = "none"; + resetFocusToParentHTML("testing iframe_html"); + testTabKey(true, editor, true, html, false, + true, "html of parent -> html[contentediable=true]"); + testTabKey(false, html, false, editor, true, + false, "html[contenteditable=true] -> html of parent"); + prev.style.display = "inline"; + resetFocusToInput("after parent html <-> html[contenteditable=true]"); + + testMouseClick(editor, true, false, prev, true, true, "iframe_html"); + + testOnEditorFlagChange("html[contentediable=true]", false); + + iframe.style.display = "none"; + + // designMode should behave like <html contenteditable="true"></html> + // but focus/blur events shouldn't be fired on its root element because + // any elements shouldn't be focused state in designMode. + iframe = document.getElementById("iframe_designMode"); + iframe.style.display = "inline"; + iframe.contentDocument.designMode = "on"; + editor = iframe.contentDocument.getElementById("editor"); + root = iframe.contentDocument.documentElement; + resetFocusToInput("initializing for iframe_designMode"); + + testTabKey(true, root, false, prev, true, + true, "input#prev[readonly] -> html in designMode"); + testTabKey(true, next, true, root, false, + false, "html in designMode -> input#next[readonly]"); + testTabKey(false, root, false, next, true, + true, "input#next[readonly] -> html in designMode"); + testTabKey(false, prev, true, root, false, + false, "html in designMode -> input#prev[readonly]"); + + prev.style.display = "none"; + resetFocusToParentHTML("testing iframe_designMode"); + testTabKey(true, root, false, html, false, + true, "html of parent -> html in designMode"); + testTabKey(false, html, false, root, false, + false, "html in designMode -> html of parent"); + prev.style.display = "inline"; + resetFocusToInput("after parent html <-> html in designMode"); + + testMouseClick(editor, false, true, prev, true, true, "iframe_designMode"); + + testOnEditorFlagChange("html in designMode", true); + + iframe.style.display = "none"; + + // When there is no HTML element but the BODY element is editable, + // the body element should get focus and enables IME. + iframe = document.getElementById("iframe_body"); + iframe.style.display = "inline"; + editor = iframe.contentDocument.getElementById("editor"); + root = iframe.contentDocument.documentElement; + resetFocusToInput("initializing for iframe_body"); + + testTabKey(true, editor, true, prev, true, + true, "input#prev[readonly] -> body[contentediable=true]"); + testTabKey(true, next, true, editor, true, + false, "body[contentediable=true] -> input#next[readonly]"); + testTabKey(false, editor, true, next, true, + true, "input#next[readonly] -> body[contentediable=true]"); + testTabKey(false, prev, true, editor, true, + false, "body[contenteditable=true] -> input#prev[readonly]"); + + prev.style.display = "none"; + resetFocusToParentHTML("testing iframe_body"); + testTabKey(true, editor, true, html, false, + true, "html of parent -> body[contentediable=true]"); + testTabKey(false, html, false, editor, true, + false, "body[contenteditable=true] -> html of parent"); + prev.style.display = "inline"; + resetFocusToInput("after parent html <-> body[contenteditable=true]"); + + testMouseClick(editor, true, false, prev, true, true, "iframe_body"); + + testOnEditorFlagChange("body[contentediable=true]", false); + + iframe.style.display = "none"; + + // When HTML/BODY elements are not editable, focus shouldn't be moved to + // the editable content directly. + iframe = document.getElementById("iframe_p"); + iframe.style.display = "inline"; + editor = iframe.contentDocument.getElementById("editor"); + root = iframe.contentDocument.documentElement; + resetFocusToInput("initializing for iframe_p"); + + testTabKey(true, root, false, prev, true, + false, "input#prev[readonly] -> html (has p[contenteditable=true])"); + testTabKey(true, editor, true, root, false, + true, "html (has p[contenteditable=true]) -> p[contentediable=true]"); + testTabKey(true, next, true, editor, true, + false, "p[contentediable=true] -> input#next[readonly]"); + testTabKey(false, editor, true, next, true, + true, "input#next[readonly] -> p[contentediable=true]"); + testTabKey(false, root, false, editor, true, + false, "p[contenteditable=true] -> html (has p[contenteditable=true])"); + testTabKey(false, prev, true, root, false, + false, "html (has p[contenteditable=true]) -> input#prev[readonly]"); + prev.style.display = "none"; + + resetFocusToParentHTML("testing iframe_p"); + testTabKey(true, root, false, html, false, + false, "html of parent -> html (has p[contentediable=true])"); + testTabKey(false, html, false, root, false, + false, "html (has p[contentediable=true]) -> html of parent"); + prev.style.display = "inline"; + resetFocusToInput("after parent html <-> html (has p[contentediable=true])"); + + testMouseClick(root, false, true, prev, true, false, "iframe_p"); + + testOnEditorFlagChange("p[contenteditable=true]", false); + + iframe.style.display = "none"; + + window.close(); +} + +</script> +</body> + +</html> diff --git a/widget/tests/window_mouse_scroll_win.html b/widget/tests/window_mouse_scroll_win.html new file mode 100644 index 0000000000..bf90abb1b5 --- /dev/null +++ b/widget/tests/window_mouse_scroll_win.html @@ -0,0 +1,1516 @@ +<html lang="en-US" + style="font-family: Arial; font-size: 10px; line-height: 16px;"> +<head> + <title>Test for mouse scroll handling on Windows</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body onunload="onUnload();"> +<div id="display" style="width: 5000px; height: 5000px;"> +<p id="p1" style="font-size: 16px; width: 100px; height: 100px;">1st <p>.</p> +<p id="p2" style="font-size: 32px; width: 100px; height: 100px;">2nd <p>.</p> +</div> +<script class="testbody" type="application/javascript"> + +window.arguments[0].SimpleTest.waitForFocus(prepareTests, window); + +const nsIDOMWindowUtils = Ci.nsIDOMWindowUtils; + +const WHEEL_PAGESCROLL = 4294967295; + +const WM_VSCROLL = 0x0115; +const WM_HSCROLL = 0x0114; +const WM_MOUSEWHEEL = 0x020A; +const WM_MOUSEHWHEEL = 0x020E; + +const SB_LINEUP = 0; +const SB_LINELEFT = 0; +const SB_LINEDOWN = 1; +const SB_LINERIGHT = 1; +const SB_PAGEUP = 2; +const SB_PAGELEFT = 2; +const SB_PAGEDOWN = 3; +const SB_PAGERIGHT = 3; + +const SHIFT_L = 0x0100; +const SHIFT_R = 0x0200; +const CTRL_L = 0x0400; +const CTRL_R = 0x0800; +const ALT_L = 0x1000; +const ALT_R = 0x2000; + +const DOM_PAGE_SCROLL_DELTA = 32768; + +const kSystemScrollSpeedOverridePref = "mousewheel.system_scroll_override.enabled"; + +const kAltKeyActionPref = "mousewheel.with_alt.action"; +const kCtrlKeyActionPref = "mousewheel.with_control.action"; +const kShiftKeyActionPref = "mousewheel.with_shift.action"; +const kWinKeyActionPref = "mousewheel.with_meta.action"; + +const kAltKeyDeltaMultiplierXPref = "mousewheel.with_alt.delta_multiplier_x"; +const kAltKeyDeltaMultiplierYPref = "mousewheel.with_alt.delta_multiplier_y"; +const kCtrlKeyDeltaMultiplierXPref = "mousewheel.with_control.delta_multiplier_x"; +const kCtrlKeyDeltaMultiplierYPref = "mousewheel.with_control.delta_multiplier_y"; +const kShiftKeyDeltaMultiplierXPref = "mousewheel.with_shift.delta_multiplier_x"; +const kShiftKeyDeltaMultiplierYPref = "mousewheel.with_shift.delta_multiplier_y"; +const kWinKeyDeltaMultiplierXPref = "mousewheel.with_meta.delta_multiplier_x"; +const kWinKeyDeltaMultiplierYPref = "mousewheel.with_meta.delta_multiplier_y"; + +const kEmulateWheelByWMSCROLLPref = "mousewheel.emulate_at_wm_scroll"; +const kVAmountPref = "mousewheel.windows.vertical_amount_override"; +const kHAmountPref = "mousewheel.windows.horizontal_amount_override"; +const kTimeoutPref = "mousewheel.windows.transaction.timeout"; + +const kMouseLineScrollEvent = "DOMMouseScroll"; +const kMousePixelScrollEvent = "MozMousePixelScroll"; + +const kVAxis = MouseScrollEvent.VERTICAL_AXIS; +const kHAxis = MouseScrollEvent.HORIZONTAL_AXIS; + +var gLineHeight = 0; +var gCharWidth = 0; +var gPageHeight = 0; +var gPageWidth = 0; + +var gP1 = document.getElementById("p1"); +var gP2 = document.getElementById("p2"); + +var gOtherWindow; + +function ok(aCondition, aMessage) { + window.arguments[0].SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) { + window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) { + window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function todo_is(aLeft, aRight, aMessage) { + window.arguments[0].SimpleTest.todo_is(aLeft, aRight, aMessage); +} + +function onUnload() { + SpecialPowers.clearUserPref(kAltKeyActionPref); + SpecialPowers.clearUserPref(kCtrlKeyActionPref); + SpecialPowers.clearUserPref(kShiftKeyActionPref); + SpecialPowers.clearUserPref(kWinKeyActionPref); + + SpecialPowers.clearUserPref(kAltKeyDeltaMultiplierXPref); + SpecialPowers.clearUserPref(kAltKeyDeltaMultiplierYPref); + SpecialPowers.clearUserPref(kCtrlKeyDeltaMultiplierXPref); + SpecialPowers.clearUserPref(kCtrlKeyDeltaMultiplierYPref); + SpecialPowers.clearUserPref(kShiftKeyDeltaMultiplierXPref); + SpecialPowers.clearUserPref(kShiftKeyDeltaMultiplierYPref); + SpecialPowers.clearUserPref(kWinKeyDeltaMultiplierXPref); + SpecialPowers.clearUserPref(kWinKeyDeltaMultiplierYPref); + + SpecialPowers.clearUserPref(kSystemScrollSpeedOverridePref); + SpecialPowers.clearUserPref(kEmulateWheelByWMSCROLLPref); + SpecialPowers.clearUserPref(kVAmountPref); + SpecialPowers.clearUserPref(kHAmountPref); + SpecialPowers.clearUserPref(kTimeoutPref); + window.arguments[0].SimpleTest.finish(); +} + +function getWindowUtils(aWindow) { + if (!aWindow) { + aWindow = window; + } + return aWindow.windowUtils; +} + +function getPointInScreen(aElement, aWindow) { + if (!aWindow) { + aWindow = window; + } + var bounds = aElement.getBoundingClientRect(); + return { x: bounds.left + aWindow.mozInnerScreenX, + y: bounds.top + aWindow.mozInnerScreenY }; +} + +function cut(aNum) { + return (aNum >= 0) ? Math.floor(aNum) : Math.ceil(aNum); +} + +/** + * Make each steps for the tests in following arrays in global scope. Each item + * of the arrays will be executed after previous test is finished. + * + * description: + * Set the description of the test. This will be used for the message of is() + * or the others. + * + * message: + * aNativeMessage of nsIDOMWindowUtils.sendNativeMouseScrollEvent(). + * Must be WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_VSCROLL or WM_HSCROLL. + * + * delta: + * The native delta value for WM_MOUSEWHEEL or WM_MOUSEHWHEEL. + * Or one of the SB_* const value for WM_VSCROLL or WM_HSCROLL. + * + * target: + * The target element, under the mouse cursor. + * + * window: + * The window which is used for getting nsIDOMWindowUtils. + * + * modifiers: + * Pressed modifier keys, 0 means no modifier key is pressed. + * Otherwise, one or more values of SHIFT_L, SHIFT_R, CTRL_L, CTRL_R, + * ALT_L or ALT_R. + * + * additionalFlags: + * aAdditionalFlags of nsIDOMWindowUtils.sendNativeMouseScrollEvent(). + * See the document of nsIDOMWindowUtils for the detail of the values. + * + * onLineScrollEvent: + * Must be a function or null. + * If the value is a function, it will be called when DOMMouseScroll event + * is received by the synthesized event. + * If return true, the common checks are canceled. + * + * onPixelScrollEvent: + * Must be a function or null. + * If the value is a function, it will be called when MozMousePixelScroll + * event is received by the synthesized event. + * If return true, the common checks are canceled. + * + * expected: + * Must not be null and this must have: + * axis: + * kVAxis if the synthesized event causes vertical scroll. Otherwise, + * it causes horizontal scroll, kHAxis. + * lines: + * Integer value which is expected detail attribute value of + * DOMMouseScroll. If the event shouldn't be fired, must be 0. + * pixels: + * Integer value or a function which returns double value. The value is + * expected detail attribute value of MozMousePixelScroll. + * If the event shouldn't be fired, must be 0. + * + * Note that if both lines and pixels are 0, the test framework waits + * a few seconds. After that, go to next test. + * + * init: + * Must be a function or null. If this value is a function, it's called + * before synthesizing the native event. + * + * finish: + * Must be a function or null. If this value is a function, it's called + * after received all expected events or timeout if no events are expected. + */ + +// First, get the computed line height, char width, page height and page width. +var gPreparingSteps = [ + { description: "Preparing gLineHeight", + message: WM_MOUSEWHEEL, delta: -120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + onLineScrollEvent(aEvent) { + return true; + }, + onPixelScrollEvent(aEvent) { + gLineHeight = aEvent.detail; + return true; + }, + expected: { + axis: kVAxis, lines: 1, pixels: 1, + }, + init() { + SpecialPowers.setIntPref(kVAmountPref, 1); + SpecialPowers.setIntPref(kHAmountPref, 1); + }, + }, + { description: "Preparing gCharWidth", + message: WM_MOUSEHWHEEL, delta: 120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + onLineScrollEvent(aEvent) { + return true; + }, + onPixelScrollEvent(aEvent) { + gCharWidth = aEvent.detail; + return true; + }, + expected: { + axis: kVAxis, lines: 1, pixels: 1, + }, + }, + { description: "Preparing gPageHeight", + message: WM_MOUSEWHEEL, delta: -120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + onLineScrollEvent(aEvent) { + return true; + }, + onPixelScrollEvent(aEvent) { + gPageHeight = aEvent.detail; + return true; + }, + expected: { + axis: kHAxis, lines: 1, pixels: 1, + }, + init() { + SpecialPowers.setIntPref(kVAmountPref, 0xFFFF); + SpecialPowers.setIntPref(kHAmountPref, 0xFFFF); + }, + }, + { description: "Preparing gPageWidth", + message: WM_MOUSEHWHEEL, delta: 120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + onLineScrollEvent(aEvent) { + return true; + }, + onPixelScrollEvent(aEvent) { + gPageWidth = aEvent.detail; + return true; + }, + expected: { + axis: kHAxis, lines: 1, pixels: 1, + }, + finish() { + ok(gLineHeight > 0, "gLineHeight isn't positive got " + gLineHeight); + ok(gCharWidth > 0, "gCharWidth isn't positive got " + gCharWidth); + ok(gPageHeight > 0, "gPageHeight isn't positive got " + gPageHeight); + ok(gPageWidth > 0, "gPageWidth isn't positive got " + gPageWidth); + + ok(gPageHeight > gLineHeight, + "gPageHeight must be larger than gLineHeight"); + ok(gPageWidth > gCharWidth, + "gPageWidth must be larger than gCharWidth"); + runNextTest(gBasicTests, 0); + }, + }, +]; + +var gBasicTests = [ + // Widget shouldn't dispatch a pixel event if the delta can be devided by + // lines to be scrolled. However, pixel events should be fired by ESM. + { description: "WM_MOUSEWHEEL, -120, 3 lines", + message: WM_MOUSEWHEEL, delta: -120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 3, pixels() { return gLineHeight * 3; }, + }, + init() { + SpecialPowers.setIntPref(kVAmountPref, 3); + SpecialPowers.setIntPref(kHAmountPref, 3); + }, + }, + + { description: "WM_MOUSEWHEEL, 120, -3 lines", + message: WM_MOUSEWHEEL, delta: 120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: -3, pixels() { return gLineHeight * -3; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, 120, 3 chars", + message: WM_MOUSEHWHEEL, delta: 120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 3, pixels() { return gCharWidth * 3; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, -120, -3 chars", + message: WM_MOUSEHWHEEL, delta: -120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: -3, pixels() { return gCharWidth * -3; }, + }, + }, + + // Pixel scroll event should be fired always but line scroll event should be + // fired only when accumulated delta value is over a line. + { description: "WM_MOUSEWHEEL, -20, 0.5 lines", + message: WM_MOUSEWHEEL, delta: -20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels() { return gLineHeight / 2; }, + }, + }, + { description: "WM_MOUSEWHEEL, -20, 0.5 lines (pending: 0.5 lines)", + message: WM_MOUSEWHEEL, delta: -20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight / 2; }, + }, + }, + { description: "WM_MOUSEWHEEL, -20, 0.5 lines", + message: WM_MOUSEWHEEL, delta: -20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels() { return gLineHeight / 2; }, + }, + }, + + { description: "WM_MOUSEWHEEL, 20, -0.5 lines (pending: 0.5 lines)", + message: WM_MOUSEWHEEL, delta: 20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels() { return gLineHeight / -2; }, + }, + }, + { description: "WM_MOUSEWHEEL, 20, -0.5 lines (pending: -0.5 lines)", + message: WM_MOUSEWHEEL, delta: 20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: -1, pixels() { return -gLineHeight / 2; }, + }, + }, + { description: "WM_MOUSEWHEEL, 20, -0.5 lines", + message: WM_MOUSEWHEEL, delta: 20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels() { return gLineHeight / -2; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, 20, 0.5 chars", + message: WM_MOUSEHWHEEL, delta: 20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels() { return gCharWidth / 2; }, + }, + }, + { description: "WM_MOUSEHWHEEL, 20, 0.5 chars (pending: 0.5 chars)", + message: WM_MOUSEHWHEEL, delta: 20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth / 2; }, + }, + }, + { description: "WM_MOUSEHWHEEL, 20, 0.5 chars", + message: WM_MOUSEHWHEEL, delta: 20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels() { return gCharWidth / 2; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, -20, -0.5 chars (pending: 0.5 chars)", + message: WM_MOUSEHWHEEL, delta: -20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels() { return gCharWidth / -2; }, + }, + }, + { description: "WM_MOUSEHWHEEL, -20, -0.5 chars (pending: -0.5 chars)", + message: WM_MOUSEHWHEEL, delta: -20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: -1, pixels() { return -gCharWidth / 2; }, + }, + }, + { description: "WM_MOUSEHWHEEL, -20, -0.5 chars", + message: WM_MOUSEHWHEEL, delta: -20, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels() { return gCharWidth / -2; }, + }, + }, + + // Even if the mouse cursor is an element whose font-size is different than + // the scrollable element, the pixel scroll amount shouldn't be changed. + // Widget shouldn't dispatch a pixel event if the delta can be devided by + // lines to be scrolled. However, pixel events should be fired by ESM. + { description: "WM_MOUSEWHEEL, -120, 3 lines, on the other div whose font-size is larger", + message: WM_MOUSEWHEEL, delta: -120, + target: gP2, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 3, pixels() { return gLineHeight * 3; }, + }, + }, + + { description: "WM_MOUSEWHEEL, 120, -3 lines, on the other div whose font-size is larger", + message: WM_MOUSEWHEEL, delta: 120, + target: gP2, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: -3, pixels() { return gLineHeight * -3; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, 120, 3 chars, on the other div whose font-size is larger", + message: WM_MOUSEHWHEEL, delta: 120, + target: gP2, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 3, pixels() { return gCharWidth * 3; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, -120, -3 chars, on the other div whose font-size is larger", + message: WM_MOUSEHWHEEL, delta: -120, + target: gP2, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: -3, pixels() { return gCharWidth * -3; }, + }, + }, + + // Modifier key tests + { description: "WM_MOUSEWHEEL, -40, 1 line with left Shift", + message: WM_MOUSEWHEEL, delta: -40, + target: gP1, x: 10, y: 10, window, + modifiers: SHIFT_L, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + { description: "WM_MOUSEWHEEL, -40, 1 line with right Shift", + message: WM_MOUSEWHEEL, delta: -40, + target: gP1, x: 10, y: 10, window, + modifiers: SHIFT_R, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + { description: "WM_MOUSEWHEEL, -40, 1 line with left Ctrl", + message: WM_MOUSEWHEEL, delta: -40, + target: gP1, x: 10, y: 10, window, + modifiers: CTRL_L, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + { description: "WM_MOUSEWHEEL, -40, 1 line with right Ctrl", + message: WM_MOUSEWHEEL, delta: -40, + target: gP1, x: 10, y: 10, window, + modifiers: CTRL_R, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + { description: "WM_MOUSEWHEEL, -40, 1 line with left Alt", + message: WM_MOUSEWHEEL, delta: -40, + target: gP1, x: 10, y: 10, window, + modifiers: ALT_L, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + { description: "WM_MOUSEWHEEL, -40, 1 line with right Alt", + message: WM_MOUSEWHEEL, delta: -40, + target: gP1, x: 10, y: 10, window, + modifiers: ALT_R, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, 40, 1 character with left Shift", + message: WM_MOUSEHWHEEL, delta: 40, + target: gP1, x: 10, y: 10, window, + modifiers: SHIFT_L, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_MOUSEHWHEEL, 40, 1 character with right Shift", + message: WM_MOUSEHWHEEL, delta: 40, + target: gP1, x: 10, y: 10, window, + modifiers: SHIFT_R, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_MOUSEHWHEEL, 40, 1 character with left Ctrl", + message: WM_MOUSEHWHEEL, delta: 40, + target: gP1, x: 10, y: 10, window, + modifiers: CTRL_L, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_MOUSEHWHEEL, 40, 1 character with right Ctrl", + message: WM_MOUSEHWHEEL, delta: 40, + target: gP1, x: 10, y: 10, window, + modifiers: CTRL_R, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_MOUSEHWHEEL, 40, 1 character with left Alt", + message: WM_MOUSEHWHEEL, delta: 40, + target: gP1, x: 10, y: 10, window, + modifiers: ALT_L, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_MOUSEHWHEEL, 40, 1 character with right Alt", + message: WM_MOUSEHWHEEL, delta: 40, + target: gP1, x: 10, y: 10, window, + modifiers: ALT_R, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + + finish() { + runNextTest(gScrollMessageTests, 0); + }, + }, +]; + +var gPageScrllTests = [ + // Pixel scroll event should be fired always but line scroll event should be + // fired only when accumulated delta value is over a line. + { description: "WM_MOUSEWHEEL, -60, 0.5 pages", + message: WM_MOUSEWHEEL, delta: -60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels() { return gPageHeight / 2; }, + }, + }, + { description: "WM_MOUSEWHEEL, -60, 0.5 pages (pending: 0.5 pages)", + message: WM_MOUSEWHEEL, delta: -60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: DOM_PAGE_SCROLL_DELTA, + pixels() { return ((gPageHeight / 2) + (gPageHeight % 2)); }, + }, + }, + { description: "WM_MOUSEWHEEL, -60, 0.5 pages", + message: WM_MOUSEWHEEL, delta: -60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels() { return gPageHeight / 2; }, + }, + }, + + { description: "WM_MOUSEWHEEL, 60, -0.5 pages (pending: 0.5 pages)", + message: WM_MOUSEWHEEL, delta: 60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels() { return gPageHeight / -2; }, + }, + }, + { description: "WM_MOUSEWHEEL, 60, -0.5 pages (pending: -0.5 pages)", + message: WM_MOUSEWHEEL, delta: 60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: -DOM_PAGE_SCROLL_DELTA, + pixels() { return -((gPageHeight / 2) + (gPageHeight % 2)); }, + }, + }, + { description: "WM_MOUSEWHEEL, 60, -0.5 pages", + message: WM_MOUSEWHEEL, delta: 60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels() { return gPageHeight / -2; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, 60, 0.5 pages", + message: WM_MOUSEHWHEEL, delta: 60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels() { return gPageWidth / 2; }, + }, + }, + { description: "WM_MOUSEHWHEEL, 60, 0.5 pages (pending: 0.5 pages)", + message: WM_MOUSEHWHEEL, delta: 60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: DOM_PAGE_SCROLL_DELTA, + pixels() { return ((gPageWidth / 2) + (gPageWidth % 2)); }, + }, + }, + { description: "WM_MOUSEHWHEEL, 60, 0.5 pages", + message: WM_MOUSEHWHEEL, delta: 60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels() { return gPageWidth / 2; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, -60, -0.5 pages (pending: 0.5 pages)", + message: WM_MOUSEHWHEEL, delta: -60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels() { return gCharWidth / -2; }, + }, + }, + { description: "WM_MOUSEHWHEEL, -60, -0.5 pages (pending: -0.5 pages)", + message: WM_MOUSEHWHEEL, delta: -60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: -DOM_PAGE_SCROLL_DELTA, + pixels() { return -((gCharWidth / 2) + (gCharWidth % 2)); }, + }, + }, + { description: "WM_MOUSEHWHEEL, -60, -0.5 pages", + message: WM_MOUSEHWHEEL, delta: -60, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels() { return gCharWidth / -2; }, + }, + }, +]; + +var gScrollMessageTests = [ + // Widget should dispatch neither line scroll event nor pixel scroll event if + // the WM_*SCROLL's lParam is NULL and mouse wheel emulation is disabled. + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is NULL, emulation disabled", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels: 0, + }, + init() { + SpecialPowers.setIntPref(kVAmountPref, 3); + SpecialPowers.setIntPref(kHAmountPref, 3); + SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false); + }, + }, + + { description: "WM_VSCROLL, SB_LINEUP, lParam is NULL, emulation disabled", + message: WM_VSCROLL, delta: SB_LINEUP, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 0, pixels: 0, + }, + }, + + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is NULL, emulation disabled", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels: 0, + }, + }, + + { description: "WM_HSCROLL, SB_LINELEFT, lParam is NULL, emulation disabled", + message: WM_HSCROLL, delta: SB_LINELEFT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 0, pixels: 0, + }, + }, + + // Widget should emulate mouse wheel behavior for WM_*SCROLL even if the + // kEmulateWheelByWMSCROLLPref is disabled but the message's lParam is not + // NULL. Then, widget doesn't dispatch a pixel event for WM_*SCROLL messages, + // but ESM dispatches it instead. + { description: "WM_VSCROLL, SB_LINEUP, lParam is not NULL, emulation disabled", + message: WM_VSCROLL, delta: SB_LINEUP, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: -1, pixels() { return -gLineHeight; }, + }, + init() { + SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false); + }, + }, + + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINELEFT, lParam is not NULL, emulation disabled", + message: WM_HSCROLL, delta: SB_LINELEFT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: -1, pixels() { return -gCharWidth; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + + { description: "WM_VSCROLL, SB_PAGEUP, lParam is not NULL, emulation disabled", + message: WM_VSCROLL, delta: SB_PAGEUP, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: -DOM_PAGE_SCROLL_DELTA, + pixels() { return -gPageHeight; }, + }, + }, + + { description: "WM_VSCROLL, SB_PAGEDOWN, lParam is not NULL, emulation disabled", + message: WM_VSCROLL, delta: SB_PAGEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: DOM_PAGE_SCROLL_DELTA, + pixels() { return gPageHeight; }, + }, + }, + + { description: "WM_HSCROLL, SB_PAGELEFT, lParam is not NULL, emulation disabled", + message: WM_HSCROLL, delta: SB_PAGELEFT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: -DOM_PAGE_SCROLL_DELTA, + pixels() { return -gPageWidth; }, + }, + }, + + { description: "WM_HSCROLL, SB_PAGERIGHT, lParam is not NULL, emulation disabled", + message: WM_HSCROLL, delta: SB_PAGERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: DOM_PAGE_SCROLL_DELTA, + pixels() { return gPageWidth; }, + }, + }, + + // Widget should emulate mouse wheel behavior for WM_*SCROLL when the + // kEmulateWheelByWMSCROLLPref is enabled even if the message's lParam is + // NULL. Then, widget doesn't dispatch a pixel event for WM_*SCROLL messages, + // but ESM dispatches it instead. + { description: "WM_VSCROLL, SB_LINEUP, lParam is NULL, emulation enabled", + message: WM_VSCROLL, delta: SB_LINEUP, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: -1, pixels() { return -gLineHeight; }, + }, + init() { + SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, true); + }, + }, + + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is NULL, emulation enabled", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINELEFT, lParam is NULL, emulation enabled", + message: WM_HSCROLL, delta: SB_LINELEFT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: -1, pixels() { return -gCharWidth; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is NULL, emulation enabled", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + + { description: "WM_VSCROLL, SB_PAGEUP, lParam is NULL, emulation enabled", + message: WM_VSCROLL, delta: SB_PAGEUP, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: -DOM_PAGE_SCROLL_DELTA, + pixels() { return -gPageHeight; }, + }, + }, + + { description: "WM_VSCROLL, SB_PAGEDOWN, lParam is NULL, emulation enabled", + message: WM_VSCROLL, delta: SB_PAGEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: DOM_PAGE_SCROLL_DELTA, + pixels() { return gPageHeight; }, + }, + }, + + { description: "WM_HSCROLL, SB_PAGELEFT, lParam is NULL, emulation enabled", + message: WM_HSCROLL, delta: SB_PAGELEFT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: -DOM_PAGE_SCROLL_DELTA, + pixels() { return -gPageWidth; }, + }, + }, + + { description: "WM_HSCROLL, SB_PAGERIGHT, lParam is NULL, emulation enabled", + message: WM_HSCROLL, delta: SB_PAGERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: DOM_PAGE_SCROLL_DELTA, + pixels() { return gPageWidth; }, + }, + }, + + // Modifier key tests for WM_*SCROLL + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with left Shift", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: SHIFT_L, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + init() { + SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false); + }, + }, + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with right Shift", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: SHIFT_R, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with left Ctrl", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: CTRL_L, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with right Ctrl", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: CTRL_L, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with left Alt", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: ALT_L, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, with right Alt", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: ALT_R, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with left Shift", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: SHIFT_L, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with right Shift", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: SHIFT_R, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with left Ctrl", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: CTRL_L, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with right Ctrl", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: CTRL_L, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with left Alt", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: ALT_L, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, with right Alt", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: ALT_R, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + + finish() { + runDeactiveWindowTests(); + }, + }, +]; + +var gDeactiveWindowTests = [ + // Typically, mouse drivers send wheel messages to focused window. + // However, we prefer to scroll a scrollable element under the mouse cursor. + { description: "WM_MOUSEWHEEL, -120, 3 lines, window is deactive", + message: WM_MOUSEWHEEL, delta: -120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 3, pixels() { return gLineHeight * 3; }, + }, + init() { + SpecialPowers.setIntPref(kVAmountPref, 3); + SpecialPowers.setIntPref(kHAmountPref, 3); + }, + onLineScrollEvent(aEvent) { + var fm = Services.focus; + is(fm.activeWindow, gOtherWindow, "The other window isn't activated"); + }, + }, + + { description: "WM_MOUSEWHEEL, 120, -3 lines, window is deactive", + message: WM_MOUSEWHEEL, delta: 120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: -3, pixels() { return gLineHeight * -3; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, 120, 3 chars, window is deactive", + message: WM_MOUSEHWHEEL, delta: 120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 3, pixels() { return gCharWidth * 3; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, -120, -3 chars, window is deactive", + message: WM_MOUSEHWHEEL, delta: -120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: -3, pixels() { return gCharWidth * -3; }, + }, + }, + + // Of course, even if some drivers prefer the cursor position, we don't need + // to change anything. + { description: "WM_MOUSEWHEEL, -120, 3 lines, window is deactive (receive the message directly)", + message: WM_MOUSEWHEEL, delta: -120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kVAxis, lines: 3, pixels() { return gLineHeight * 3; }, + }, + }, + + { description: "WM_MOUSEWHEEL, 120, -3 lines, window is deactive (receive the message directly)", + message: WM_MOUSEWHEEL, delta: 120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kVAxis, lines: -3, pixels() { return gLineHeight * -3; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, 120, 3 chars, window is deactive (receive the message directly)", + message: WM_MOUSEHWHEEL, delta: 120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kHAxis, lines: 3, pixels() { return gCharWidth * 3; }, + }, + }, + + { description: "WM_MOUSEHWHEEL, -120, -3 chars, window is deactive (receive the message directly)", + message: WM_MOUSEHWHEEL, delta: -120, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kHAxis, lines: -3, pixels() { return gCharWidth * -3; }, + }, + }, + + // Same for WM_*SCROLL if lParam is not NULL + { description: "WM_VSCROLL, SB_LINEUP, lParam is not NULL, emulation disabled, window is deactive", + message: WM_VSCROLL, delta: SB_LINEUP, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: -1, pixels() { return -gLineHeight; }, + }, + init() { + SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false); + }, + }, + + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, window is deactive", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINELEFT, lParam is not NULL, emulation disabled, window is deactive", + message: WM_HSCROLL, delta: SB_LINELEFT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: -1, pixels() { return -gCharWidth; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, window is deactive", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + + // Same for WM_*SCROLL if lParam is NULL but emulation is enabled + { description: "WM_VSCROLL, SB_LINEUP, lParam is NULL, emulation enabled, window is deactive", + message: WM_VSCROLL, delta: SB_LINEUP, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: -1, pixels() { return -gLineHeight; }, + }, + init() { + SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, true); + }, + }, + + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is NULL, emulation enabled, window is deactive", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINELEFT, lParam is NULL, emulation enabled, window is deactive", + message: WM_HSCROLL, delta: SB_LINELEFT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: -1, pixels() { return -gCharWidth; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is NULL, emulation enabled, window is deactive", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: 0, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + + // Same for WM_*SCROLL if lParam is not NULL and message sent to the deactive window directly + { description: "WM_VSCROLL, SB_LINEUP, lParam is not NULL, emulation disabled, window is deactive (receive the message directly)", + message: WM_VSCROLL, delta: SB_LINEUP, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL | + nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kVAxis, lines: -1, pixels() { return -gLineHeight; }, + }, + init() { + SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, false); + }, + }, + + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is not NULL, emulation disabled, window is deactive (receive the message directly)", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL | + nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINELEFT, lParam is not NULL, emulation disabled, window is deactive (receive the message directly)", + message: WM_HSCROLL, delta: SB_LINELEFT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL | + nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kHAxis, lines: -1, pixels() { return -gCharWidth; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is not NULL, emulation disabled, window is deactive (receive the message directly)", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL | + nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + }, + + // Same for WM_*SCROLL if lParam is NULL but emulation is enabled, and message sent to the deactive window directly + { description: "WM_VSCROLL, SB_LINEUP, lParam is NULL, emulation enabled, window is deactive (receive the message directly)", + message: WM_VSCROLL, delta: SB_LINEUP, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kVAxis, lines: -1, pixels() { return -gLineHeight; }, + }, + init() { + SpecialPowers.setBoolPref(kEmulateWheelByWMSCROLLPref, true); + }, + }, + + { description: "WM_VSCROLL, SB_LINEDOWN, lParam is NULL, emulation enabled, window is deactive (receive the message directly)", + message: WM_VSCROLL, delta: SB_LINEDOWN, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kVAxis, lines: 1, pixels() { return gLineHeight; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINELEFT, lParam is NULL, emulation enabled, window is deactive (receive the message directly)", + message: WM_HSCROLL, delta: SB_LINELEFT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kHAxis, lines: -1, pixels() { return -gCharWidth; }, + }, + }, + + { description: "WM_HSCROLL, SB_LINERIGHT, lParam is NULL, emulation enabled, window is deactive (receive the message directly)", + message: WM_HSCROLL, delta: SB_LINERIGHT, + target: gP1, x: 10, y: 10, window, + modifiers: 0, + additionalFlags: nsIDOMWindowUtils.MOUSESCROLL_PREFER_WIDGET_AT_POINT, + expected: { + axis: kHAxis, lines: 1, pixels() { return gCharWidth; }, + }, + + finish() { + gOtherWindow.close(); + gOtherWindow = null; + window.close(); + }, + }, +]; + +function runDeactiveWindowTests() { + gOtherWindow = window.open("window_mouse_scroll_win_2.html", "_blank", + "chrome,width=100,height=100,top=700,left=700"); + + window.arguments[0].SimpleTest.waitForFocus(function() { + runNextTest(gDeactiveWindowTests, 0); + }, gOtherWindow); +} + +function runNextTest(aTests, aIndex) { + if (aIndex > 0 && aTests[aIndex - 1] && aTests[aIndex - 1].finish) { + aTests[aIndex - 1].finish(); + } + + if (aTests.length == aIndex) { + return; + } + + var test = aTests[aIndex++]; + if (test.init) { + test.init(); + } + test.handled = { lines: false, pixels: false }; + + switch (test.message) { + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + case WM_VSCROLL: + case WM_HSCROLL: + var expectedLines = test.expected.lines; + var expectedPixels = + cut((typeof test.expected.pixels == "function") ? + test.expected.pixels() : test.expected.pixels); + var handler = function(aEvent) { + var doCommonTests = true; + + if (!aEvent) { + ok(!test.handled.lines, + test.description + ", line scroll event has been handled"); + ok(!test.handled.pixels, + test.description + ", pixel scroll event has been handled"); + doCommonTests = false; + } else if (aEvent.type == kMouseLineScrollEvent) { + ok(!test.handled.lines, + test.description + ":(" + aEvent.type + "), same event has already been handled"); + test.handled.lines = true; + isnot(expectedLines, 0, + test.description + ":(" + aEvent.type + "), event shouldn't be fired"); + if (test.onLineScrollEvent && test.onLineScrollEvent(aEvent)) { + doCommonTests = false; + } + } else if (aEvent.type == kMousePixelScrollEvent) { + ok(!test.handled.pixels, + test.description + ":(" + aEvent.type + "), same event has already been handled"); + test.handled.pixels = true; + isnot(expectedPixels, 0, + test.description + ":(" + aEvent.type + "), event shouldn't be fired"); + if (test.onPixelScrollEvent && test.onPixelScrollEvent(aEvent)) { + doCommonTests = false; + } + } + + if (doCommonTests) { + var expectedDelta = + (aEvent.type == kMouseLineScrollEvent) ? + expectedLines : expectedPixels; + is(aEvent.target.id, test.target.id, + test.description + ":(" + aEvent.type + "), ID mismatch"); + is(aEvent.axis, test.expected.axis, + test.description + ":(" + aEvent.type + "), axis mismatch"); + ok(aEvent.detail != 0, + test.description + ":(" + aEvent.type + "), delta must not be 0"); + is(aEvent.detail, expectedDelta, + test.description + ":(" + aEvent.type + "), delta mismatch"); + is(aEvent.shiftKey, (test.modifiers & (SHIFT_L | SHIFT_R)) != 0, + test.description + ":(" + aEvent.type + "), shiftKey mismatch"); + is(aEvent.ctrlKey, (test.modifiers & (CTRL_L | CTRL_R)) != 0, + test.description + ":(" + aEvent.type + "), ctrlKey mismatch"); + is(aEvent.altKey, (test.modifiers & (ALT_L | ALT_R)) != 0, + test.description + ":(" + aEvent.type + "), altKey mismatch"); + } + + if (!aEvent || (test.handled.lines || expectedLines == 0) && + (test.handled.pixels || expectedPixels == 0)) { + // Don't scroll actually. + if (aEvent) { + aEvent.preventDefault(); + } + test.target.removeEventListener(kMouseLineScrollEvent, handler, true); + test.target.removeEventListener(kMousePixelScrollEvent, handler, true); + setTimeout(runNextTest, 0, aTests, aIndex); + } + }; + + test.target.addEventListener(kMouseLineScrollEvent, handler, true); + test.target.addEventListener(kMousePixelScrollEvent, handler, true); + + if (expectedLines == 0 && expectedPixels == 0) { + // The timeout might not be enough if system is slow by other process, + // so, the test might be passed unexpectedly. However, it must be able + // to be detected by random orange. + setTimeout(handler, 500); + } + + var utils = getWindowUtils(test.window); + var ptInScreen = getPointInScreen(test.target, test.window); + var isVertical = + ((test.message == WM_MOUSEWHEEL) || (test.message == WM_VSCROLL)); + var deltaX = !isVertical ? test.delta : 0; + var deltaY = isVertical ? test.delta : 0; + utils.sendNativeMouseScrollEvent(ptInScreen.x + test.x, + ptInScreen.y + test.y, + test.message, deltaX, deltaY, 0, + test.modifiers, + test.additionalFlags, + test.target); + break; + default: + ok(false, test.description + ": invalid message"); + // Let's timeout. + } +} + +function prepareTests() { + // Disable special action with modifier key + SpecialPowers.setIntPref(kAltKeyActionPref, 1); + SpecialPowers.setIntPref(kCtrlKeyActionPref, 1); + SpecialPowers.setIntPref(kShiftKeyActionPref, 1); + SpecialPowers.setIntPref(kWinKeyActionPref, 1); + + SpecialPowers.setIntPref(kAltKeyDeltaMultiplierXPref, 100); + SpecialPowers.setIntPref(kAltKeyDeltaMultiplierYPref, 100); + SpecialPowers.setIntPref(kCtrlKeyDeltaMultiplierXPref, 100); + SpecialPowers.setIntPref(kCtrlKeyDeltaMultiplierYPref, 100); + SpecialPowers.setIntPref(kShiftKeyDeltaMultiplierXPref, 100); + SpecialPowers.setIntPref(kShiftKeyDeltaMultiplierYPref, 100); + SpecialPowers.setIntPref(kWinKeyDeltaMultiplierXPref, 100); + SpecialPowers.setIntPref(kWinKeyDeltaMultiplierYPref, 100); + + SpecialPowers.setBoolPref(kSystemScrollSpeedOverridePref, false); + SpecialPowers.setIntPref(kTimeoutPref, -1); + + runNextTest(gPreparingSteps, 0); +} + +</script> +</body> + +</html> diff --git a/widget/tests/window_mouse_scroll_win_2.html b/widget/tests/window_mouse_scroll_win_2.html new file mode 100644 index 0000000000..c8d3762405 --- /dev/null +++ b/widget/tests/window_mouse_scroll_win_2.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> + Helper file for window_mouse_scroll_win.html +</body> +</html> diff --git a/widget/tests/window_picker_no_crash_child.html b/widget/tests/window_picker_no_crash_child.html new file mode 100644 index 0000000000..c980f979be --- /dev/null +++ b/widget/tests/window_picker_no_crash_child.html @@ -0,0 +1,6 @@ +<!doctype html> +<title>Picker window</title> +<form name="form1"> + <input type="file" name="uploadbox" id="uploadbox" onclick="window.clicked = true;"> + <input type="file" name="multiple" id="multiple" multiple onclick="window.clicked = true"> +</form> diff --git a/widget/tests/window_state_windows.xhtml b/widget/tests/window_state_windows.xhtml new file mode 100644 index 0000000000..60989db144 --- /dev/null +++ b/widget/tests/window_state_windows.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window id="NativeWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="300" + height="300" + onload="onLoad();" + title="Window State Tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function onLoad() { + var win = Services.wm.getMostRecentWindow("navigator:browser"); + + /* + switch(win.windowState) { + case win.STATE_FULLSCREEN: + dump("STATE_FULLSCREEN \n"); + break; + case win.STATE_MAXIMIZED: + dump("STATE_MAXIMIZED \n"); + break; + case win.STATE_MINIMIZED: + dump("STATE_MINIMIZED \n"); + break; + case win.STATE_NORMAL: + dump("STATE_NORMAL \n"); + break; + } + */ + + // Make sure size mode changes are reflected in the widget. + win.restore(); + ok(win.windowState == win.STATE_NORMAL, "window state is restored."); + win.minimize(); + ok(win.windowState == win.STATE_MINIMIZED, "window state is minimized."); + + // Windows resizes children to 0x0. Code in nsWindow filters these changes out. Without + // this all sorts of screwy things can happen in child widgets. + ok(document.documentElement.clientHeight > 0, "document height should not be zero for a minimized window!"); + ok(document.documentElement.clientWidth > 0, "document width should not be zero for a minimized window!"); + + // Make sure size mode changes are reflected in the widget. + win.restore(); + ok(win.windowState == win.STATE_NORMAL, "window state is restored."); + win.maximize(); + ok(win.windowState == win.STATE_MAXIMIZED, "window state is maximized."); + win.restore(); + ok(win.windowState == win.STATE_NORMAL, "window state is restored."); + + /* + dump(win.screenX + "\n"); + win.minimize(); + dump(win.screenX + "\n"); + win.restore(); + dump(win.screenX + "\n"); + */ + + SimpleTest.finish(); + } + + ]]> + </script> + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> +</window> diff --git a/widget/tests/window_wheeltransaction.xhtml b/widget/tests/window_wheeltransaction.xhtml new file mode 100644 index 0000000000..f3c081b105 --- /dev/null +++ b/widget/tests/window_wheeltransaction.xhtml @@ -0,0 +1,1569 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Wheel scroll tests" + width="600" height="600" + onload="onload();" + onunload="onunload();" + 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/paint_listener.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<style type="text/css"> + #rootview { + overflow: auto; + width: 400px; + height: 400px; + border: 1px solid; + } + #container { + overflow: auto; + width: 600px; + height: 600px; + } + #rootview pre { + margin: 20px 0 20px 20px; + padding: 0; + overflow: auto; + display: block; + width: 100px; + height: 100.5px; + font-size: 16px; + } +</style> +<div id="rootview" onscroll="onScrollView(event);"> + <div id="container"> + <pre id="subview1" onscroll="onScrollView(event);"> +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. + </pre> + <pre id="subview2" onscroll="onScrollView(event);"> +Text. +Text. +Text. +Text. +Text. +Text. +Text. +Text. +Text. +Text. + </pre> + <pre id="subview3" onscroll="onScrollView(event);"> +Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. Text. + </pre> + </div> +</div> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.arguments[0].SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage); +} + +var gCurrentTestListStatus = { nextListIndex: 0 }; +var gCurrentTest; + +const kListenEvent_None = 0; +const kListenEvent_OnScroll = 1; +const kListenEvent_OnScrollFailed = 2; +const kListenEvent_OnTransactionTimeout = 4; +const kListenEvent_All = kListenEvent_OnScroll | + kListenEvent_OnScrollFailed | + kListenEvent_OnTransactionTimeout; +var gLitesnEvents = kListenEvent_None; + +/** + * At unexpected transaction timeout, we need to stop *all* timers. But it is + * difficult and it can be create more complex testing code. So, we should use + * only one timer at one time. For that, we must store the timer id to this + * variable. And the functions which may be called via a timer must clear the + * current timer by |_clearTimer| function. + */ +var gTimer; + +var gPrefSvc = SpecialPowers.Services.prefs; +const kPrefSmoothScroll = "general.smoothScroll"; +const kPrefNameTimeout = "mousewheel.transaction.timeout"; +const kPrefNameIgnoreMoveDelay = "mousewheel.transaction.ignoremovedelay"; +const kPrefTestEventsAsyncEnabled = "test.events.async.enabled"; + +const kDefaultTimeout = gPrefSvc.getIntPref(kPrefNameTimeout); +const kDefaultIgnoreMoveDelay = gPrefSvc.getIntPref(kPrefNameIgnoreMoveDelay); + +gPrefSvc.setBoolPref(kPrefSmoothScroll, false); +gPrefSvc.setBoolPref(kPrefTestEventsAsyncEnabled, true); + +var gTimeout, gIgnoreMoveDelay; +var gEnoughForTimeout, gEnoughForIgnoreMoveDelay; + +function setTimeoutPrefs(aTimeout, aIgnoreMoveDelay) +{ + gPrefSvc.setIntPref(kPrefNameTimeout, aTimeout); + gPrefSvc.setIntPref(kPrefNameIgnoreMoveDelay, aIgnoreMoveDelay); + gTimeout = aTimeout; + gIgnoreMoveDelay = aIgnoreMoveDelay; + gEnoughForTimeout = gTimeout * 2; + gEnoughForIgnoreMoveDelay = gIgnoreMoveDelay * 1.2; +} + +function resetTimeoutPrefs() +{ + if (gTimeout == kDefaultTimeout) + return; + setTimeoutPrefs(kDefaultTimeout, kDefaultIgnoreMoveDelay); + initTestList(); +} + +function growUpTimeoutPrefs() +{ + if (gTimeout != kDefaultTimeout) + return; + setTimeoutPrefs(5000, 1000); + initTestList(); +} + +// setting enough time for testing. +gPrefSvc.setIntPref(kPrefNameTimeout, gTimeout); +gPrefSvc.setIntPref(kPrefNameIgnoreMoveDelay, gIgnoreMoveDelay); + +var gRootView = document.getElementById("rootview"); +var gSubView1 = document.getElementById("subview1"); +var gSubView2 = document.getElementById("subview2"); +var gSubView3 = document.getElementById("subview3"); + +gRootView.addEventListener("MozMouseScrollFailed", onMouseScrollFailed); +gRootView.addEventListener("MozMouseScrollTransactionTimeout", + onTransactionTimeout); + +function finish() +{ + window.close(); +} + +async function onload() +{ + // Before actually running tests, we disable auto-dir scrolling, becasue the + // tests in this file are meant to test scrolling transactions, not meant to + // test default actions for wheel events, so we simply disabled auto-dir + // scrolling, which are well tested in + // dom/events/test/window_wheel_default_action.html. + await SpecialPowers.pushPrefEnv({"set": [["mousewheel.autodir.enabled", + false]]}); + + runNextTestList(); +} + +function onunload() +{ + resetTimeoutPrefs(); + gPrefSvc.clearUserPref(kPrefSmoothScroll); + gPrefSvc.clearUserPref(kPrefTestEventsAsyncEnabled); + disableNonTestMouseEvents(false); + SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); + window.arguments[0].SimpleTest.finish(); +} + +function offsetForRootView() +{ + let rootViewRect = gRootView.getBoundingClientRect(); + let subView1Rect = gSubView1.getBoundingClientRect(); + return { + x: (subView1Rect.left - rootViewRect.left) / 2, + y: (subView1Rect.top - rootViewRect.top) / 2, + } +} + +function _offsetFor(aSubView) +{ + let rootViewRect = gRootView.getBoundingClientRect(); + let subViewRect = aSubView.getBoundingClientRect(); + return { + x: subViewRect.left - rootViewRect.left + subViewRect.width / 2, + y: subViewRect.top - rootViewRect.top + subViewRect.height / 2, + } +} + +function offsetForSubView1() +{ + return _offsetFor(gSubView1); +} + +function offsetForSubView2() +{ + return _offsetFor(gSubView2); +} + +function offsetForSubView3() +{ + return _offsetFor(gSubView3); +} + +/** + * Define the tests here: + * Scrolls are processed async always. Therefore, we need to call all tests + * by timer. gTestLists is array of testing lists. In other words, an item + * of gTestList is a group of one or more testing. Each items has following + * properties: + * + * - retryWhenTransactionTimeout + * The testing of wheel transaction might be fialed randomly by + * timeout. Then, automatically the failed test list will be retested + * automatically only this number of times. + * + * - steps + * This property is array of testing. Each steps must have following + * properties at least. + * + * - func + * This property means function which will be called via + * |setTimeout|. The function cannot have params. If you need + * some additional parameters, you can specify some original + * properties for the test function. If you do so, you should + * document it in the testing function. + * - delay + * This property means delay time until the function to be called. + * I.e., the value used for the second param of |setTimeout|. + * + * And also you need one more property when you call a testing function. + * + * - description + * This property is description of the test. This is used for + * logging. + * + * At testing, you can access to current step via |gCurrentTest|. + */ + +var gTestLists; +function initTestList() +{ + gTestLists = [ + /************************************************************************** + * Continuous scrolling test for |gRootView| + * |gRootView| has both scrollbars and it has three children which are + * |gSubView1|, |gSubView2| and |gSubView3|. They have scrollbars. If + * the current transaction targets |gRootView|, other children should not + * be scrolled even if the wheel events are fired on them. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Vertical wheel events should scroll |gRootView| even if the position + // of wheel events in a child view which has scrollbar. + { func: testContinuousScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Continuous scrolling test for root view (vertical/forward)" }, + { func: testContinuousScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: true, expectedView: gRootView, + description: "Continuous scrolling test for root view (vertical/backward)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Horizontal wheel events should scroll |gRootView| even if the + // position of wheel events in a child view which has scrollbar. + { func: testContinuousScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: false, expectedView: gRootView, + description: "Continuous scrolling test for root view (horizontal/forward)" }, + { func: testContinuousScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: false, expectedView: gRootView, + description: "Continuous scrolling test for root view (horizontal/backward)" } + ] + }, + + + /************************************************************************** + * Continuous scrolling test for |gSubView1| + * |gSubView1| has both scrollbars. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Vertical wheel events should scroll |gSubView1|. + { func: testContinuousScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + description: "Continuous scrolling test for sub view 1 (vertical/forward)" }, + { func: testContinuousScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: true, expectedView: gSubView1, + description: "Continuous scrolling test for sub view 1 (vertical/backward)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Horitontal wheel events should scroll |gSubView1|. + { func: testContinuousScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gSubView1, + description: "Continuous scrolling test for sub view 1 (horizontal/forward)" }, + { func: testContinuousScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: false, expectedView: gSubView1, + description: "Continuous scrolling test for sub view 1 (horizontal/backward)" } + ] + }, + + + /************************************************************************** + * Continuous scrolling test for |gSubView2| + * |gSubView2| has only vertical scrollbar. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Vertical wheel events should scroll |gSubView2|. + { func: testContinuousScroll, delay: 0, offset: offsetForSubView2, + isForward: true, isVertical: true, expectedView: gSubView2, + description: "Continuous scrolling test for sub view 2 (vertical/forward)" }, + { func: testContinuousScroll, delay: 0, offset: offsetForSubView2, + isForward: false, isVertical: true, expectedView: gSubView2, + description: "Continuous scrolling test for sub view 2 (vertical/backward)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Horizontal wheel events should scroll its nearest scrollable ancestor + // view, i.e., it is |gRootView|. + { func: testContinuousScroll, delay: 0, offset: offsetForSubView2, + isForward: true, isVertical: false, expectedView: gRootView, + description: "Continuous scrolling test for sub view 2 (horizontal/forward)" }, + { func: testContinuousScroll, delay: 0, offset: offsetForSubView2, + isForward: false, isVertical: false, expectedView: gRootView, + description: "Continuous scrolling test for sub view 2 (horizontal/backward)" } + ] + }, + + + /************************************************************************** + * Continuous scrolling test for |gSubView3| + * |gSubView3| has only horizontal scrollbar. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Vertical wheel events should scroll its nearest scrollable ancestor + // view, i.e., it is |gRootView|. + { func: testContinuousScroll, delay: 0, offset: offsetForSubView3, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Continuous scrolling test for sub view 3 (vertical/forward)" }, + { func: testContinuousScroll, delay: 0, offset: offsetForSubView3, + isForward: false, isVertical: true, expectedView: gRootView, + description: "Continuous scrolling test for sub view 3 (vertical/backward)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Horitontal wheel events should scroll |gSubView3|. + { func: testContinuousScroll, delay: 0, offset: offsetForSubView3, + isForward: true, isVertical: false, expectedView: gSubView3, + description: "Continuous scrolling test for sub view 3 (horizontal/forward)" }, + { func: testContinuousScroll, delay: 0, offset: offsetForSubView3, + isForward: false, isVertical: false, expectedView: gSubView3, + description: "Continuous scrolling test for sub view 3 (horizontal/backward)" } + ] + }, + + + /************************************************************************** + * Don't reset transaction by a different direction wheel event + * Even if a wheel event doesn't same direction as last wheel event, the + * current transaction should not be reset. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical -> Horizontal + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gRootView| by a vertical wheel + // event. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Don't reset transaction by a different direction wheel event (1-1)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: true, expectedView: gRootView, + description: "Don't reset transaction by a different direction wheel event (1-2)" }, + // Send a horizontal wheel event over |gSubView1| but |gRootView| should + // be scrolled. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Don't reset transaction by a different direction wheel event (1-3)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal -> Vertical + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gRootView| by a horizontal wheel + // event. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: false, expectedView: gRootView, + description: "Don't reset transaction by a different direction wheel event (2-1)" }, + // Scroll back to left-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: false, expectedView: gRootView, + description: "Don't reset transaction by a different direction wheel event (2-2)" }, + // Send a vertical wheel event over |gSubView1| but |gRootView| should + // be scrolled. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Don't reset transaction by a different direction wheel event (2-3)" } + ] + }, + + + /************************************************************************** + * Don't reset transaction even if a wheel event cannot scroll + * Even if a wheel event cannot scroll to specified direction in the + * current target view, the transaction should not be reset. E.g., there + * are some devices which can scroll obliquely. If so, probably, users + * cannot input only intended direction. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // A view only has vertical scrollbar case. + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gSubView2|. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView2, + isForward: true, isVertical: true, expectedView: gSubView2, + description: "Don't reset transaction even if a wheel event cannot scroll (1-1)" }, + // |gSubView2| doesn't have horizontal scrollbar but should not scroll + // any views. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView2, + isForward: true, isVertical: false, expectedView: null, + description: "Don't reset transaction even if a wheel event cannot scroll (1-2)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // A view only has horizontal scrollbar case. + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gSubView3|. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView3, + isForward: true, isVertical: false, expectedView: gSubView3, + description: "Don't reset transaction even if a wheel event cannot scroll (2-1)" }, + // |gSubView3| doesn't have vertical scrollbar but should not scroll any + // views. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView3, + isForward: true, isVertical: true, expectedView: null, + description: "Don't reset transaction even if a wheel event cannot scroll (2-2)" } + ] + }, + + + /************************************************************************** + * Reset transaction by mouse down/mouse up events + * Mouse down and mouse up events should cause resetting the current + * transaction. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gRootView|. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Reset transaction by mouse down/mouse up events (v-1)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: true, expectedView: gRootView, + description: "Reset transaction by mouse down/mouse up events (v-2)" }, + // Send mouse button events which should reset the current transaction. + // So, the next wheel event should scroll |gSubView1|. + { func: sendMouseButtonEvents, delay: 0, + description: "sendMouseButtonEvents" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + description: "Reset transaction by mouse down/mouse up events (v-3)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gRootView|. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: false, expectedView: gRootView, + description: "Reset transaction by mouse down/mouse up events (h-1)" }, + // Scroll back to left-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: false, expectedView: gRootView, + description: "Reset transaction by mouse down/mouse up events (h-2)" }, + // Send mouse button events which should reset the current transaction. + // So, the next wheel event should scroll |gSubView1|. + { func: sendMouseButtonEvents, delay: 0, + description: "sendMouseButtonEvents" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gSubView1, + description: "Reset transaction by mouse down/mouse up events (h-3)" } + ] + }, + + + /************************************************************************** + * Reset transaction by a key event + * A key event should cause resetting the current transaction. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gRootView|. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Reset transaction by a key event (v-1)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: true, expectedView: gRootView, + description: "Reset transaction by a key event (v-2)" }, + // Send a key event which should reset the current transaction. So, the + // next wheel event should scroll |gSubView1|. + { func: sendKeyEvents, delay: 0, key: "a", + description: "sendKeyEvents" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + description: "Reset transaction by a key event (v-3)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gRootView|. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: false, expectedView: gRootView, + description: "Reset transaction by a key event (h-1)" }, + // Scroll back to left-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: false, expectedView: gRootView, + description: "Reset transaction by a key event (h-2)" }, + // Send a key event which should reset the current transaction. So, the + // next wheel event should scroll |gSubView1|. + { func: sendKeyEvents, delay: 0, key: "a", + description: "sendKeyEvents" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gSubView1, + description: "Reset transaction by a key event (h-3)" } + ] + }, + + + /************************************************************************** + * Reset transaction by a mouse move event + * A mouse move event can cause reseting the current transaction even if + * mouse cursor is inside the target view of current transaction. Only + * when a wheel event is fired after |gIgnoreMoveDelay| milliseconds since + * the first mouse move event from last wheel event, the transaction + * should be reset. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gRootView|. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Reset transaction by a mouse move event (v-1)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: true, expectedView: gRootView, + description: "Reset transaction by a mouse move event (v-2)" }, + // Send a mouse move event immediately after last wheel event, then, + // current transaction should be kept. + { func: sendMouseMoveEvent, delay: 0, offset: offsetForSubView1, + description: "sendMouseMoveEvent" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (v-3)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: true, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (v-4)" }, + // Send a mouse move event after |gIgnoreMoveDelay| milliseconds since + // last wheel event, then, current transaction should be kept. + { func: sendMouseMoveEvent, delay: gEnoughForIgnoreMoveDelay, + offset: offsetForSubView1, + description: "sendMouseMoveEvent" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (v-5)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: true, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (v-6)" }, + // Send a wheel event after |gIgnoreMoveDelay| milliseconds since last + // mouse move event but it is fired immediately after the last wheel + // event, then, current transaction should be kept. + { func: sendMouseMoveEvent, delay: 0, offset: offsetForSubView1, + description: "sendMouseMoveEvent" }, + { func: testOneTimeScroll, delay: gEnoughForIgnoreMoveDelay, + offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (v-7)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: true, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (v-8)" }, + // Send a wheel event after |gIgnoreMoveDelay| milliseconds have passed + // since last mouse move event which is fired after |gIgnoreMoveDelay| + // milliseconds since last wheel event, then, current transaction should + // be reset. + { func: sendMouseMoveEvent, delay: gEnoughForIgnoreMoveDelay, + offset: offsetForSubView1, + description: "sendMouseMoveEvent" }, + { func: testOneTimeScroll, delay: gEnoughForIgnoreMoveDelay, + offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + canFailRandomly: { possibleView: gRootView }, + description: "Reset transaction by a mouse move event (v-9)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gRootView|. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (h-1)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (h-2)" }, + // Send a mouse move event immediately after last wheel event, then, + // current transaction should be kept. + { func: sendMouseMoveEvent, delay: 0, offset: offsetForSubView1, + description: "sendMouseMoveEvent" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (h-3)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (h-4)" }, + // Send a mouse move event after |gIgnoreMoveDelay| milliseconds since + // last wheel event, then, current transaction should be kept. + { func: sendMouseMoveEvent, delay: gEnoughForIgnoreMoveDelay, + offset: offsetForSubView1, + description: "sendMouseMoveEvent" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (h-5)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (h-6)" }, + // Send a wheel event after |gIgnoreMoveDelay| milliseconds since last + // mouse move event but it is fired immediately after the last wheel + // event, then, current transaction should be kept. + { func: sendMouseMoveEvent, delay: 0, offset: offsetForSubView1, + description: "sendMouseMoveEvent" }, + { func: testOneTimeScroll, delay: gEnoughForIgnoreMoveDelay, + offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (h-7)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Reset transaction by a mouse move event (h-8)" }, + // Send a wheel event after |gIgnoreMoveDelay| milliseconds have passed + // since last mouse move event which is fired after |gIgnoreMoveDelay| + // milliseconds since last wheel event, then, current transaction should + // be reset. + { func: sendMouseMoveEvent, delay: gEnoughForIgnoreMoveDelay, + offset: offsetForSubView1, + description: "sendMouseMoveEvent" }, + { func: testOneTimeScroll, delay: gEnoughForIgnoreMoveDelay, + offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gSubView1, + canFailRandomly: { possibleView: gRootView }, + description: "Reset transaction by a mouse move event (h-9)" } + ] + }, + + + /************************************************************************** + * Reset transaction by a mouse move event on outside of view + * When mouse cursor is moved to outside of the current target view, the + * transaction should be reset immediately. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gSubView1|. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + description: "Reset transaction by a mouse move event on outside of view (v-1)" }, + // Send mouse move event over |gRootView|. + { func: sendMouseMoveEvent, delay: 0, offset: offsetForRootView, + description: "sendMouseMoveEvent" }, + // Send Wheel event over |gRootView| which should be scrolled. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Reset transaction by a mouse move event on outside of view (v-2)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Create a transaction which targets |gSubView1|. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + description: "Reset transaction by a mouse move event on outside of view (h-1)" }, + // Send mouse move event over |gRootView|. + { func: sendMouseMoveEvent, delay: 0, offset: offsetForRootView, + description: "sendMouseMoveEvent" }, + // Send Wheel event over |gRootView| which should be scrolled. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Reset transaction by a mouse move event on outside of view (h-2)" } + ] + }, + + + /************************************************************************** + * Timeout test + * A view should not be scrolled during another to be transaction for + * another view scrolling. However, a wheel event which is sent after + * timeout, a view which is under the mouse cursor should be scrolled. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // First, create a transaction which should target the |gRootView|. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Timeout test (v-1)" }, + // Scroll back to top-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: true, expectedView: gRootView, + description: "Timeout test (v-2)" }, + // A wheel event over |gSubView1| should not scroll it during current + // transaction. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Timeout test (v-3)" }, + // Scroll back to top-most again. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: true, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Timeout test (v-4)" }, + // A wheel event over |gSubView1| after timeout should scroll + // |gSubView1|. + { func: testOneTimeScroll, delay: gEnoughForTimeout, + offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + isTimeoutTesting: true, + description: "Timeout test (v-5)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // First, create a transaction which should target the |gRootView|. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: true, isVertical: false, expectedView: gRootView, + description: "Timeout test (h-1)" }, + // Scroll back to left-most for easy cursor position specifying. + { func: testOneTimeScroll, delay: 0, offset: offsetForRootView, + isForward: false, isVertical: false, expectedView: gRootView, + description: "Timeout test (h-2)" }, + // A wheel event over |gSubView1| should not scroll it during current + // transaction. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Timeout test (h-3)" }, + // Scroll back to left-most again. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: false, isVertical: false, expectedView: gRootView, + canFailRandomly: { possibleView: gSubView1 }, + description: "Timeout test (h-4)" }, + // A wheel event over |gSubView1| after timeout should scroll + // |gSubView1|. + { func: testOneTimeScroll, delay: gEnoughForTimeout, + offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gSubView1, + isTimeoutTesting: true, + description: "Timeout test (h-5)" } + ] + }, + + + /************************************************************************** + * Timeout test even with many wheel events + * This tests whether timeout is occurred event if wheel events are sent. + * The transaction should not be updated by non-scrollable wheel events. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + // Vertical case + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Scroll |gSubView1| to bottom-most. + { func: testContinuousScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + description: "Timeout test even with many wheel events (v-1)" }, + // Don't scroll any views before timeout. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: null, + canFailRandomly: { possibleView: gRootView }, + description: "Timeout test even with many wheel events (v-2)" }, + // Recreate a transaction which is scrolling |gRootView| after time out. + { func: testRestartScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gRootView, + description: "Timeout test even with many wheel events (v-3)" } + ] + }, + + + { retryWhenTransactionTimeout: 5, + steps: [ + // Horizontal case + { func: initElements, delay: 0, forVertical: false, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + // Scroll |gSubView1| to right-most. + { func: testContinuousScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gSubView1, + description: "Timeout test even with many wheel events (h-1)" }, + // Don't scroll any views before timeout. + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: null, + canFailRandomly: { possibleView: gRootView }, + description: "Timeout test even with many wheel events (h-2)" }, + // Recreate a transaction which is scrolling |gRootView| after time out. + { func: testRestartScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gRootView, + description: "Timeout test even with many wheel events (h-3)" } + ] + }, + + + /************************************************************************** + * Very large scrolling wheel event + * If the delta value is larger than the scrolling page size, it should be + * scrolled only one page instead of the delta value. + **************************************************************************/ + { retryWhenTransactionTimeout: 5, + steps: [ + { func: initElements, delay: 0, forVertical: true, + description: "initElements" }, + { func: clearWheelTransaction, delay: 0, + description: "clearWheelTransaction" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + delta: 5000, + description: "Very large delta scrolling (v-1)" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: true, expectedView: gSubView1, + delta: 5000, + description: "Very large delta scrolling (v-2)" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gSubView1, + delta: 5000, + description: "Very large delta scrolling (h-1)" }, + { func: testOneTimeScroll, delay: 0, offset: offsetForSubView1, + isForward: true, isVertical: false, expectedView: gSubView1, + delta: 5000, + description: "Very large delta scrolling (h-2)" } + ] + } + ]; +} + +/****************************************************************************** + * Actions for preparing tests + ******************************************************************************/ + +function initElements() +{ + _clearTimer(); + + function resetScrollPosition(aElement) + { + aElement.scrollTop = 0; + aElement.scrollLeft = 0; + } + + const kDisplay = gCurrentTest.forVertical ? "block" : "inline-block"; + gSubView1.style.display = kDisplay; + gSubView2.style.display = kDisplay; + gSubView3.style.display = kDisplay; + + resetScrollPosition(gRootView); + resetScrollPosition(gSubView1); + resetScrollPosition(gSubView2); + resetScrollPosition(gSubView3); + _getDOMWindowUtils(window).advanceTimeAndRefresh(0); + + runNextTestStep(); +} + +function clearWheelTransaction() +{ + _clearTimer(); + _clearTransaction(); + runNextTestStep(); +} + +function sendKeyEvents() +{ + _clearTimer(); + synthesizeKey(gCurrentTest.key, {}, window); + runNextTestStep(); +} + +function sendMouseButtonEvents() +{ + _clearTimer(); + synthesizeMouse(gRootView, -1, -1, { type:"mousedown" }, window); + synthesizeMouse(gRootView, -1, -1, { type:"mouseup" }, window); + runNextTestStep(); +} + +function sendMouseMoveEvent() +{ + _clearTimer(); + _fireMouseMoveEvent(gCurrentTest.offset()); + runNextTestStep(); +} + +/****************************************************************************** + * Utilities for testing functions + ******************************************************************************/ + +function _clearTransaction() +{ + synthesizeMouse(gRootView, -1, -1, { type:"mousedown" }, window); + synthesizeMouse(gRootView, -1, -1, { type:"mouseup" }, window); +} + +function _saveScrollPositions() +{ + function save(aElement) + { + aElement.prevTop = aElement.scrollTop; + aElement.prevLeft = aElement.scrollLeft; + } + save(gRootView); + save(gSubView1); + save(gSubView2); + save(gSubView3); +} + +function _fireMouseMoveEvent(aOffset) +{ + synthesizeMouse(gRootView, aOffset.x, aOffset.y, { type:"mousemove" }, window); +} + +function _fireWheelScrollEvent(aOffset, aIsVertical, aForward, aDelta) +{ + var event = { deltaMode: WheelEvent.DOM_DELTA_LINE }; + if (aIsVertical) { + event.deltaY = aForward ? aDelta : -aDelta; + } else { + event.deltaX = aForward ? aDelta : -aDelta; + } + sendWheelAndPaint(gRootView, aOffset.x, aOffset.y, event, null, window); +} + +function _canScroll(aElement, aIsVertical, aForward) +{ + if (aIsVertical) { + if (!aForward) + return aElement.scrollTop > 0; + return aElement.scrollHeight > aElement.scrollTop + aElement.clientHeight; + } + if (!aForward) + return aElement.scrollLeft > 0; + return aElement.scrollWidth > aElement.scrollLeft + aElement.clientWidth; +} + +const kNotScrolled = 0; +const kScrolledToTop = 1; +const kScrolledToBottom = 2; +const kScrolledToLeft = 4; +const kScrolledToRight = 8; + +const kScrolledVertical = kScrolledToTop | kScrolledToBottom; +const kScrolledHorizontal = kScrolledToLeft | kScrolledToRight; + +function _getScrolledState(aElement) +{ + var ret = kNotScrolled; + if (aElement.scrollTop != aElement.prevTop) { + ret |= aElement.scrollTop < aElement.prevTop ? kScrolledToTop : + kScrolledToBottom; + } + if (aElement.scrollLeft != aElement.prevLeft) { + ret |= aElement.scrollLeft < aElement.prevLeft ? kScrolledToLeft : + kScrolledToRight; + } + return ret; +} + +function _getExpectedScrolledState() +{ + // eslint-disable-next-line no-nested-ternary + return gCurrentTest.isVertical ? + gCurrentTest.isForward ? kScrolledToBottom : kScrolledToTop : + gCurrentTest.isForward ? kScrolledToRight : kScrolledToLeft; +} + +function _getScrolledStateText(aScrolledState) +{ + if (aScrolledState == kNotScrolled) + return "Not scrolled"; + + var s = "scrolled to "; + if (aScrolledState & kScrolledVertical) { + s += aScrolledState & kScrolledToTop ? "backward" : "forward"; + s += " (vertical)" + if (aScrolledState & kScrolledHorizontal) + s += " and to "; + } + if (aScrolledState & kScrolledHorizontal) { + s += aScrolledState & kScrolledToLeft ? "backward" : "forward"; + s += " (horizontal)" + } + return s; +} + +function _getCurrentTestList() +{ + return gTestLists[gCurrentTestListStatus.nextListIndex - 1]; +} + +function _clearTimer() +{ + clearTimeout(gTimer); + gTimer = 0; +} + +/****************************************************************************** + * Testing functions + ******************************************************************************/ + +/** + * Note that testing functions must set following variables: + * + * gCurrentTest.repeatTest: See comment in |continueTest|. + * gCurrentTest.autoRepeatDelay: See comment in |continueTest|. + * gListenScrollEvent: When this is not true, the event handlers ignores the + * events. + */ + +function testContinuousScroll() +{ + /** + * Testing continuous scrolling. This function synthesizes a wheel event. If + * the test was success, this function will be recalled automatically. + * And when a generating wheel event cannot scroll the expected view, this + * function fires the wheel event only one time. + * + * @param gCurrentTest.offset + * A function to compute the cursor position of firing wheel event. + * The values are offset from |gRootView|. + * @param gCurrentTest.isVertical + * Whether the wheel event is for virtical scrolling or horizontal. + * @param gCurrentTest.isForward + * Whether the wheel event is to forward or to backward. + * @param gCurrentTest.expectedView + * The expected view which will be scrolled by wheel event. This + * value must not be null. + */ + + _clearTimer(); + _saveScrollPositions(); + if (!gCurrentTest.expectedView) { + runNextTestStep(); + return; + } + + gLitesnEvents = kListenEvent_All; + gCurrentTest.repeatTest = true; + gCurrentTest.autoRepeatDelay = 0; + + if (!_canScroll(gCurrentTest.expectedView, + gCurrentTest.isVertical, gCurrentTest.isForward)) { + gCurrentTest.expectedView = null; + } + var delta = gCurrentTest.delta ? gCurrentTest.delta : 4; + _fireWheelScrollEvent(gCurrentTest.offset(), + gCurrentTest.isVertical, gCurrentTest.isForward, delta); +} + +function testOneTimeScroll() +{ + /** + * Testing one wheel event. |runNextTestStep| will be called immediately + * after this function by |onScrollView| or |onTimeout|. + * + * @param gCurrentTest.offset + * A function to compute the cursor position of firing wheel event. + * The values are offset from |gRootView|. + * @param gCurrentTest.isVertical + * Whether the wheel event is for virtical scrolling or horizontal. + * @param gCurrentTest.isForward + * Whether the wheel event is to forward or to backward. + * @param gCurrentTest.expectedView + * The expected view which will be scrolled by wheel event. This + * value can be null. It means any views should not be scrolled. + */ + + _clearTimer(); + _saveScrollPositions(); + + gLitesnEvents = kListenEvent_All; + gCurrentTest.repeatTest = false; + gCurrentTest.autoRepeatDelay = 0; + + var delta = gCurrentTest.delta ? gCurrentTest.delta : 4; + _fireWheelScrollEvent(gCurrentTest.offset(), + gCurrentTest.isVertical, gCurrentTest.isForward, delta); +} + +function testRestartScroll() +{ + /** + * Testing restart to scroll in expected view after timeout from the current + * transaction. This function recall this itself until to success this test + * or timeout from this test. + * + * @param gCurrentTest.offset + * A function to compute the cursor position of firing wheel event. + * The values are offset from |gRootView|. + * @param gCurrentTest.isVertical + * Whether the wheel event is for virtical scrolling or horizontal. + * @param gCurrentTest.isForward + * Whether the wheel event is to forward or to backward. + * @param gCurrentTest.expectedView + * The expected view which will be scrolled by wheel event. This + * value must not be null. + */ + + _clearTimer(); + _saveScrollPositions(); + + if (!gCurrentTest.wasTransactionTimeout) { + gCurrentTest.repeatTest = true; + gCurrentTest.autoRepeatDelay = gTimeout / 3; + gLitesnEvents = kListenEvent_All; + gCurrentTest.isTimeoutTesting = true; + if (gCurrentTest.expectedView) { + gCurrentTest.expectedViewAfterTimeout = gCurrentTest.expectedView; + gCurrentTest.expectedView = null; + } + } else { + gCurrentTest.repeatTest = false; + gCurrentTest.autoRepeatDelay = 0; + gLitesnEvents = kListenEvent_All; + gCurrentTest.isTimeoutTesting = false; + gCurrentTest.expectedView = gCurrentTest.expectedViewAfterTimeout; + } + + var delta = gCurrentTest.delta ? gCurrentTest.delta : 4; + _fireWheelScrollEvent(gCurrentTest.offset(), + gCurrentTest.isVertical, gCurrentTest.isForward, delta); +} + +/****************************************************************************** + * Event handlers + ******************************************************************************/ + +function onScrollView(aEvent) +{ + /** + * Scroll event handler of |gRootView|, |gSubView1|, |gSubView2| and + * |gSubView3|. If testing is failed, this function cancels all left tests. + * For checking the event is expected, the event firer must call + * |_saveScrollPositions|. + * + * @param gCurrentTest.expectedView + * The expected view which should be scrolled by the wheel event. + * This value can be null. It means any views should not be + * scrolled. + * @param gCurrentTest.isVertical + * The expected view should be scrolled vertical or horizontal. + * @param gCurrentTest.isForward + * The expected view should be scrolled to forward or backward. + * @param gCurrentTest.canFailRandomly + * If this is not undefined, this test can fail by unexpected view + * scrolling which is caused by unexpected timeout. If this is + * defined, |gCurrentTest.possibleView| must be set. If the view is + * same as the event target, the failure can be random. At this + * time, we should retry the current test list. + */ + + if (!(gLitesnEvents & kListenEvent_OnScroll)) + return; + + // Now testing a timeout, but a view is scrolled before timeout. + if (gCurrentTest.isTimeoutTesting && !gCurrentTest.wasTransactionTimeout) { + is(aEvent.target.id, "", + "The view scrolled before timeout (the expected view after timeout is " + + gCurrentTest.expectedView ? gCurrentTest.expectedView.id : "null" + + "): " + gCurrentTest.description); + runNextTestList(); + return; + } + + // Check whether the scrolled event should be fired or not. + if (!gCurrentTest.expectedView) { + is(aEvent.target.id, "", + "no views should be scrolled (" + + _getScrolledStateText(_getScrolledState(aEvent.target)) + "): " + + gCurrentTest.description); + runNextTestList(); + return; + } + + // Check whether the scrolled view is expected or not. + if (aEvent.target != gCurrentTest.expectedView) { + // If current test can fail randomly and the possible view is same as the + // event target, this failure may be caused by unexpected timeout. + // At this time, we should retry the current tests with slower settings. + if (gCurrentTest.canFailRandomly && + gCurrentTest.canFailRandomly.possibleView == aEvent.target && + gCurrentTestListStatus.retryWhenTransactionTimeout > 0) { + gCurrentTestListStatus.retryWhenTransactionTimeout--; + retryCurrentTestList(); + return; + } + is(aEvent.target.id, gCurrentTest.expectedView.id, + "wrong view was scrolled: " + gCurrentTest.description); + runNextTestList(); + return; + } + + // Check whether the scrolling direction is expected or not. + var expectedState = _getExpectedScrolledState(); + var currentState = _getScrolledState(aEvent.target); + if (expectedState != currentState) { + is(_getScrolledStateText(currentState), + _getScrolledStateText(expectedState), + "scrolled to wrong direction: " + gCurrentTest.description); + runNextTestList(); + return; + } + + ok(true, "passed: " + gCurrentTest.description); + continueTest(); +} + +function onMouseScrollFailed() +{ + /** + * Scroll failed event handler. If testing is failed, this function cancels + * all remains of current test-list, and go to next test-list. + * + * NOTE: This event is fired immediately after |_fireWheelScrollEvent|. + * + * @param gCurrentTest.expectedView + * The expected view which should be scrolled by the wheel event. + * This value can be null. It means any views should not be + * scrolled. When this is not null, this event means the test may + * be failed. + */ + + if (!(gLitesnEvents & kListenEvent_OnScrollFailed)) + return; + + ok(!gCurrentTest.expectedView, + "failed to scroll on current target: " + gCurrentTest.description); + if (gCurrentTest.expectedView) { + runNextTestList(); + return; + } + + continueTest(); +} + +function onTransactionTimeout() +{ + /** + * Scroll transaction timeout event handler. If the timeout is unexpected, + * i.e., |gCurrentTest.isTimeoutTesting| is not true, this function retry + * the current test-list. However, if the current test-list failed by timeout + * |gCurrentTestListStatus.retryWhenTransactionTimeout| times already, marking + * to failed the current test-list, and go to next test-list. + * + * @param gCurrentTest.expectedView + * The expected view which should be scrolled by the wheel event. + * This value can be null. It means any views should not be + * scrolled. When this is not null, this event means the testing may + * be failed. + * @param gCurrentTest.isTimeoutTesting + * If this value is true, the current testing have waited this + * event. Otherwise, the testing may be failed. + * @param gCurrentTestListStatus.retryWhenTransactionTimeout + * If |gCurrentTest.isTimeoutTesting| is not true but this event is + * fired, the failure may be randomly. Then, this event handler + * retry to test the current test-list until this cound will be zero. + */ + + if (!gCurrentTest.isTimeoutTesting && + gCurrentTestListStatus.retryWhenTransactionTimeout > 0) { + gCurrentTestListStatus.retryWhenTransactionTimeout--; + // retry current test list + retryCurrentTestList(); + return; + } + + gCurrentTest.wasTransactionTimeout = true; + + if (!(gLitesnEvents & kListenEvent_OnTransactionTimeout)) + return; + + ok(gCurrentTest.isTimeoutTesting, + "transaction timeout: " + gCurrentTest.description); + if (!gCurrentTest.isTimeoutTesting) { + runNextTestList(); + return; + } + + continueTest(); +} + +/****************************************************************************** + * Main function for this tests + ******************************************************************************/ + +function runNextTestStep() +{ + // When this is first time or the current test list is finised, load next + // test-list. + _clearTimer(); + if (!gCurrentTest) + runNextTestList(); + else + runTestStepAt(gCurrentTestListStatus.nextStepIndex); +} + +function runNextTestList() +{ + _clearTimer(); + + gLitesnEvents = kListenEvent_None; + _clearTransaction(); + resetTimeoutPrefs(); + if (gCurrentTestListStatus.nextListIndex >= gTestLists.length) { + finish(); + return; + } + + gCurrentTestListStatus.nextListIndex++; + gCurrentTestListStatus.retryWhenTransactionTimeout = + _getCurrentTestList().retryWhenTransactionTimeout; + runTestStepAt(0); +} + +function runTestStepAt(aStepIndex) +{ + _clearTimer(); + + disableNonTestMouseEvents(true); + + // load a step of testing. + gCurrentTestListStatus.nextStepIndex = aStepIndex; + gCurrentTest = + _getCurrentTestList().steps[gCurrentTestListStatus.nextStepIndex++]; + if (gCurrentTest) { + gCurrentTest.wasTransactionTimeout = false; + gTimer = setTimeout(gCurrentTest.func, gCurrentTest.delay); + } else { + // If current test-list doesn't have more testing, go to next test-list + // after cleaning up the current transaction. + _clearTransaction(); + runNextTestList(); + } +} + +function retryCurrentTestList() +{ + _clearTimer(); + + gLitesnEvents = kListenEvent_None; + _clearTransaction(); + ok(true, "WARNING: retry current test-list..."); + growUpTimeoutPrefs(); // retry the test with longer timeout settings. + runTestStepAt(0); +} + +function continueTest() +{ + /** + * This function is called from an event handler when a test succeeded. + * + * @param gCurrentTest.repeatTest + * When this is true, onScrollView calls |gCurrentTest.func|. So, + * same test can repeat. Otherwise, this calls |runNextTestStep|. + * @param gCurrentTest.autoRepeatDelay + * The delay value in milliseconds, this is used to call + * |gCurrentTest.func| via |setTimeout|. + */ + + _clearTimer(); + gLitesnEvents = kListenEvent_OnTransactionTimeout; + + // We should call each functions via setTimeout. Because sometimes this test + // is broken by stack overflow. + if (gCurrentTest.repeatTest) { + gTimer = setTimeout(gCurrentTest.func, gCurrentTest.autoRepeatDelay); + } else { + gTimer = setTimeout(runNextTestStep, 0); + } +} + +]]> +</script> + +</window> |