diff options
Diffstat (limited to 'widget/tests/browser')
19 files changed, 3496 insertions, 0 deletions
diff --git a/widget/tests/browser/browser.ini b/widget/tests/browser/browser.ini new file mode 100644 index 0000000000..5bd1ce0dc1 --- /dev/null +++ b/widget/tests/browser/browser.ini @@ -0,0 +1,66 @@ +[DEFAULT] +skip-if = os == 'andriod' + +[browser_test_ContentCache.js] +[browser_test_InputContextURI.js] +[browser_test_clipboardcache.js] +skip-if = + (os == 'linux' && ccov) || tsan # Bug 1613516, the test consistently timeouts on Linux coverage builds. + 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] +run-if = (os == 'mac' || os == 'win' || os == 'linux') +skip-if = + os == "win" && 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_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_clipboardcache.js b/widget/tests/browser/browser_test_clipboardcache.js new file mode 100644 index 0000000000..bce0b9a918 --- /dev/null +++ b/widget/tests/browser/browser_test_clipboardcache.js @@ -0,0 +1,141 @@ +/* -*- 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; + 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. + + let { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + // Path from nsAnonymousTemporaryFile.cpp, GetTempDir. + dir = FileUtils.getFile("TmpD", ["mozilla-temp-files"]); + } else { + dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + 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..c358d335d8 --- /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: -moz-win-borderless-glass; + } + `; + 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..e66e2f10b9 --- /dev/null +++ b/widget/tests/browser/browser_test_swipe_gesture.js @@ -0,0 +1,1275 @@ +/* -*- 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 + // whole-page-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.whole-page-pixel-size", 550.0], + ], + }); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.loadURIString(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 whole-page-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 + // whole-page-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.whole-page-pixel-size", 1100.0], + ], + }); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.loadURIString(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 + // whole-page-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.whole-page-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.loadURIString(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 + // whole-page-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.whole-page-pixel-size", 550.0], + ], + }); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.loadURIString(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.whole-page-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.loadURIString(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 whole-page-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.whole-page-pixel-size", 1.0], + ], + }); + + const firstPage = "about:about"; + const secondPage = "about:mozilla"; + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + firstPage, + true /* waitForLoad */ + ); + + BrowserTestUtils.loadURIString(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.loadURIString(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.loadURIString( + 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.loadURIString( + 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.loadURIString( + 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.loadURIString(tab.linkedBrowser, "about:mozilla"); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false /* includeSubFrames */, + "about:mozilla" + ); + + BrowserTestUtils.loadURIString(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.loadURIString( + 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.loadURIString(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> |