diff options
Diffstat (limited to 'widget/tests/browser')
-rw-r--r-- | widget/tests/browser/browser.ini | 17 | ||||
-rw-r--r-- | widget/tests/browser/browser_test_ContentCache.js | 302 | ||||
-rw-r--r-- | widget/tests/browser/browser_test_InputContextURI.js | 159 | ||||
-rw-r--r-- | widget/tests/browser/browser_test_clipboardcache.js | 145 | ||||
-rw-r--r-- | widget/tests/browser/browser_test_swipe_gesture.js | 1012 | ||||
-rw-r--r-- | widget/tests/browser/helper_swipe_gesture.html | 20 |
6 files changed, 1655 insertions, 0 deletions
diff --git a/widget/tests/browser/browser.ini b/widget/tests/browser/browser.ini new file mode 100644 index 0000000000..837b41feef --- /dev/null +++ b/widget/tests/browser/browser.ini @@ -0,0 +1,17 @@ +[browser_test_clipboardcache.js] +skip-if = + os == 'android' || (os == 'linux' && ccov) || tsan # Bug 1613516, the test consistently timeouts on Linux coverage builds. + os == "win" && bits == 32 && !debug # Bug 1759422 +[browser_test_ContentCache.js] +skip-if = os == 'android' +[browser_test_InputContextURI.js] +skip-if = os == 'android' +[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 +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..1859e682ec --- /dev/null +++ b/widget/tests/browser/browser_test_ContentCache.js @@ -0,0 +1,302 @@ +/* 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..8279b76fa6 --- /dev/null +++ b/widget/tests/browser/browser_test_InputContextURI.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" +); +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..6da1be9a26 --- /dev/null +++ b/widget/tests/browser/browser_test_clipboardcache.js @@ -0,0 +1,145 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +// 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_swipe_gesture.js b/widget/tests/browser/browser_test_swipe_gesture.js new file mode 100644 index 0000000000..1c7b413b1c --- /dev/null +++ b/widget/tests/browser/browser_test_swipe_gesture.js @@ -0,0 +1,1012 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +/* import-globals-from ../../..//gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js */ + +"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"], + ["browser.swipe.navigation-icon-move-distance", 0], + ["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.loadURI(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"); + ok( + 0.98 < computedOpacity && computedOpacity < 0.99, + "opacity of prevbox is not quite 1" + ); + let opacity = gHistorySwipeAnimation._prevBox.style.opacity; + ok(0.98 < opacity && opacity < 0.99, "opacity of prevbox is not quite 1"); + + const translateDistance = Services.prefs.getIntPref( + "browser.swipe.navigation-icon-move-distance", + 0 + ); + if (translateDistance != 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 (translateDistance != 0) { + // 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"], + ["browser.swipe.navigation-icon-move-distance", 0], + ["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.loadURI(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"); + ok( + 0.98 < computedOpacity && computedOpacity < 0.99, + "opacity of prevbox is not quite 1" + ); + let opacity = gHistorySwipeAnimation._prevBox.style.opacity; + ok(0.98 < opacity && opacity < 0.99, "opacity of prevbox is not quite 1"); + + 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"], + ["browser.swipe.navigation-icon-move-distance", 0], + ["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.loadURI(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.loadURI(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.loadURI(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.loadURI(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 transitionPromise = new Promise(resolve => { + gHistorySwipeAnimation._prevBox.addEventListener( + "transitionstart", + 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 transitionPromise; + + await TestUtils.waitForCondition(() => { + return ( + gHistorySwipeAnimation._prevBox == null && + gHistorySwipeAnimation._nextBox == 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.loadURI(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.loadURI( + 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.loadURI( + 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.eight", "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.loadURI( + 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(); +}); + +// 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.loadURI(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/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> |