From 4caa33d62760a19e26ca09ccef7003f1393a7e6c Mon Sep 17 00:00:00 2001
From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com>
Date: Wed, 15 Apr 2026 21:25:31 +0200
Subject: [PATCH] gh-13267: Add unit tests for site control panel (gh-13268)
---
src/zen/tests/manifest.toml | 10 +
src/zen/tests/mochitests/moz.build | 1 +
.../tests/mochitests/readermode/browser.toml | 82 ++++
.../browser_bug1124271_readerModePinnedTab.js | 57 +++
.../browser_bug1453818_samesite_cookie.js | 138 ++++++
...browser_bug1780350_readerModeSaveScroll.js | 70 +++
.../readermode/browser_drag_url_readerMode.js | 61 +++
.../browser_localfile_readerMode.js | 54 +++
.../readermode/browser_readerMode.js | 410 ++++++++++++++++++
.../readermode/browser_readerMode_bc_reuse.js | 44 ++
.../readermode/browser_readerMode_cached.js | 32 ++
.../browser_readerMode_colorSchemePref.js | 172 ++++++++
.../browser_readerMode_customColorScheme.js | 62 +++
.../browser_readerMode_hidden_nodes.js | 56 +++
.../readermode/browser_readerMode_menu.js | 161 +++++++
.../browser_readerMode_readingTime.js | 111 +++++
.../readermode/browser_readerMode_refresh.js | 56 +++
.../browser_readerMode_remoteType.js | 87 ++++
...ser_readerMode_samesite_cookie_redirect.js | 52 +++
.../browser_readerMode_tabnavigation.js | 60 +++
.../browser_readerMode_textLayoutPref.js | 205 +++++++++
.../browser_readerMode_with_anchor.js | 89 ++++
.../mochitests/readermode/getCookies.sjs | 16 +
src/zen/tests/mochitests/readermode/head.js | 10 +
.../readermode/linkToGetCookies.html | 13 +
.../readermode/readerModeArticle.html | 28 ++
.../readerModeArticleContainsLink.html | 20 +
.../readerModeArticleHiddenNodes.html | 22 +
.../readermode/readerModeArticleMedium.html | 16 +
.../readermode/readerModeArticleShort.html | 14 +
.../readermode/readerModeArticleTextPlain.txt | 10 +
.../readermode/readerModeNonArticle.html | 9 +
.../readermode/readerModeRandom.sjs | 23 +
.../readermode/setSameSiteCookie.html | 9 +
.../setSameSiteCookie.html^headers^ | 1 +
src/zen/tests/moz.build | 1 +
src/zen/tests/site_control/browser.toml | 20 +
.../browser_site_control_header_buttons.js | 110 +++++
.../browser_site_control_manage_addons.js | 88 ++++
...ser_site_control_shows_installed_addons.js | 112 +++++
.../browser_site_data_panel_opens.js | 32 ++
.../browser_site_data_permission_toggle.js | 59 +++
.../browser_site_data_security_scheme.js | 40 ++
src/zen/tests/site_control/head.js | 45 ++
44 files changed, 2768 insertions(+)
create mode 100644 src/zen/tests/mochitests/readermode/browser.toml
create mode 100644 src/zen/tests/mochitests/readermode/browser_bug1124271_readerModePinnedTab.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_bug1453818_samesite_cookie.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_bug1780350_readerModeSaveScroll.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_drag_url_readerMode.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_localfile_readerMode.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_bc_reuse.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_cached.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_colorSchemePref.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_customColorScheme.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_hidden_nodes.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_menu.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_readingTime.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_refresh.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_remoteType.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_samesite_cookie_redirect.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_tabnavigation.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_textLayoutPref.js
create mode 100644 src/zen/tests/mochitests/readermode/browser_readerMode_with_anchor.js
create mode 100644 src/zen/tests/mochitests/readermode/getCookies.sjs
create mode 100644 src/zen/tests/mochitests/readermode/head.js
create mode 100644 src/zen/tests/mochitests/readermode/linkToGetCookies.html
create mode 100644 src/zen/tests/mochitests/readermode/readerModeArticle.html
create mode 100644 src/zen/tests/mochitests/readermode/readerModeArticleContainsLink.html
create mode 100644 src/zen/tests/mochitests/readermode/readerModeArticleHiddenNodes.html
create mode 100644 src/zen/tests/mochitests/readermode/readerModeArticleMedium.html
create mode 100644 src/zen/tests/mochitests/readermode/readerModeArticleShort.html
create mode 100644 src/zen/tests/mochitests/readermode/readerModeArticleTextPlain.txt
create mode 100644 src/zen/tests/mochitests/readermode/readerModeNonArticle.html
create mode 100644 src/zen/tests/mochitests/readermode/readerModeRandom.sjs
create mode 100644 src/zen/tests/mochitests/readermode/setSameSiteCookie.html
create mode 100644 src/zen/tests/mochitests/readermode/setSameSiteCookie.html^headers^
create mode 100644 src/zen/tests/site_control/browser.toml
create mode 100644 src/zen/tests/site_control/browser_site_control_header_buttons.js
create mode 100644 src/zen/tests/site_control/browser_site_control_manage_addons.js
create mode 100644 src/zen/tests/site_control/browser_site_control_shows_installed_addons.js
create mode 100644 src/zen/tests/site_control/browser_site_data_panel_opens.js
create mode 100644 src/zen/tests/site_control/browser_site_data_permission_toggle.js
create mode 100644 src/zen/tests/site_control/browser_site_data_security_scheme.js
create mode 100644 src/zen/tests/site_control/head.js
diff --git a/src/zen/tests/manifest.toml b/src/zen/tests/manifest.toml
index 18334b2f8..e0518b35f 100644
--- a/src/zen/tests/manifest.toml
+++ b/src/zen/tests/manifest.toml
@@ -3,6 +3,16 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+[readermode]
+source = "toolkit/components/reader/tests/browser"
+is_direct_path = true
+disable = [
+ "browser_drag_url_readerMode.js",
+ "browser_localfile_readerMode.js",
+ "browser_readerMode.js",
+ "browser_readerMode_colorSchemePref.js",
+]
+
[safebrowsing]
source = "browser/components/safebrowsing/content/test"
is_direct_path = true
diff --git a/src/zen/tests/mochitests/moz.build b/src/zen/tests/mochitests/moz.build
index e6636a897..9aed50afd 100644
--- a/src/zen/tests/mochitests/moz.build
+++ b/src/zen/tests/mochitests/moz.build
@@ -8,6 +8,7 @@
BROWSER_CHROME_MANIFESTS += [
+ "readermode/browser.toml",
"safebrowsing/browser.toml",
"sandbox/browser.toml",
"shell/browser.toml",
diff --git a/src/zen/tests/mochitests/readermode/browser.toml b/src/zen/tests/mochitests/readermode/browser.toml
new file mode 100644
index 000000000..83597a756
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser.toml
@@ -0,0 +1,82 @@
+[DEFAULT]
+support-files = ["head.js"]
+
+["browser_bug1124271_readerModePinnedTab.js"]
+support-files = ["readerModeArticle.html"]
+
+["browser_bug1453818_samesite_cookie.js"]
+support-files = [
+ "getCookies.sjs",
+ "linkToGetCookies.html",
+ "setSameSiteCookie.html",
+ "setSameSiteCookie.html^headers^",
+]
+
+["browser_bug1780350_readerModeSaveScroll.js"]
+support-files = ["readerModeArticleContainsLink.html"]
+
+["browser_drag_url_readerMode.js"]
+disabled="Disabled by import_external_tests.py"
+support-files = ["readerModeArticle.html"]
+
+["browser_localfile_readerMode.js"]
+disabled="Disabled by import_external_tests.py"
+support-files = ["readerModeArticle.html"]
+
+["browser_readerMode.js"]
+disabled="Disabled by import_external_tests.py"
+support-files = [
+ "readerModeNonArticle.html",
+ "readerModeArticle.html",
+ "readerModeArticleHiddenNodes.html",
+]
+
+["browser_readerMode_bc_reuse.js"]
+support-files = ["readerModeArticle.html"]
+
+["browser_readerMode_cached.js"]
+support-files = ["readerModeRandom.sjs"]
+
+["browser_readerMode_colorSchemePref.js"]
+disabled="Disabled by import_external_tests.py"
+support-files = ["readerModeArticle.html"]
+
+["browser_readerMode_customColorScheme.js"]
+support-files = ["readerModeArticle.html"]
+
+["browser_readerMode_hidden_nodes.js"]
+support-files = ["readerModeArticleHiddenNodes.html"]
+
+["browser_readerMode_menu.js"]
+support-files = ["readerModeArticleShort.html"]
+
+["browser_readerMode_readingTime.js"]
+support-files = [
+ "readerModeArticle.html",
+ "readerModeArticleShort.html",
+ "readerModeArticleMedium.html",
+]
+
+["browser_readerMode_refresh.js"]
+support-files = [
+ "readerModeArticleShort.html",
+ "readerModeArticleTextPlain.txt",
+]
+
+["browser_readerMode_remoteType.js"]
+support-files = ["readerModeArticleShort.html"]
+
+["browser_readerMode_samesite_cookie_redirect.js"]
+support-files = [
+ "getCookies.sjs",
+ "setSameSiteCookie.html",
+ "setSameSiteCookie.html^headers^",
+]
+
+["browser_readerMode_tabnavigation.js"]
+
+["browser_readerMode_textLayoutPref.js"]
+support-files = ["readerModeArticle.html"]
+
+["browser_readerMode_with_anchor.js"]
+support-files = ["readerModeArticle.html"]
diff --git a/src/zen/tests/mochitests/readermode/browser_bug1124271_readerModePinnedTab.js b/src/zen/tests/mochitests/readermode/browser_bug1124271_readerModePinnedTab.js
new file mode 100644
index 000000000..2eb609578
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_bug1124271_readerModePinnedTab.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that the reader mode button won't open in a new tab when clicked from a pinned tab
+
+const PREF = "reader.parse-on-load.enabled";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+var readerButton = document.getElementById("reader-mode-button");
+
+add_task(async function () {
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF);
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+
+ // Enable the reader mode button.
+ Services.prefs.setBoolPref(PREF, true);
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ gBrowser.pinTab(tab);
+
+ let initialTabsCount = gBrowser.tabs.length;
+
+ // Point tab to a test page that is reader-able.
+ let url = TEST_PATH + "readerModeArticle.html";
+ await BrowserTestUtils.loadURIString({
+ browser: tab.linkedBrowser,
+ uriString: url,
+ });
+ await TestUtils.waitForCondition(() => !readerButton.hidden);
+
+ let tabLoadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ readerButton.click();
+ await tabLoadPromise;
+
+ // Ensure no new tabs are opened when exiting reader mode in a pinned tab
+ is(gBrowser.tabs.length, initialTabsCount, "No additional tabs were opened.");
+
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow"
+ );
+ readerButton.click();
+ await pageShownPromise;
+ // Ensure no new tabs are opened when exiting reader mode in a pinned tab
+ is(gBrowser.tabs.length, initialTabsCount, "No additional tabs were opened.");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_bug1453818_samesite_cookie.js b/src/zen/tests/mochitests/readermode/browser_bug1453818_samesite_cookie.js
new file mode 100644
index 000000000..326ebdcaf
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_bug1453818_samesite_cookie.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ORIGIN1 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+const TEST_ORIGIN2 = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.org"
+);
+
+async function clickLink(browser) {
+ info("Waiting for the page to load after clicking the link...");
+ let pageLoaded = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "DOMContentLoaded"
+ );
+ await SpecialPowers.spawn(browser, [], async function () {
+ let link = content.document.getElementById("link");
+ ok(link, "The link element was found.");
+ link.click();
+ });
+ await pageLoaded;
+}
+
+async function checkCookiePresent(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ let cookieSpan = content.document.getElementById("cookieSpan");
+ ok(cookieSpan, "cookieSpan element should be in document");
+ is(
+ cookieSpan.textContent,
+ "foo=bar",
+ "The SameSite cookie was sent correctly."
+ );
+ });
+}
+
+async function checkCookie(browser) {
+ info("Check that the SameSite cookie was not sent.");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let cookieSpan = content.document.getElementById("cookieSpan");
+ ok(cookieSpan, "cookieSpan element should be in document");
+ is(
+ cookieSpan.textContent,
+ "",
+ "The SameSite cookie was blocked correctly."
+ );
+ });
+}
+
+async function runTest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["reader.parse-on-load.enabled", true]],
+ });
+
+ info("Set a SameSite=strict cookie.");
+ await BrowserTestUtils.withNewTab(
+ TEST_ORIGIN1 + "setSameSiteCookie.html",
+ () => {}
+ );
+
+ info("Check that the cookie has been correctly set.");
+ await BrowserTestUtils.withNewTab(
+ TEST_ORIGIN1 + "getCookies.sjs",
+ async function (browser) {
+ await checkCookiePresent(browser);
+ }
+ );
+
+ info(
+ "Open a cross-origin page with a link to the domain that set the cookie."
+ );
+ {
+ let browser;
+ let pageLoaded;
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ () => {
+ let t = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_ORIGIN2 + "linkToGetCookies.html"
+ );
+ gBrowser.selectedTab = t;
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "DOMContentLoaded"
+ );
+ return t;
+ },
+ false
+ );
+
+ info("Waiting for the page to load in normal mode...");
+ await pageLoaded;
+
+ await clickLink(browser);
+ await checkCookie(browser);
+ await BrowserTestUtils.removeTab(tab);
+ }
+
+ info("Open the cross-origin page again.");
+ await BrowserTestUtils.withNewTab(
+ TEST_ORIGIN2 + "linkToGetCookies.html",
+ async function (browser) {
+ let pageShown = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let readerButton = document.getElementById("reader-mode-button");
+ ok(readerButton, "readerButton should be available");
+ readerButton.click();
+
+ info("Waiting for the page to be displayed in reader mode...");
+ await pageShown;
+
+ await clickLink(browser);
+ await checkCookie(browser);
+ }
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["test.wait300msAfterTabSwitch", true]],
+ });
+});
+
+add_task(async function () {
+ await runTest(true);
+});
+
+add_task(async function () {
+ await runTest(false);
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_bug1780350_readerModeSaveScroll.js b/src/zen/tests/mochitests/readermode/browser_bug1780350_readerModeSaveScroll.js
new file mode 100644
index 000000000..76add5511
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_bug1780350_readerModeSaveScroll.js
@@ -0,0 +1,70 @@
+/* eslint-disable @microsoft/sdl/no-insecure-url */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+// This test verifies that when a link is clicked from
+// within reader view, when navigating back to reader view
+// the previous scroll position of the page is restored.
+add_task(async function test_save_scroll_position() {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticleContainsLink.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(browser);
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await Promise.all([pageShownPromise, browserLoadedPromise]);
+ let scrollEventPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "scroll",
+ true
+ );
+ // Set scroll position in reader to 200px down.
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.documentElement.scrollTop = 200;
+ });
+ await scrollEventPromise;
+
+ // Click linked page and check that scroll pos resets.
+ await SpecialPowers.spawn(browser, [], async function () {
+ let linkElement = content.document.getElementById("link");
+ linkElement.click();
+ });
+ await BrowserTestUtils.browserLoaded(browser);
+ await SpecialPowers.spawn(browser, [], async function () {
+ is(
+ content.document.documentElement.scrollTop,
+ 0,
+ "vertical scroll position should reset to zero when navigating to linked page."
+ );
+ content.window.history.back();
+ });
+
+ // Navigate back to reader and check that scroll position is restored.
+ await BrowserTestUtils.browserLoaded(browser);
+ scrollEventPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "scroll",
+ true
+ );
+ await scrollEventPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ is(
+ doc.documentElement.scrollTop,
+ 200,
+ "should restore saved scroll position when navigating back from link."
+ );
+ });
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_drag_url_readerMode.js b/src/zen/tests/mochitests/readermode/browser_drag_url_readerMode.js
new file mode 100644
index 000000000..b34f8bb97
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_drag_url_readerMode.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(async function test_readerModeURLDrag() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PATH + "readerModeArticle.html",
+ },
+
+ async browser => {
+ let readerButton = document.getElementById("reader-mode-button");
+ await TestUtils.waitForCondition(
+ () => !readerButton.hidden,
+ "Reader mode button should become visible"
+ );
+
+ is_element_visible(
+ readerButton,
+ "Reader mode button is present on a reader-able page"
+ );
+
+ // Switch page into reader mode.
+ let promiseTabLoad = BrowserTestUtils.browserLoaded(browser);
+ readerButton.click();
+ await promiseTabLoad;
+ let urlbar = gURLBar.inputField;
+ let readerUrl = gBrowser.selectedBrowser.currentURI.spec;
+ ok(
+ readerUrl.startsWith("about:reader"),
+ "about:reader loaded after clicking reader mode button"
+ );
+
+ let dataTran = new DataTransfer();
+ let urlEvent = new DragEvent("dragstart", { dataTransfer: dataTran });
+ let oldUrl = TEST_PATH + "readerModeArticle.html";
+ let urlBarContainer = gURLBar.querySelector(".urlbar-input-container");
+ // We intentionally turn off a11y_checks for the following click, because
+ // it is send to prepare the URL Bar for the mouse-specific action - for a
+ // drag event, while there are other ways are accessible for users of
+ // assistive technology and keyboards, therefore this test can be excluded
+ // from the accessibility tests.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ urlBarContainer.click();
+ AccessibilityUtils.resetEnv();
+ urlbar.dispatchEvent(urlEvent);
+
+ let newUrl = urlEvent.dataTransfer.getData("text/plain");
+ ok(!newUrl.includes("about:reader"), "URL does not contain about:reader");
+
+ Assert.equal(newUrl, oldUrl, "URL is the same");
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_localfile_readerMode.js b/src/zen/tests/mochitests/readermode/browser_localfile_readerMode.js
new file mode 100644
index 000000000..118e4bb23
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_localfile_readerMode.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_BASE_URI = getResolvedURI(getRootDirectory(gTestPath)).spec;
+
+let readerButton = document.getElementById("reader-mode-button");
+
+/**
+ * Reader mode should work on local files.
+ */
+add_task(async function test_readermode_available_for_local_files() {
+ await BrowserTestUtils.withNewTab(
+ TEST_BASE_URI + "readerModeArticle.html",
+ async function (browser) {
+ await TestUtils.waitForCondition(
+ () => !readerButton.hidden,
+ "Reader mode button should become visible"
+ );
+
+ is_element_visible(
+ readerButton,
+ "Reader mode button is present on a reader-able page"
+ );
+
+ // Switch page into reader mode.
+ let promiseTabLoad = BrowserTestUtils.browserLoaded(browser);
+ readerButton.click();
+ await promiseTabLoad;
+
+ let readerUrl = gBrowser.selectedBrowser.currentURI.spec;
+ ok(
+ readerUrl.startsWith("about:reader"),
+ "about:reader loaded after clicking reader mode button"
+ );
+ is_element_visible(
+ readerButton,
+ "Reader mode button is present on about:reader"
+ );
+
+ is(
+ gURLBar.untrimmedValue,
+ TEST_BASE_URI + "readerModeArticle.html",
+ "gURLBar value is about:reader URL"
+ );
+ is(
+ gURLBar.value,
+ gURLBar.untrimmedValue,
+ "gURLBar is displaying original article URL"
+ );
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode.js b/src/zen/tests/mochitests/readermode/browser_readerMode.js
new file mode 100644
index 000000000..8def225f6
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode.js
@@ -0,0 +1,410 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test that the reader mode button appears and works properly on
+ * reader-able content.
+ */
+const TEST_PREFS = [["reader.parse-on-load.enabled", true]];
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+var readerButton = document.getElementById("reader-mode-button");
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+add_task(async function test_reader_button() {
+ registerCleanupFunction(function () {
+ // Reset test prefs.
+ TEST_PREFS.forEach(([name]) => {
+ Services.prefs.clearUserPref(name);
+ });
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+
+ // Set required test prefs.
+ TEST_PREFS.forEach(([name, value]) => {
+ Services.prefs.setBoolPref(name, value);
+ });
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ is_element_hidden(
+ readerButton,
+ "Reader mode button is not present on a new tab"
+ );
+ ok(
+ !UITour.isInfoOnTarget(window, "readerMode-urlBar"),
+ "Info panel shouldn't appear without the reader mode button"
+ );
+
+ // Point tab to a test page that is reader-able.
+ let url = TEST_PATH + "readerModeArticle.html";
+ // Set up favicon for testing.
+ let favicon =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2OUAAAAABJRU5ErkJggg==";
+ info("Adding visit so we can add favicon");
+ await PlacesTestUtils.addVisits(new URL(url));
+ info("Adding favicon");
+ await PlacesTestUtils.addFavicons(new Map([[url, favicon]]));
+ info("Opening tab and waiting for reader mode button to show up");
+
+ await BrowserTestUtils.loadURIString({
+ browser: tab.linkedBrowser,
+ uriString: url,
+ });
+ await TestUtils.waitForCondition(() => !readerButton.hidden);
+
+ is_element_visible(
+ readerButton,
+ "Reader mode button is present on a reader-able page"
+ );
+
+ // Switch page into reader mode.
+ let promiseTabLoad = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ readerButton.click();
+ await promiseTabLoad;
+
+ let readerUrl = gBrowser.selectedBrowser.currentURI.spec;
+ ok(
+ readerUrl.startsWith("about:reader"),
+ "about:reader loaded after clicking reader mode button"
+ );
+ is_element_visible(
+ readerButton,
+ "Reader mode button is present on about:reader"
+ );
+ let iconEl = tab.iconImage;
+ await TestUtils.waitForCondition(
+ () => iconEl.getBoundingClientRect().width != 0
+ );
+ is_element_visible(iconEl, "Favicon should be visible");
+ is(iconEl.src, favicon, "Correct favicon should be loaded");
+
+ is(gURLBar.untrimmedValue, url, "gURLBar value is about:reader URL");
+ is(
+ gURLBar.value,
+ UrlbarTestUtils.trimURL(url),
+ "gURLBar is displaying original article URL"
+ );
+
+ // Check selected value for URL bar
+ await new Promise((resolve, reject) => {
+ waitForClipboard(
+ url,
+ function () {
+ gURLBar.focus();
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ },
+ resolve,
+ reject
+ );
+ });
+
+ info("Got correct URL when copying");
+
+ // Switch page back out of reader mode.
+ let promisePageShow = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow",
+ false,
+ e => e.target.location.href != "about:blank"
+ );
+ readerButton.click();
+ await promisePageShow;
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ url,
+ "Back to the original page after clicking active reader mode button"
+ );
+ ok(
+ gBrowser.selectedBrowser.canGoForward,
+ "Moved one step back in the session history."
+ );
+
+ let nonReadableUrl = TEST_PATH + "readerModeNonArticle.html";
+
+ // Load a new tab that is NOT reader-able.
+ let newTab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ await BrowserTestUtils.loadURIString({
+ browser: newTab.linkedBrowser,
+ uriString: nonReadableUrl,
+ });
+ await TestUtils.waitForCondition(() => readerButton.hidden);
+ is_element_hidden(
+ readerButton,
+ "Reader mode button is not present on a non-reader-able page"
+ );
+
+ // Switch back to the original tab to make sure reader mode button is still visible.
+ gBrowser.removeCurrentTab();
+ await TestUtils.waitForCondition(() => !readerButton.hidden);
+ is_element_visible(
+ readerButton,
+ "Reader mode button is present on a reader-able page"
+ );
+
+ // Load a new tab in reader mode that is NOT reader-able in the reader mode.
+ newTab = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let promiseAboutReaderError = BrowserTestUtils.waitForContentEvent(
+ newTab.linkedBrowser,
+ "AboutReaderContentError"
+ );
+ await BrowserTestUtils.loadURIString({
+ browser: newTab.linkedBrowser,
+ uriString: "about:reader?url=" + nonReadableUrl,
+ });
+ await promiseAboutReaderError;
+ await TestUtils.waitForCondition(() => !readerButton.hidden);
+ is_element_visible(
+ readerButton,
+ "Reader mode button is present on about:reader even in error state"
+ );
+
+ // Switch page back out of reader mode.
+ promisePageShow = BrowserTestUtils.waitForContentEvent(
+ newTab.linkedBrowser,
+ "pageshow",
+ false,
+ e => e.target.location.href != "about:blank"
+ );
+ readerButton.click();
+ await promisePageShow;
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ nonReadableUrl,
+ "Back to the original non-reader-able page after clicking active reader mode button"
+ );
+ await TestUtils.waitForCondition(() => readerButton.hidden);
+ is_element_hidden(
+ readerButton,
+ "Reader mode button is not present on a non-reader-able page"
+ );
+});
+
+add_task(async function test_getOriginalUrl() {
+ let { ReaderMode } = ChromeUtils.importESModule(
+ "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs"
+ );
+ let url = "https://foo.com/article.html";
+
+ is(
+ ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(url)),
+ url,
+ "Found original URL from encoded URL"
+ );
+ is(
+ ReaderMode.getOriginalUrl("about:reader?foobar"),
+ null,
+ "Did not find original URL from malformed reader URL"
+ );
+ is(
+ ReaderMode.getOriginalUrl(url),
+ null,
+ "Did not find original URL from non-reader URL"
+ );
+
+ let badUrl = "https://foo.com/?;$%^^";
+ is(
+ ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(badUrl)),
+ badUrl,
+ "Found original URL from encoded malformed URL"
+ );
+ is(
+ ReaderMode.getOriginalUrl("about:reader?url=" + badUrl),
+ badUrl,
+ "Found original URL from non-encoded malformed URL"
+ );
+});
+
+add_task(async function test_reader_view_element_attribute_transform() {
+ registerCleanupFunction(function () {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+
+ function observeAttribute(element, attributes, triggerFn, checkFn) {
+ return new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ for (let mu of mutations) {
+ if (element.getAttribute(mu.attributeName) !== mu.oldValue) {
+ if (checkFn()) {
+ resolve();
+ observer.disconnect();
+ return;
+ }
+ }
+ }
+ });
+
+ observer.observe(element, {
+ attributes: true,
+ attributeOldValue: true,
+ attributeFilter: Array.isArray(attributes) ? attributes : [attributes],
+ });
+
+ triggerFn();
+ });
+ }
+
+ let menuitem = document.getElementById("menu_readerModeItem");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ is(
+ menuitem.hidden,
+ true,
+ "menuitem element should have the hidden attribute"
+ );
+
+ info("Navigate a reader-able page");
+ function waitForNonBlankPage() {
+ return BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow",
+ false,
+ e => e.target.location.href != "about:blank"
+ );
+ }
+ let waitForPageshow = waitForNonBlankPage();
+ await observeAttribute(
+ menuitem,
+ "hidden",
+ () => {
+ let url = TEST_PATH + "readerModeArticle.html";
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+ },
+ () => !menuitem.hidden
+ );
+ is(
+ menuitem.hidden,
+ false,
+ "menuitem's hidden attribute should be false on a reader-able page"
+ );
+ await waitForPageshow;
+
+ info("Navigate a non-reader-able page");
+ waitForPageshow = waitForNonBlankPage();
+ await observeAttribute(
+ menuitem,
+ "hidden",
+ () => {
+ let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+ },
+ () => menuitem.hidden
+ );
+ is(
+ menuitem.hidden,
+ true,
+ "menuitem's hidden attribute should be true on a non-reader-able page"
+ );
+ await waitForPageshow;
+
+ info("Navigate a reader-able page");
+ waitForPageshow = waitForNonBlankPage();
+ await observeAttribute(
+ menuitem,
+ "hidden",
+ () => {
+ let url = TEST_PATH + "readerModeArticle.html";
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+ },
+ () => !menuitem.hidden
+ );
+ is(
+ menuitem.hidden,
+ false,
+ "menuitem's hidden attribute should be false on a reader-able page"
+ );
+ await waitForPageshow;
+
+ info("Enter Reader Mode");
+ waitForPageshow = waitForNonBlankPage();
+ await observeAttribute(
+ readerButton,
+ "readeractive",
+ () => {
+ readerButton.click();
+ },
+ () => readerButton.getAttribute("readeractive") == "true"
+ );
+ is(
+ readerButton.getAttribute("readeractive"),
+ "true",
+ "readerButton's readeractive attribute should be true when entering reader mode"
+ );
+ await waitForPageshow;
+
+ info("Exit Reader Mode");
+ waitForPageshow = waitForNonBlankPage();
+ await observeAttribute(
+ readerButton,
+ ["readeractive", "hidden"],
+ () => {
+ readerButton.click();
+ },
+ () => {
+ info(
+ `active: ${readerButton.getAttribute("readeractive")}; hidden: ${
+ menuitem.hidden
+ }`
+ );
+ return !readerButton.getAttribute("readeractive") && !menuitem.hidden;
+ }
+ );
+ ok(
+ !readerButton.getAttribute("readeractive"),
+ "readerButton's readeractive attribute should be empty when reader mode is exited"
+ );
+ ok(!menuitem.hidden, "menuitem should not be hidden.");
+ await waitForPageshow;
+
+ info("Navigate a non-reader-able page");
+ waitForPageshow = waitForNonBlankPage();
+ await observeAttribute(
+ menuitem,
+ "hidden",
+ () => {
+ let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+ },
+ () => menuitem.hidden
+ );
+ is(
+ menuitem.hidden,
+ true,
+ "menuitem's hidden attribute should be true on a non-reader-able page"
+ );
+ await waitForPageshow;
+});
+
+add_task(async function test_reader_mode_lang() {
+ let url = TEST_PATH + "readerModeArticle.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+
+ await BrowserTestUtils.loadURIString({
+ browser: tab.linkedBrowser,
+ uriString: url,
+ });
+ await TestUtils.waitForCondition(() => !readerButton.hidden);
+
+ // Switch page into reader mode.
+ let promiseTabLoad = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ readerButton.click();
+ await promiseTabLoad;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let container = content.document.querySelector(".container");
+ is(container.lang, "en");
+ });
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_bc_reuse.js b/src/zen/tests/mochitests/readermode/browser_readerMode_bc_reuse.js
new file mode 100644
index 000000000..7e8724ddb
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_bc_reuse.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const TEST_URL = TEST_PATH + "readerModeArticle.html";
+
+add_task(async function test_TODO() {
+ await BrowserTestUtils.withNewTab(
+ "data:text/html,
Opener",
+ async browser => {
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TEST_URL,
+ true
+ );
+ await SpecialPowers.spawn(browser, [TEST_URL], url => {
+ content.eval(`window.x = open("${url}", "_blank");`);
+ });
+ let newTab = await newTabPromise;
+
+ let readerButton = document.getElementById("reader-mode-button");
+ await BrowserTestUtils.waitForMutationCondition(
+ readerButton,
+ { attributes: true },
+ () => !readerButton.hidden
+ );
+ let tabLoaded = BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+ readerButton.click();
+ await tabLoaded;
+ isnot(
+ newTab.linkedBrowser.browsingContext.group.id,
+ browser.browsingContext.group.id,
+ "BC should be in a different group now."
+ );
+ BrowserTestUtils.removeTab(newTab);
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_cached.js b/src/zen/tests/mochitests/readermode/browser_readerMode_cached.js
new file mode 100644
index 000000000..1a301218c
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_cached.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// This test verifies that the article is properly using the cached data
+// when switching into reader mode. The article produces a random number
+// contained within it, so if the article gets reloaded instead of using
+// the cached version, it would have a different value in it.
+const URL =
+ "http://mochi.test:8888/browser/toolkit/components/reader/tests/browser/readerModeRandom.sjs";
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+
+ let randomNumber = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.document.getElementById("rnd").textContent;
+ });
+
+ let promiseTabLoad = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await promiseTabLoad;
+ await TestUtils.waitForCondition(() => !readerButton.hidden);
+
+ let newRandomNumber = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.document.getElementById("rnd").textContent;
+ });
+
+ is(randomNumber, newRandomNumber, "used the same value");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_colorSchemePref.js b/src/zen/tests/mochitests/readermode/browser_readerMode_colorSchemePref.js
new file mode 100644
index 000000000..b1643964a
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_colorSchemePref.js
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+async function testColorScheme(aPref, aExpectation) {
+ // Set the browser content theme to light or dark.
+ Services.prefs.setIntPref("browser.theme.content-theme", aPref);
+
+ // Reader Mode Color Scheme Preference must be manually set by the user, will
+ // default to "auto" initially.
+ Services.prefs.setCharPref("reader.color_scheme", aExpectation);
+
+ let aBodyExpectation = aExpectation;
+ if (aBodyExpectation === "auto") {
+ aBodyExpectation = aPref === 1 ? "light" : "dark";
+ }
+
+ // Open a browser tab, enter reader mode, and test if we have the valid
+ // reader mode color scheme preference pre-selected.
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
+
+ Assert.equal(colorScheme, aExpectation);
+
+ await SpecialPowers.spawn(browser, [aBodyExpectation], expectation => {
+ let bodyClass = content.document.body.className;
+ ok(
+ bodyClass.includes(expectation),
+ "The body of the test document has the correct color scheme."
+ );
+ });
+ }
+ );
+}
+
+/**
+ * Test that opening reader mode maintains the correct color scheme preference
+ * until the user manually sets a different color scheme.
+ */
+add_task(async function () {
+ await testColorScheme(0, "auto");
+ await testColorScheme(1, "auto");
+ await testColorScheme(0, "light");
+ await testColorScheme(1, "light");
+ await testColorScheme(0, "dark");
+ await testColorScheme(1, "dark");
+ await testColorScheme(0, "sepia");
+ await testColorScheme(1, "sepia");
+});
+
+async function testColorsFocus() {
+ // Set the theme selection to auto.
+ Services.prefs.setCharPref("reader.color_scheme", "auto");
+
+ // Open a browser tab, enter reader mode, and test if focus stays
+ // within the menu for the Default tab.
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ let doc = content.document;
+ doc.querySelector(".colors-button").click();
+
+ let defaultTab = doc.querySelector("#tabs-deck-button-fxtheme");
+ let themeButton = doc.querySelector(".auto-button");
+ themeButton.focus();
+
+ EventUtils.synthesizeKey("KEY_Tab", {}, content);
+ is(
+ doc.activeElement,
+ defaultTab,
+ "Focus moves back to the Default tab"
+ );
+
+ let themeInput = doc.querySelector("#radio-itemauto-button");
+ defaultTab.focus();
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, content);
+ is(doc.activeElement, themeInput, "Focus moves to selected theme");
+ });
+ }
+ );
+
+ // Set the theme selection to custom.
+ Services.prefs.setCharPref("reader.color_scheme", "custom");
+
+ // Open a browser tab, enter reader mode, and test if focus stays
+ // within the menu for the Custom tab.
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+ const Event = content.Event;
+ doc.querySelector(".colors-button").click();
+
+ let customTab = doc.querySelector("#tabs-deck-button-customtheme");
+ let resetButton = doc.querySelector(".custom-colors-reset-button");
+
+ ok(
+ ContentTaskUtils.isHidden(resetButton),
+ "Reset button should be hidden to start with"
+ );
+
+ // Simulate changing a color to make the reset button visible.
+ let colorInput = doc.querySelector("moz-input-color");
+ let shadowRoot = colorInput.shadowRoot;
+ let input = shadowRoot.querySelector("input");
+ input.value = "#123456";
+ input.dispatchEvent(
+ new Event("input", { bubbles: true, composed: true })
+ );
+
+ // Wait for the reset button to become visible.
+ await ContentTaskUtils.waitForCondition(() => {
+ return resetButton.hidden === false;
+ }, "Reset Defaults button should be visible after a color change.");
+
+ resetButton.focus();
+
+ EventUtils.synthesizeKey("KEY_Tab", {}, content);
+ is(doc.activeElement, customTab, "Focus moves back to the Custom tab");
+
+ customTab.focus();
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, content);
+ is(doc.activeElement, resetButton, "Focus moves to Reset theme button");
+ });
+ }
+ );
+}
+
+/**
+ * Test that the focus stays within the colors menu.
+ */
+add_task(async function () {
+ await testColorsFocus();
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_customColorScheme.js b/src/zen/tests/mochitests/readermode/browser_readerMode_customColorScheme.js
new file mode 100644
index 000000000..9ee35bb94
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_customColorScheme.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+async function testCustomColors(aPref, color) {
+ // Set the theme selection to custom.
+ Services.prefs.setCharPref("reader.color_scheme", "custom");
+
+ // Set the custom pref to the color value.
+ Services.prefs.setCharPref(`reader.custom_colors.${aPref}`, color);
+
+ // Open a browser tab, enter reader mode, and test if the page colors
+ // reflect the pref selection.
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
+ Assert.equal(colorScheme, "custom");
+ let prefValue = Services.prefs.getStringPref(
+ `reader.custom_colors.${aPref}`
+ );
+ let cssProp = `--custom-theme-${aPref}`;
+
+ await SpecialPowers.spawn(
+ browser,
+ [prefValue, cssProp],
+ (customColor, prop) => {
+ let style = content.window.getComputedStyle(content.document.body);
+ let actualColor = style.getPropertyValue(prop);
+ Assert.equal(customColor, actualColor);
+ }
+ );
+ }
+ );
+}
+
+/**
+ * Test that the custom color scheme selection updates the document colors correctly.
+ */
+add_task(async function () {
+ await testCustomColors("foreground", "#ffffff");
+ await testCustomColors("background", "#000000");
+ await testCustomColors("unvisited-links", "#ffffff");
+ await testCustomColors("visited-links", "#ffffff");
+ await testCustomColors("selection-highlight", "#ffffff");
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_hidden_nodes.js b/src/zen/tests/mochitests/readermode/browser_readerMode_hidden_nodes.js
new file mode 100644
index 000000000..a6d058dfb
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_hidden_nodes.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test that the reader mode button appears and works properly on
+ * reader-able content.
+ */
+const TEST_PREFS = [["reader.parse-on-load.enabled", true]];
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+var readerButton = document.getElementById("reader-mode-button");
+
+add_task(async function test_reader_button() {
+ registerCleanupFunction(function () {
+ // Reset test prefs.
+ TEST_PREFS.forEach(([name]) => {
+ Services.prefs.clearUserPref(name);
+ });
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+
+ // Set required test prefs.
+ TEST_PREFS.forEach(([name, value]) => {
+ Services.prefs.setBoolPref(name, value);
+ });
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ is_element_hidden(
+ readerButton,
+ "Reader mode button is not present on a new tab"
+ );
+ // Point tab to a test page that is not reader-able due to hidden nodes.
+ let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
+ let paintPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "MozAfterPaint",
+ false,
+ e =>
+ e.originalTarget.location.href.endsWith("HiddenNodes.html") &&
+ e.originalTarget.document.readyState == "complete"
+ );
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+ await paintPromise;
+
+ is_element_hidden(
+ readerButton,
+ "Reader mode button is still not present on tab with unreadable content."
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_menu.js b/src/zen/tests/mochitests/readermode/browser_readerMode_menu.js
new file mode 100644
index 000000000..b60a5b110
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_menu.js
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+/**
+ * Test that the reader mode menus open and close correctly on click
+ * and on keyboard input.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticleShort.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ function dispatchMouseEvent(win, target, eventName) {
+ let mouseEvent = new win.MouseEvent(eventName, {
+ view: win,
+ bubbles: true,
+ cancelable: true,
+ composed: true,
+ });
+ target.dispatchEvent(mouseEvent);
+ }
+
+ function simulateClick(target) {
+ dispatchMouseEvent(win, target, "mousedown");
+ dispatchMouseEvent(win, target, "mouseup");
+ dispatchMouseEvent(win, target, "click");
+ }
+
+ async function testOpenCloseDropdown(target) {
+ let button = doc.querySelector(`.${target}-button`);
+
+ let dropdown = doc.querySelector(`.${target}-dropdown`);
+ ok(!dropdown.classList.contains("open"), "dropdown is closed");
+
+ simulateClick(button);
+ ok(dropdown.classList.contains("open"), "dropdown is open");
+
+ // simulate clicking on the article title to close the dropdown
+ let title = doc.querySelector(".reader-title");
+ simulateClick(title);
+ ok(!dropdown.classList.contains("open"), "dropdown is closed");
+
+ // reopen the dropdown
+ simulateClick(button);
+ ok(dropdown.classList.contains("open"), "dropdown is open");
+
+ // now click on the button again to close it
+ simulateClick(button);
+ ok(!dropdown.classList.contains("open"), "dropdown is closed");
+
+ // reopen the dropdown
+ simulateClick(button);
+ ok(dropdown.classList.contains("open"), "dropdown is open");
+
+ // use the ESC key to close it
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+ ok(!dropdown.classList.contains("open"), "dropdown is closed");
+ }
+
+ let doc = content.document;
+ let win = content.window;
+
+ testOpenCloseDropdown("style");
+ testOpenCloseDropdown("colors");
+ });
+ }
+ );
+});
+
+/**
+ * Test that the reader mode menus close on scroll, unless they are
+ * currently being hovered.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticleContainsLink.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ let scrollEventPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "scroll",
+ true
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let dropdown = doc.querySelector(".text-layout-dropdown");
+
+ doc.querySelector(".text-layout-button").click();
+ ok(dropdown.classList.contains("open"), "dropdown is open");
+
+ // hover outside the dropdown and scroll
+ let domain = doc.querySelector(".reader-domain");
+ EventUtils.synthesizeMouseAtCenter(
+ domain,
+ { type: "mousemove" },
+ content.window
+ );
+ content.window.scrollBy(0, 200);
+ });
+ await scrollEventPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let dropdown = content.document.querySelector(".text-layout-dropdown");
+ ok(!dropdown.classList.contains("open"), "dropdown is closed");
+ });
+
+ scrollEventPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "scroll",
+ true
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let dropdown = doc.querySelector(".text-layout-dropdown");
+
+ // reopen the dropdown
+ doc.querySelector(".text-layout-button").click();
+ ok(dropdown.classList.contains("open"), "dropdown is open");
+
+ // hover over the dropdown and scroll
+ let dropdownPopup = dropdown.querySelector(".dropdown-popup");
+ EventUtils.synthesizeMouseAtCenter(
+ dropdownPopup,
+ {
+ type: "mousemove",
+ },
+ content.window
+ );
+ content.window.scrollBy(0, 200);
+ });
+ await scrollEventPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let dropdown = content.document.querySelector(".text-layout-dropdown");
+ ok(dropdown.classList.contains("open"), "dropdown remains open");
+ });
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_readingTime.js b/src/zen/tests/mochitests/readermode/browser_readerMode_readingTime.js
new file mode 100644
index 000000000..602d8030a
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_readingTime.js
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+/**
+ * Test that the reader mode correctly calculates and displays the
+ * estimated reading time for a normal length article
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ // make sure there is a reading time on the page and that it displays the correct information
+ let readingTimeElement = content.document.querySelector(
+ ".reader-estimated-time"
+ );
+ ok(readingTimeElement, "Reading time element should be in document");
+ const args = JSON.parse(readingTimeElement.dataset.l10nArgs);
+ is(args.rangePlural, "other", "Reading time should be '9-12 minutes'");
+ ok(
+ /\b9\b.*\b12\b/.test(args.range),
+ "Reading time should be '9-12 minutes'"
+ );
+
+ let container = content.document.querySelector(".container");
+ let lang = container.getAttribute("lang");
+ is(lang, "en", "Should have reused lang attr from the original page.");
+ });
+ }
+ );
+});
+
+/**
+ * Test that the reader mode correctly calculates and displays the
+ * estimated reading time for a short article
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticleShort.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ // make sure there is a reading time on the page and that it displays the correct information
+ let readingTimeElement = content.document.querySelector(
+ ".reader-estimated-time"
+ );
+ ok(readingTimeElement, "Reading time element should be in document");
+ const args = JSON.parse(readingTimeElement.dataset.l10nArgs);
+ is(args.rangePlural, "one", "Reading time should be '~1 minute'");
+ ok(/\b1\b/.test(args.range), "Reading time should be '~1 minute'");
+
+ let container = content.document.querySelector(".container");
+ ok(
+ !container.hasAttribute("lang"),
+ "Can't detect language, shouldn't set a lang attribute."
+ );
+ });
+ }
+ );
+});
+
+/**
+ * Test that the reader mode correctly calculates and displays the
+ * estimated reading time for a medium article where a single number
+ * is displayed.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticleMedium.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ // make sure there is a reading time on the page and that it displays the correct information
+ let readingTimeElement = content.document.querySelector(
+ ".reader-estimated-time"
+ );
+ ok(readingTimeElement, "Reading time element should be in document");
+ const args = JSON.parse(readingTimeElement.dataset.l10nArgs);
+ is(args.rangePlural, "other", "Reading time should be '~3 minutes'");
+ ok(/\b3\b/.test(args.range), "Reading time should be '~3 minutes'");
+ });
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_refresh.js b/src/zen/tests/mochitests/readermode/browser_readerMode_refresh.js
new file mode 100644
index 000000000..41aa5d22c
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_refresh.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+async function testRefresh(url) {
+ // Open an article in a browser tab
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+
+ let readerButton = document.getElementById("reader-mode-button");
+ let refreshButton = document.getElementById("reload-button");
+
+ // Enter Reader Mode
+ readerButton.click();
+ await pageShownPromise;
+
+ // Wait for refreshing to be available.
+ await BrowserTestUtils.waitForMutationCondition(
+ refreshButton,
+ { attributes: true },
+ () => !refreshButton.disabled
+ );
+
+ // Refresh the page
+ pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ refreshButton.click();
+ await pageShownPromise;
+ await SpecialPowers.spawn(browser, [], () => {
+ ok(
+ !content.document.documentElement.hasAttribute("data-is-error"),
+ "The data-is-error attribute is present when Reader Mode failed to load an article."
+ );
+ });
+ });
+}
+
+add_task(async function () {
+ // Testing a non-text/plain document
+ await testRefresh(TEST_PATH + "readerModeArticle.html");
+
+ // Testing a test/plain document
+ await testRefresh(TEST_PATH + "readerModeArticleTextPlain.txt");
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_remoteType.js b/src/zen/tests/mochitests/readermode/browser_readerMode_remoteType.js
new file mode 100644
index 000000000..c2510667c
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_remoteType.js
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+const CROSS_SITE_TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.org"
+);
+
+/**
+ * Test that switching an article into readermode doesn't change its' remoteType.
+ * Test that the reader mode correctly calculates and displays the
+ * estimated reading time for a short article
+ */
+add_task(async function () {
+ info("opening readermode normally to ensure process doesn't change");
+ let articleRemoteType;
+ let aboutReaderURL;
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticleShort.html",
+ async function (browser) {
+ articleRemoteType = browser.remoteType;
+
+ // Click on the readermode button to switch into reader mode, and get the
+ // URL for that reader mode.
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ aboutReaderURL = browser.documentURI.spec;
+ ok(
+ aboutReaderURL.startsWith("about:reader"),
+ "about:reader should have been opened"
+ );
+ is(
+ browser.remoteType,
+ articleRemoteType,
+ "remote type should not have changed"
+ );
+ }
+ );
+
+ info(
+ "opening new tab directly with about reader URL into correct remote type"
+ );
+ await BrowserTestUtils.withNewTab(aboutReaderURL, async function (browser) {
+ is(
+ browser.remoteType,
+ articleRemoteType,
+ "Should have performed about:reader load in the correct remote type"
+ );
+ });
+
+ info("navigating process into correct remote type");
+ await BrowserTestUtils.withNewTab(
+ CROSS_SITE_TEST_PATH + "readerModeArticleShort.html",
+ async function (browser) {
+ if (SpecialPowers.useRemoteSubframes) {
+ isnot(
+ browser.remoteType,
+ articleRemoteType,
+ "Cross-site article should have different remote type with fission"
+ );
+ }
+
+ BrowserTestUtils.startLoadingURIString(browser, aboutReaderURL);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ is(
+ browser.remoteType,
+ articleRemoteType,
+ "Should have switched into the correct remote type"
+ );
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_samesite_cookie_redirect.js b/src/zen/tests/mochitests/readermode/browser_readerMode_samesite_cookie_redirect.js
new file mode 100644
index 000000000..22703b9a8
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_samesite_cookie_redirect.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ORIGIN = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+add_task(async function test_ss_cookie_redirect() {
+ // Set the samesite cookie
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_ORIGIN + "setSameSiteCookie.html"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ let server = new HttpServer();
+ server.start(-1);
+ server.registerPathHandler("/foo", (request, response) => {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", TEST_ORIGIN + "getCookies.sjs");
+ });
+ registerCleanupFunction(() => server.stop());
+ const { primaryPort, primaryHost } = server.identity;
+ const serverURL = `http://${primaryHost}:${primaryPort}/foo`;
+
+ // Now open `getCookies.sjs` but via a redirect:
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let loaded = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ "about:reader?url=" + encodeURIComponent(serverURL)
+ );
+ await loaded;
+ await SpecialPowers.spawn(browser, [], () => {
+ is(
+ content.document.getElementById("cookieSpan").textContent,
+ "",
+ "Shouldn't get cookies."
+ );
+ });
+ });
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_tabnavigation.js b/src/zen/tests/mochitests/readermode/browser_readerMode_tabnavigation.js
new file mode 100644
index 000000000..77b377060
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_tabnavigation.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+/**
+ * Test tab navigation through the main toolbar and to the site domain link.
+ */
+add_task(async function test_tabnavigation() {
+ // Open a browser tab, enter reader mode, and test if the reset button
+ // restores the page layout to the default pref values.
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+ await ContentTaskUtils.waitForCondition(() =>
+ doc.querySelector(".narrate-toggle:not([hidden])")
+ );
+ function ensureFocusOrder(list, shiftKey = false) {
+ let first = list.shift();
+ first.focus();
+ while (list.length) {
+ let next = list.shift();
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey }, content);
+ is(
+ doc.activeElement,
+ next,
+ `Focus should be on ${next.className}, was on ${doc.activeElement?.className}`
+ );
+ }
+ }
+ let expectedFocuses = Array.from(
+ doc.querySelectorAll(".toolbar-button")
+ ).filter(n => !n.matches("[hidden] *"));
+ expectedFocuses.push(doc.querySelector(".reader-domain"));
+
+ ensureFocusOrder(Array.from(expectedFocuses));
+
+ let reversed = Array.from(expectedFocuses);
+ reversed.reverse();
+ ensureFocusOrder(reversed, true);
+ });
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_textLayoutPref.js b/src/zen/tests/mochitests/readermode/browser_readerMode_textLayoutPref.js
new file mode 100644
index 000000000..bff7287b0
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_textLayoutPref.js
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const defaultValues = {
+ "font-family": "sans-serif",
+ "font-weight": "normal",
+ "content-width": "30em",
+ "line-height": "1.6em",
+ "letter-spacing": "0.00em",
+ "word-spacing": "0.00em",
+ "text-alignment": "start",
+};
+
+/**
+ * Test that the layout and advanced layout pref selection updates
+ * the document layout correctly.
+ */
+async function testTextLayout(aPref, value, cssProp, cssValue) {
+ // Set the pref to the custom value.
+ const valueType = typeof value;
+ if (valueType == "number") {
+ Services.prefs.setIntPref(`reader.${aPref}`, value);
+ } else if (valueType == "string") {
+ Services.prefs.setCharPref(`reader.${aPref}`, value);
+ }
+
+ // Open a browser tab, enter reader mode, and test if the page layout
+ // reflects the pref selection.
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ let customPref;
+ if (valueType == "number") {
+ customPref = Services.prefs.getIntPref(`reader.${aPref}`);
+ } else if (valueType == "string") {
+ customPref = Services.prefs.getCharPref(`reader.${aPref}`);
+ }
+ Assert.equal(customPref, value);
+
+ await SpecialPowers.spawn(
+ browser,
+ [cssValue, cssProp],
+ (expectedValue, prop) => {
+ let container = content.document.querySelector(".container");
+ let style = content.window.getComputedStyle(container);
+ let actualValue = style.getPropertyValue(`--${prop}`);
+ Assert.equal(actualValue, expectedValue);
+ }
+ );
+ }
+ );
+}
+
+/**
+ * Test that the reset button restores all layout options to defaults.
+ */
+async function testTextLayoutReset() {
+ // Set all prefs to non-default values.
+ Services.prefs.setIntPref(`reader.font_size`, 15);
+ Services.prefs.setCharPref(`reader.font_type`, "serif");
+ Services.prefs.setCharPref(`reader.font_weight`, "bold");
+ Services.prefs.setIntPref(`reader.content_width`, 6);
+ Services.prefs.setIntPref(`reader.line_height`, 6);
+ Services.prefs.setIntPref(`reader.character_spacing`, 3);
+ Services.prefs.setIntPref(`reader.word_spacing`, 3);
+ Services.prefs.setCharPref(`reader.text_alignment`, "left");
+
+ // Open a browser tab, enter reader mode, and test if the reset button
+ // restores the page layout to the default pref values.
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ await SpecialPowers.spawn(
+ browser,
+ [Object.keys(defaultValues), defaultValues],
+ (props, defaults) => {
+ let resetButton = content.document.querySelector(
+ ".text-layout-reset-button"
+ );
+ resetButton.click();
+ let container = content.document.querySelector(".container");
+ let style = content.window.getComputedStyle(container);
+
+ for (let prop of props) {
+ let resetValue = style.getPropertyValue(`--${prop}`);
+ Assert.equal(resetValue, defaults[prop]);
+ }
+
+ // Cannot test font size because the font size reset happens asynchronously,
+ // but we can check that font size buttons are re-enabled.
+ let plusButton = content.document.querySelector(
+ ".text-size-plus-button"
+ );
+ Assert.equal(plusButton.hasAttribute("disabled"), false);
+ }
+ );
+ }
+ );
+}
+
+/**
+ * Test that the focus stays within the text and layout menu.
+ */
+async function testTextLayoutFocus() {
+ // Open a browser tab, enter reader mode, and test if the focus stays
+ // within the menu.
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+
+ await SpecialPowers.spawn(browser, [], () => {
+ let doc = content.document;
+ doc.querySelector(".text-layout-button").click();
+
+ let firstFocusableElement = doc.querySelector(
+ ".text-size-minus-button"
+ );
+ let advancedHeader = doc.querySelector(".accordion-header");
+ advancedHeader.focus();
+
+ EventUtils.synthesizeKey("KEY_Tab", {}, content);
+ is(
+ doc.activeElement,
+ firstFocusableElement,
+ "Focus moves back to the first focusable button"
+ );
+
+ firstFocusableElement.focus();
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, content);
+ is(
+ doc.activeElement,
+ advancedHeader,
+ "Focus moves to last focusable button"
+ );
+
+ // Expand the advanced layout accordion.
+ advancedHeader.click();
+ let resetButton = doc.querySelector(".text-layout-reset-button");
+ resetButton.focus();
+
+ EventUtils.synthesizeKey("KEY_Tab", {}, content);
+ is(
+ doc.activeElement,
+ firstFocusableElement,
+ "Focus moves back to the first focusable button"
+ );
+
+ firstFocusableElement.focus();
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, content);
+ is(
+ doc.activeElement,
+ resetButton,
+ "Focus moves from first focusable button to last focusable button"
+ );
+ });
+ }
+ );
+}
+
+add_task(async function () {
+ await testTextLayout("font_size", 7, "font-size", "24px");
+ await testTextLayout("font_type", "monospace", "font-family", "monospace");
+ await testTextLayout("font_weight", "bold", "font-weight", "bolder");
+ await testTextLayout("content_width", 7, "content-width", "50em");
+ await testTextLayout("line_height", 7, "line-height", "2.2em");
+ await testTextLayout("character_spacing", 7, "letter-spacing", "0.18em");
+ await testTextLayout("word_spacing", 7, "word-spacing", "0.30em");
+ await testTextLayout("text_alignment", "right", "text-alignment", "right");
+ await testTextLayoutReset();
+ await testTextLayoutFocus();
+});
diff --git a/src/zen/tests/mochitests/readermode/browser_readerMode_with_anchor.js b/src/zen/tests/mochitests/readermode/browser_readerMode_with_anchor.js
new file mode 100644
index 000000000..229daaed9
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/browser_readerMode_with_anchor.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+add_task(async function test_loading_withHash() {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html#foo",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await pageShownPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let foo = content.document.getElementById("foo");
+ ok(foo, "foo element should be in document");
+ let { scrollTop } = content.document.documentElement;
+ if (scrollTop == 0) {
+ await ContentTaskUtils.waitForEvent(content.document, "scroll");
+ ({ scrollTop } = content.document.documentElement);
+ }
+ let { offsetTop } = foo;
+ Assert.lessOrEqual(
+ Math.abs(scrollTop - offsetTop),
+ 1,
+ `scrollTop (${scrollTop}) should be within 1 CSS pixel of offsetTop (${offsetTop})`
+ );
+ });
+ }
+ );
+});
+
+add_task(async function test_loading_withoutHash() {
+ await BrowserTestUtils.withNewTab(
+ TEST_PATH + "readerModeArticle.html",
+ async function (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "AboutReaderContentReady"
+ );
+ let pageLoadedPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "load",
+ true
+ );
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ await Promise.all([pageShownPromise, pageLoadedPromise]);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ Assert.equal(
+ content.document.documentElement.scrollTop,
+ 0,
+ "scrollTop should be 0"
+ );
+ });
+ let scrollEventPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "scroll",
+ true
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#foo-anchor",
+ {},
+ browser
+ );
+ await scrollEventPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let foo = content.document.getElementById("foo");
+ ok(foo, "foo element should be in document");
+ let { scrollTop } = content.document.documentElement;
+ let { offsetTop } = foo;
+ Assert.lessOrEqual(
+ Math.abs(scrollTop - offsetTop),
+ 1,
+ `scrollTop (${scrollTop}) should be within 1 CSS pixel of offsetTop (${offsetTop})`
+ );
+ });
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/readermode/getCookies.sjs b/src/zen/tests/mochitests/readermode/getCookies.sjs
new file mode 100644
index 000000000..02e29fd87
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/getCookies.sjs
@@ -0,0 +1,16 @@
+function handleRequest(request, response) {
+ const cookies = request.hasHeader("Cookie")
+ ? request.getHeader("Cookie")
+ : "";
+ response.write(`
+
+
+
+
+
+
+ Cookie: ${cookies}
+
+
+ `);
+}
diff --git a/src/zen/tests/mochitests/readermode/head.js b/src/zen/tests/mochitests/readermode/head.js
new file mode 100644
index 000000000..859c6794d
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/head.js
@@ -0,0 +1,10 @@
+/* exported is_element_visible, is_element_hidden */
+
+function is_element_visible(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(BrowserTestUtils.isVisible(element), msg || "Element should be visible");
+}
+function is_element_hidden(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(BrowserTestUtils.isHidden(element), msg || "Element should be hidden");
+}
diff --git a/src/zen/tests/mochitests/readermode/linkToGetCookies.html b/src/zen/tests/mochitests/readermode/linkToGetCookies.html
new file mode 100644
index 000000000..a05d32af4
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/linkToGetCookies.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.
+
+ Cross-origin link to getCookies.html
+
+
+
diff --git a/src/zen/tests/mochitests/readermode/readerModeArticle.html b/src/zen/tests/mochitests/readermode/readerModeArticle.html
new file mode 100644
index 000000000..a0f1c64da
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/readerModeArticle.html
@@ -0,0 +1,28 @@
+
+
+
+Article title
+
+
+
+
+
+
Article title
+
+
by Jane Doe
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
by John Doe
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
+
+
diff --git a/src/zen/tests/mochitests/readermode/readerModeArticleContainsLink.html b/src/zen/tests/mochitests/readermode/readerModeArticleContainsLink.html
new file mode 100644
index 000000000..be2d7d646
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/readerModeArticleContainsLink.html
@@ -0,0 +1,20 @@
+
+
+
+Article title
+
+
+
+
+
+
Article title
+
by Jane Doe
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Link to another page.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
+
+
diff --git a/src/zen/tests/mochitests/readermode/readerModeArticleHiddenNodes.html b/src/zen/tests/mochitests/readermode/readerModeArticleHiddenNodes.html
new file mode 100644
index 000000000..92441b797
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/readerModeArticleHiddenNodes.html
@@ -0,0 +1,22 @@
+
+
+
+Article title
+
+
+
+
+
+
+
Article title
+
by Jane Doe
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.
+
+
+
diff --git a/src/zen/tests/mochitests/readermode/readerModeArticleMedium.html b/src/zen/tests/mochitests/readermode/readerModeArticleMedium.html
new file mode 100644
index 000000000..70b172cf6
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/readerModeArticleMedium.html
@@ -0,0 +1,16 @@
+
+
+
+Article title
+
+
+
+
+
+
Article title
+
by Jane Doe
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu
+
+
diff --git a/src/zen/tests/mochitests/readermode/readerModeArticleShort.html b/src/zen/tests/mochitests/readermode/readerModeArticleShort.html
new file mode 100644
index 000000000..692471f27
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/readerModeArticleShort.html
@@ -0,0 +1,14 @@
+
+
+
+
Article title
+
+
+
+
+
+
Article title
+
by Jane Doe
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu
+
+
diff --git a/src/zen/tests/mochitests/readermode/readerModeArticleTextPlain.txt b/src/zen/tests/mochitests/readermode/readerModeArticleTextPlain.txt
new file mode 100644
index 000000000..c5b7861b7
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/readerModeArticleTextPlain.txt
@@ -0,0 +1,10 @@
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tortor id aliquet lectus proin nibh nisl condimentum. Eget magna fermentum iaculis eu non diam phasellus. Sed viverra tellus in hac habitasse platea dictumst. Quis commodo odio aenean sed. Diam vulputate ut pharetra sit amet aliquam id diam. Felis imperdiet proin fermentum leo vel orci. Diam vel quam elementum pulvinar. Vestibulum lectus mauris ultrices eros in cursus turpis massa. Sagittis vitae et leo duis ut diam. Quam elementum pulvinar etiam non quam lacus suspendisse faucibus interdum. At augue eget arcu dictum varius duis at consectetur. Bibendum enim facilisis gravida neque convallis a cras semper auctor. Suspendisse interdum consectetur libero id faucibus. Neque ornare aenean euismod elementum nisi.
+
+Lacus sed turpis tincidunt id aliquet. Euismod nisi porta lorem mollis. Sollicitudin aliquam ultrices sagittis orci. A diam sollicitudin tempor id eu nisl nunc. Molestie a iaculis at erat pellentesque adipiscing commodo elit. Tellus mauris a diam maecenas. Dolor morbi non arcu risus quis. Dictum non consectetur a erat nam at lectus. Convallis posuere morbi leo urna molestie. Blandit turpis cursus in hac habitasse platea dictumst quisque sagittis. Sed ullamcorper morbi tincidunt ornare massa eget egestas. Sit amet risus nullam eget felis eget nunc. Turpis in eu mi bibendum neque egestas congue. Accumsan in nisl nisi scelerisque eu ultrices vitae. Vel quam elementum pulvinar etiam non quam lacus.
+
+Erat velit scelerisque in dictum non consectetur a. Vulputate sapien nec sagittis aliquam malesuada bibendum. Odio facilisis mauris sit amet massa vitae tortor condimentum lacinia. Tempor nec feugiat nisl pretium. At urna condimentum mattis pellentesque id nibh tortor. Viverra tellus in hac habitasse platea dictumst. Turpis massa tincidunt dui ut ornare. Nunc id cursus metus aliquam eleifend mi. Etiam dignissim diam quis enim lobortis scelerisque fermentum. Aenean sed adipiscing diam donec adipiscing tristique risus nec feugiat. Vitae aliquet nec ullamcorper sit amet risus nullam eget felis. Quis hendrerit dolor magna eget est lorem ipsum dolor. Ultrices vitae auctor eu augue ut lectus. Curabitur gravida arcu ac tortor dignissim convallis. Justo laoreet sit amet cursus sit. Lorem ipsum dolor sit amet. Sed sed risus pretium quam vulputate dignissim suspendisse in.
+
+Egestas erat imperdiet sed euismod nisi porta lorem mollis. Pharetra magna ac placerat vestibulum lectus mauris ultrices eros in. Est ante in nibh mauris cursus mattis. Habitasse platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Nunc aliquet bibendum enim facilisis gravida neque. Massa sapien faucibus et molestie. Sapien eget mi proin sed libero enim sed faucibus. Mauris a diam maecenas sed enim ut sem. Consectetur adipiscing elit duis tristique sollicitudin nibh sit. Sed arcu non odio euismod lacinia at.
+
+Ultricies mi quis hendrerit dolor. A erat nam at lectus urna duis convallis convallis tellus. Est sit amet facilisis magna etiam tempor orci. Porttitor massa id neque aliquam vestibulum. Lobortis feugiat vivamus at augue eget arcu dictum varius duis. Diam sit amet nisl suscipit adipiscing. Leo in vitae turpis massa. Netus et malesuada fames ac. Ac turpis egestas sed tempus urna et pharetra. Ut eu sem integer vitae justo. At erat pellentesque adipiscing commodo elit at. Consectetur purus ut faucibus pulvinar elementum integer enim. Cursus eget nunc scelerisque viverra mauris in aliquam sem. Aenean et tortor at risus viverra adipiscing at in. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras. Tincidunt id aliquet risus feugiat in ante. Amet consectetur adipiscing elit pellentesque. Dignissim enim sit amet venenatis urna cursus eget nunc. Sit amet porttitor eget dolor morbi non.
diff --git a/src/zen/tests/mochitests/readermode/readerModeNonArticle.html b/src/zen/tests/mochitests/readermode/readerModeNonArticle.html
new file mode 100644
index 000000000..e216af3c1
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/readerModeNonArticle.html
@@ -0,0 +1,9 @@
+
+
+
+
Non article title
+
+
+
+
+
diff --git a/src/zen/tests/mochitests/readermode/readerModeRandom.sjs b/src/zen/tests/mochitests/readermode/readerModeRandom.sjs
new file mode 100644
index 000000000..f6bb15c06
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/readerModeRandom.sjs
@@ -0,0 +1,23 @@
+// Generate a article ending in a piece of text with some random values in it.
+
+let readerModeText = `
+
+
+
Article title
+
+
+
+
+
+
Article title
+
by Jane Doe
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu
+`;
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ aResponse.setHeader("Content-Type", "text/html", false);
+ aResponse.write(
+ readerModeText + "
" + Date.now() + "," + Math.random() + "
"
+ );
+}
diff --git a/src/zen/tests/mochitests/readermode/setSameSiteCookie.html b/src/zen/tests/mochitests/readermode/setSameSiteCookie.html
new file mode 100644
index 000000000..67bb71492
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/setSameSiteCookie.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
This page just set a cookie with the SameSite attribute.
+
+
diff --git a/src/zen/tests/mochitests/readermode/setSameSiteCookie.html^headers^ b/src/zen/tests/mochitests/readermode/setSameSiteCookie.html^headers^
new file mode 100644
index 000000000..c0229c93b
--- /dev/null
+++ b/src/zen/tests/mochitests/readermode/setSameSiteCookie.html^headers^
@@ -0,0 +1 @@
+Set-Cookie: foo=bar; Path='/' ; SameSite=strict
diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build
index c63e29a55..f0e09031a 100644
--- a/src/zen/tests/moz.build
+++ b/src/zen/tests/moz.build
@@ -11,6 +11,7 @@ BROWSER_CHROME_MANIFESTS += [
"media/browser.toml",
"pinned/browser.toml",
"popover/browser.toml",
+ "site_control/browser.toml",
"spaces/browser.toml",
"split_view/browser.toml",
"tabs/browser.toml",
diff --git a/src/zen/tests/site_control/browser.toml b/src/zen/tests/site_control/browser.toml
new file mode 100644
index 000000000..e555b254e
--- /dev/null
+++ b/src/zen/tests/site_control/browser.toml
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files = [
+ "head.js",
+]
+
+["browser_site_control_header_buttons.js"]
+
+["browser_site_control_manage_addons.js"]
+
+["browser_site_control_shows_installed_addons.js"]
+
+["browser_site_data_panel_opens.js"]
+
+["browser_site_data_permission_toggle.js"]
+
+["browser_site_data_security_scheme.js"]
diff --git a/src/zen/tests/site_control/browser_site_control_header_buttons.js b/src/zen/tests/site_control/browser_site_control_header_buttons.js
new file mode 100644
index 000000000..be61b14f6
--- /dev/null
+++ b/src/zen/tests/site_control/browser_site_control_header_buttons.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function shareButton() {
+ return document.getElementById("zen-site-data-header-share");
+}
+
+function bookmarkButton() {
+ return document.getElementById("zen-site-data-header-bookmark");
+}
+
+function readerButton() {
+ return document.getElementById("zen-site-data-header-reader-mode");
+}
+
+add_task(async function test_share_button_reflects_scheme() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ try {
+ await openSiteDataPanel();
+ ok(
+ !shareButton().hasAttribute("disabled"),
+ "share button is enabled on https pages"
+ );
+ await closeSiteDataPanel();
+
+ await loadUrl("about:blank");
+
+ await openSiteDataPanel();
+ ok(
+ shareButton().hasAttribute("disabled"),
+ "share button is disabled on about:blank"
+ );
+ await closeSiteDataPanel();
+ } finally {
+ await closeSiteDataPanel();
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function test_bookmark_button_reflects_starred_state() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ let bookmark;
+ try {
+ await openSiteDataPanel();
+ ok(
+ !bookmarkButton().classList.contains("active"),
+ "bookmark button is not active on an un-bookmarked page"
+ );
+ await closeSiteDataPanel();
+
+ bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: HTTPS_PAGE,
+ title: "test-bookmark",
+ });
+ await BrowserTestUtils.waitForCondition(
+ () => BookmarkingUI.star?.hasAttribute("starred"),
+ "BookmarkingUI picks up the new bookmark"
+ );
+
+ await openSiteDataPanel();
+ ok(
+ bookmarkButton().classList.contains("active"),
+ "bookmark button becomes active once the page is bookmarked"
+ );
+ await closeSiteDataPanel();
+ } finally {
+ if (bookmark) {
+ await PlacesUtils.bookmarks.remove(bookmark);
+ }
+ await closeSiteDataPanel();
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function test_reader_mode_button_disabled_when_unavailable() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ try {
+ await openSiteDataPanel();
+ const urlbarReader = document.getElementById("reader-mode-button");
+ const expectedDisabled =
+ urlbarReader?.hidden && !urlbarReader?.hasAttribute("readeractive");
+ Assert.equal(
+ readerButton().disabled,
+ !!expectedDisabled,
+ "reader-mode header button mirrors the urlbar reader button availability"
+ );
+ await closeSiteDataPanel();
+ } finally {
+ await closeSiteDataPanel();
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/src/zen/tests/site_control/browser_site_control_manage_addons.js b/src/zen/tests/site_control/browser_site_control_manage_addons.js
new file mode 100644
index 000000000..45559b413
--- /dev/null
+++ b/src/zen/tests/site_control/browser_site_control_manage_addons.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_manage_addons_opens_addons_manager() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ let addonsTab;
+ try {
+ await openSiteDataPanel();
+
+ const manageButton = document.getElementById("zen-site-data-manage-addons");
+ ok(manageButton, "manage-addons link exists in the panel");
+
+ const newTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => url.startsWith("about:addons"),
+ true
+ );
+
+ manageButton.click();
+
+ addonsTab = await newTabOpened;
+ Assert.ok(
+ addonsTab.linkedBrowser.currentURI.spec.startsWith("about:addons"),
+ "about:addons opened in a new tab"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => siteDataPanel().state === "closed",
+ "panel auto-closes after opening the addons manager"
+ );
+ } finally {
+ await closeSiteDataPanel();
+ if (addonsTab) {
+ BrowserTestUtils.removeTab(addonsTab);
+ }
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function test_get_addons_button_opens_amo() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ const amoUrl = Services.urlFormatter.formatURLPref(
+ "extensions.getAddons.link.url"
+ );
+
+ let amoTab;
+ try {
+ await openSiteDataPanel();
+
+ const getAddonsButton = document.getElementById(
+ "zen-site-data-new-addon-button"
+ );
+ ok(getAddonsButton, "get-addons button exists in the panel");
+
+ const newTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => url === amoUrl,
+ true
+ );
+
+ getAddonsButton.dispatchEvent(new Event("command", { bubbles: true }));
+
+ amoTab = await newTabOpened;
+ Assert.equal(
+ amoTab.linkedBrowser.currentURI.spec,
+ amoUrl,
+ "AMO URL opened in a new tab"
+ );
+ } finally {
+ await closeSiteDataPanel();
+ if (amoTab) {
+ BrowserTestUtils.removeTab(amoTab);
+ }
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/src/zen/tests/site_control/browser_site_control_shows_installed_addons.js b/src/zen/tests/site_control/browser_site_control_shows_installed_addons.js
new file mode 100644
index 000000000..52ff61097
--- /dev/null
+++ b/src/zen/tests/site_control/browser_site_control_shows_installed_addons.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function loadTestExtension(id, name) {
+ const ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ name,
+ },
+ useAddonManager: "temporary",
+ });
+ await ext.startup();
+ return ext;
+}
+
+function addonItemsInPanel() {
+ return siteDataPanel().querySelectorAll(
+ ".unified-extensions-item[data-extensionid]"
+ );
+}
+
+add_task(async function test_panel_lists_installed_addons() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ const extA = await loadTestExtension("ext-a@zen-test", "Zen Test Addon A");
+ const extB = await loadTestExtension("ext-b@zen-test", "Zen Test Addon B");
+
+ try {
+ await openSiteDataPanel();
+
+ await BrowserTestUtils.waitForCondition(() => {
+ const ids = Array.from(addonItemsInPanel()).map(el =>
+ el.getAttribute("data-extensionid")
+ );
+ return ids.includes(extA.id) && ids.includes(extB.id);
+ }, "both installed addons appear in the panel");
+
+ const renderedIds = Array.from(addonItemsInPanel()).map(el =>
+ el.getAttribute("data-extensionid")
+ );
+ Assert.ok(
+ renderedIds.includes(extA.id),
+ `addon A (${extA.id}) is rendered`
+ );
+ Assert.ok(
+ renderedIds.includes(extB.id),
+ `addon B (${extB.id}) is rendered`
+ );
+
+ const itemA = siteDataPanel().querySelector(
+ `.unified-extensions-item[data-extensionid="${extA.id}"]`
+ );
+ Assert.equal(
+ itemA.querySelector(".unified-extensions-item-name").textContent,
+ "Zen Test Addon A",
+ "addon A is rendered with its declared name"
+ );
+
+ await closeSiteDataPanel();
+ } finally {
+ await closeSiteDataPanel();
+ await extA.unload();
+ await extB.unload();
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function test_panel_drops_uninstalled_addon() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ const ext = await loadTestExtension(
+ "ext-transient@zen-test",
+ "Zen Transient Addon"
+ );
+
+ try {
+ await openSiteDataPanel();
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ siteDataPanel().querySelector(
+ `.unified-extensions-item[data-extensionid="${ext.id}"]`
+ ),
+ "addon initially present in the panel"
+ );
+ await closeSiteDataPanel();
+
+ await ext.unload();
+
+ await openSiteDataPanel();
+ Assert.equal(
+ siteDataPanel().querySelector(
+ `.unified-extensions-item[data-extensionid="${ext.id}"]`
+ ),
+ null,
+ "uninstalled addon no longer appears in the panel"
+ );
+ await closeSiteDataPanel();
+ } finally {
+ await closeSiteDataPanel();
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/src/zen/tests/site_control/browser_site_data_panel_opens.js b/src/zen/tests/site_control/browser_site_data_panel_opens.js
new file mode 100644
index 000000000..9d4502753
--- /dev/null
+++ b/src/zen/tests/site_control/browser_site_data_panel_opens.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_clicking_icon_opens_and_prepares_panel() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ try {
+ const panel = await openSiteDataPanel();
+ Assert.equal(panel.state, "open", "panel is in the open state");
+
+ // #preparePanel ran — the security-info button got its identity attr
+ // set (it has no default value in markup).
+ const securityButton = document.getElementById(
+ "zen-site-data-security-info"
+ );
+ ok(
+ securityButton.hasAttribute("identity"),
+ "panel popupshowing populated the security-info identity attribute"
+ );
+
+ await closeSiteDataPanel();
+ Assert.equal(panel.state, "closed", "panel closed cleanly");
+ } finally {
+ await closeSiteDataPanel();
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/src/zen/tests/site_control/browser_site_data_permission_toggle.js b/src/zen/tests/site_control/browser_site_data_permission_toggle.js
new file mode 100644
index 000000000..228a5129d
--- /dev/null
+++ b/src/zen/tests/site_control/browser_site_data_permission_toggle.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SitePermissions } = ChromeUtils.importESModule(
+ "resource:///modules/SitePermissions.sys.mjs"
+);
+
+add_task(async function test_toggling_permission_flips_ui_and_store() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ await new Promise(r => setTimeout(r, 500)); // ensure the tab's browser is fully ready
+
+ const principal = gBrowser.contentPrincipal;
+ SitePermissions.setForPrincipal(principal, "geo", SitePermissions.ALLOW);
+
+ try {
+ await openSiteDataPanel();
+
+ const geoItem = document.querySelector(
+ ".permission-popup-permission-item-geo"
+ );
+ ok(geoItem, "geo permission row rendered in the panel");
+ Assert.equal(
+ geoItem.getAttribute("state"),
+ "allow",
+ "row starts in the allow state"
+ );
+
+ // The click handler lives on the container; synthesize a click on the
+ // label (child) which bubbles to it.
+ const label = geoItem.querySelector(
+ ".permission-popup-permission-label-container"
+ );
+ EventUtils.synthesizeMouseAtCenter(label, {});
+
+ await BrowserTestUtils.waitForCondition(
+ () => geoItem.getAttribute("state") === "block",
+ "row flips to the block state after the click"
+ );
+
+ // The backing store must agree.
+ const stored = SitePermissions.getForPrincipal(principal, "geo");
+ Assert.equal(
+ stored.state,
+ SitePermissions.BLOCK,
+ "SitePermissions records BLOCK for the principal after the toggle"
+ );
+
+ await closeSiteDataPanel();
+ } finally {
+ SitePermissions.removeFromPrincipal(principal, "geo");
+ await closeSiteDataPanel();
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/src/zen/tests/site_control/browser_site_data_security_scheme.js b/src/zen/tests/site_control/browser_site_data_security_scheme.js
new file mode 100644
index 000000000..f74c2ef7b
--- /dev/null
+++ b/src/zen/tests/site_control/browser_site_data_security_scheme.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function securityIdentity() {
+ return document
+ .getElementById("zen-site-data-security-info")
+ .getAttribute("identity");
+}
+
+add_task(async function test_security_identity_by_scheme() {
+ const tab = BrowserTestUtils.addTab(gBrowser, HTTPS_PAGE, {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ try {
+ await openSiteDataPanel();
+ Assert.equal(
+ securityIdentity(),
+ "secure",
+ "https page shows identity='secure'"
+ );
+ await closeSiteDataPanel();
+
+ await loadUrl(HTTP_PAGE);
+
+ await openSiteDataPanel();
+ Assert.equal(
+ securityIdentity(),
+ "not-secure",
+ "http page shows identity='not-secure'"
+ );
+ await closeSiteDataPanel();
+ } finally {
+ await closeSiteDataPanel();
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/src/zen/tests/site_control/head.js b/src/zen/tests/site_control/head.js
new file mode 100644
index 000000000..cea56cf75
--- /dev/null
+++ b/src/zen/tests/site_control/head.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const HTTP_PAGE = "https://mochi.test:8888/";
+const HTTPS_PAGE = "https://example.com/";
+
+async function loadUrl(url) {
+ const browser = gBrowser.selectedBrowser;
+ const loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await loaded;
+}
+
+function siteDataIcon() {
+ return document.getElementById("zen-site-data-icon-button");
+}
+
+function siteDataPanel() {
+ return document.getElementById("zen-unified-site-data-panel");
+}
+
+async function openSiteDataPanel() {
+ // Make sure its closed
+ await closeSiteDataPanel();
+ await new Promise(r => setTimeout(r, 500));
+ gNavToolbox.setAttribute("zen-has-implicit-hover", "true");
+ await new Promise(r => setTimeout(r, 500));
+ const panel = siteDataPanel();
+ const shown = BrowserTestUtils.waitForPopupEvent(panel, "shown");
+ EventUtils.synthesizeMouseAtCenter(siteDataIcon(), {});
+ await shown;
+ return panel;
+}
+
+async function closeSiteDataPanel() {
+ const panel = siteDataPanel();
+ if (panel.state === "closed") {
+ return;
+ }
+ const hidden = BrowserTestUtils.waitForPopupEvent(panel, "hidden");
+ panel.hidePopup(true);
+ await hidden;
+}