summaryrefslogtreecommitdiffstats
path: root/widget/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'widget/tests/browser')
-rw-r--r--widget/tests/browser/browser.ini17
-rw-r--r--widget/tests/browser/browser_test_ContentCache.js302
-rw-r--r--widget/tests/browser/browser_test_InputContextURI.js159
-rw-r--r--widget/tests/browser/browser_test_clipboardcache.js145
-rw-r--r--widget/tests/browser/browser_test_swipe_gesture.js1012
-rw-r--r--widget/tests/browser/helper_swipe_gesture.html20
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>