summaryrefslogtreecommitdiffstats
path: root/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/firefoxview/tests/browser/browser_history_firefoxview.js')
-rw-r--r--browser/components/firefoxview/tests/browser/browser_history_firefoxview.js544
1 files changed, 544 insertions, 0 deletions
diff --git a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
new file mode 100644
index 0000000000..a6c697e398
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
@@ -0,0 +1,544 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(globalThis, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+});
+const { ProfileAge } = ChromeUtils.importESModule(
+ "resource://gre/modules/ProfileAge.sys.mjs"
+);
+
+const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history";
+const IMPORT_HISTORY_DISMISSED_PREF =
+ "browser.tabs.firefox-view.importHistory.dismissed";
+const HISTORY_EVENT = [["firefoxview_next", "history", "visits", undefined]];
+const SHOW_ALL_HISTORY_EVENT = [
+ ["firefoxview_next", "show_all_history", "tabs", undefined],
+];
+
+const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
+const DAY_MS = 24 * 60 * 60 * 1000;
+const today = new Date();
+const yesterday = new Date(Date.now() - DAY_MS);
+const twoDaysAgo = new Date(Date.now() - DAY_MS * 2);
+const threeDaysAgo = new Date(Date.now() - DAY_MS * 3);
+const fourDaysAgo = new Date(Date.now() - DAY_MS * 4);
+const oneMonthAgo = new Date(today);
+
+// Set the date for the first day of the last month
+oneMonthAgo.setDate(1);
+if (oneMonthAgo.getMonth() === 0) {
+ // If today's date is in January, use first day in December from the previous year
+ oneMonthAgo.setMonth(11);
+ oneMonthAgo.setFullYear(oneMonthAgo.getFullYear() - 1);
+} else {
+ oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
+}
+
+function isElInViewport(element) {
+ const boundingRect = element.getBoundingClientRect();
+ return (
+ boundingRect.top >= 0 &&
+ boundingRect.left >= 0 &&
+ boundingRect.bottom <=
+ (window.innerHeight || document.documentElement.clientHeight) &&
+ boundingRect.right <=
+ (window.innerWidth || document.documentElement.clientWidth)
+ );
+}
+
+async function historyComponentReady(historyComponent) {
+ await TestUtils.waitForCondition(
+ () =>
+ [...historyComponent.allHistoryItems.values()].reduce(
+ (acc, { length }) => acc + length,
+ 0
+ ) === 24
+ );
+
+ let expected = historyComponent.historyMapByDate.length;
+ let actual = historyComponent.cards.length;
+
+ is(expected, actual, `Total number of cards should be ${expected}`);
+}
+
+async function historyTelemetry() {
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 1;
+ },
+ "Waiting for history firefoxview telemetry event.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ HISTORY_EVENT,
+ { category: "firefoxview_next" },
+ { clear: true, process: "parent" }
+ );
+}
+
+async function sortHistoryTelemetry(sortHistoryEvent) {
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 1;
+ },
+ "Waiting for sort_history firefoxview telemetry event.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ sortHistoryEvent,
+ { category: "firefoxview_next" },
+ { clear: true, process: "parent" }
+ );
+}
+
+async function showAllHistoryTelemetry() {
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 1;
+ },
+ "Waiting for show_all_history firefoxview telemetry event.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ SHOW_ALL_HISTORY_EVENT,
+ { category: "firefoxview_next" },
+ { clear: true, process: "parent" }
+ );
+}
+
+async function addHistoryItems(dateAdded) {
+ await PlacesUtils.history.insert({
+ url: URLs[0],
+ title: "Example Domain 1",
+ visits: [{ date: dateAdded }],
+ });
+ await PlacesUtils.history.insert({
+ url: URLs[1],
+ title: "Example Domain 2",
+ visits: [{ date: dateAdded }],
+ });
+ await PlacesUtils.history.insert({
+ url: URLs[2],
+ title: "Example Domain 3",
+ visits: [{ date: dateAdded }],
+ });
+ await PlacesUtils.history.insert({
+ url: URLs[3],
+ title: "Example Domain 4",
+ visits: [{ date: dateAdded }],
+ });
+}
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.firefox-view.search.enabled", true]],
+ });
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_list_ordering() {
+ await PlacesUtils.history.clear();
+ await addHistoryItems(today);
+ await addHistoryItems(yesterday);
+ await addHistoryItems(twoDaysAgo);
+ await addHistoryItems(threeDaysAgo);
+ await addHistoryItems(fourDaysAgo);
+ await addHistoryItems(oneMonthAgo);
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ await navigateToCategoryAndWait(document, "history");
+
+ let historyComponent = document.querySelector("view-history");
+ historyComponent.profileAge = 8;
+
+ await historyComponentReady(historyComponent);
+
+ let firstCard = historyComponent.cards[0];
+
+ info("The first card should have a header for 'Today'.");
+ await BrowserTestUtils.waitForMutationCondition(
+ firstCard.querySelector("[slot=header]"),
+ { attributes: true },
+ () =>
+ document.l10n.getAttributes(firstCard.querySelector("[slot=header]"))
+ .id === "firefoxview-history-date-today"
+ );
+
+ // Select first history item in first card
+ await clearAllParentTelemetryEvents();
+ await TestUtils.waitForCondition(() => {
+ return historyComponent.lists[0].rowEls.length;
+ });
+ let firstHistoryLink = historyComponent.lists[0].rowEls[0].mainEl;
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ document,
+ "visibilitychange"
+ );
+ await EventUtils.synthesizeMouseAtCenter(firstHistoryLink, {}, content);
+ await historyTelemetry();
+ await promiseHidden;
+ await openFirefoxViewTab(browser.ownerGlobal);
+
+ // Test number of cards when sorted by site/domain
+ await clearAllParentTelemetryEvents();
+ let sortHistoryEvent = [
+ [
+ "firefoxview_next",
+ "sort_history",
+ "tabs",
+ undefined,
+ { sort_type: "site", search_start: "false" },
+ ],
+ ];
+ // Select sort by site option
+ await EventUtils.synthesizeMouseAtCenter(
+ historyComponent.sortInputs[1],
+ {},
+ content
+ );
+ await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
+ await sortHistoryTelemetry(sortHistoryEvent);
+
+ let expectedNumOfCards = historyComponent.historyMapBySite.length;
+
+ info(`Total number of cards should be ${expectedNumOfCards}`);
+ await BrowserTestUtils.waitForMutationCondition(
+ historyComponent.shadowRoot,
+ { childList: true, subtree: true },
+ () => expectedNumOfCards === historyComponent.cards.length
+ );
+
+ await clearAllParentTelemetryEvents();
+ sortHistoryEvent = [
+ [
+ "firefoxview_next",
+ "sort_history",
+ "tabs",
+ undefined,
+ { sort_type: "date", search_start: "false" },
+ ],
+ ];
+ // Select sort by date option
+ await EventUtils.synthesizeMouseAtCenter(
+ historyComponent.sortInputs[0],
+ {},
+ content
+ );
+ await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
+ await sortHistoryTelemetry(sortHistoryEvent);
+
+ // clean up extra tabs
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
+ }
+ });
+});
+
+add_task(async function test_empty_states() {
+ await PlacesUtils.history.clear();
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ await navigateToCategoryAndWait(document, "history");
+
+ let historyComponent = document.querySelector("view-history");
+ historyComponent.profileAge = 8;
+ await TestUtils.waitForCondition(() => historyComponent.emptyState);
+ let emptyStateCard = historyComponent.emptyState;
+ ok(
+ emptyStateCard.headerEl.textContent.includes(
+ "Get back to where you’ve been"
+ ),
+ "Initial empty state header has the expected text."
+ );
+ ok(
+ emptyStateCard.descriptionEls[0].textContent.includes(
+ "As you browse, the pages you visit will be listed here."
+ ),
+ "Initial empty state description has the expected text."
+ );
+
+ // Test empty state when History mode is set to never remember
+ Services.prefs.setBoolPref(NEVER_REMEMBER_HISTORY_PREF, true);
+ // Manually update the history component from the test, since changing this setting
+ // in about:preferences will require a browser reload
+ historyComponent.requestUpdate();
+ await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
+ emptyStateCard = historyComponent.emptyState;
+ ok(
+ emptyStateCard.headerEl.textContent.includes("Nothing to show"),
+ "Empty state with never remember history header has the expected text."
+ );
+ ok(
+ emptyStateCard.descriptionEls[1].textContent.includes(
+ "remember your activity as you browse. To change that"
+ ),
+ "Empty state with never remember history description has the expected text."
+ );
+ // Reset History mode to Remember
+ Services.prefs.setBoolPref(NEVER_REMEMBER_HISTORY_PREF, false);
+ // Manually update the history component from the test, since changing this setting
+ // in about:preferences will require a browser reload
+ historyComponent.requestUpdate();
+ await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
+
+ // Test import history banner shows if profile age is 7 days or less and
+ // user hasn't already imported history from another browser
+ Services.prefs.setBoolPref(IMPORT_HISTORY_DISMISSED_PREF, false);
+ Services.prefs.setBoolPref(HAS_IMPORTED_HISTORY_PREF, true);
+ ok(!historyComponent.cards.length, "Import history banner not shown yet");
+ historyComponent.profileAge = 0;
+ await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
+ ok(
+ !historyComponent.cards.length,
+ "Import history banner still not shown yet"
+ );
+ Services.prefs.setBoolPref(HAS_IMPORTED_HISTORY_PREF, false);
+ await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
+ ok(
+ historyComponent.cards[0].textContent.includes(
+ "Import history from another browser"
+ ),
+ "Import history banner is shown"
+ );
+ let importHistoryCloseButton =
+ historyComponent.cards[0].querySelector("button.close");
+ importHistoryCloseButton.click();
+ await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
+ ok(
+ Services.prefs.getBoolPref(IMPORT_HISTORY_DISMISSED_PREF, true) &&
+ !historyComponent.cards.length,
+ "Import history banner has been dismissed."
+ );
+ // Reset profileAge to greater than 7 to avoid affecting other tests
+ historyComponent.profileAge = 8;
+ Services.prefs.setBoolPref(IMPORT_HISTORY_DISMISSED_PREF, false);
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function test_observers_removed_when_view_is_hidden() {
+ await PlacesUtils.history.clear();
+ const NEW_TAB_URL = "https://example.com";
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ NEW_TAB_URL
+ );
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToCategoryAndWait(document, "history");
+ const historyComponent = document.querySelector("view-history");
+ historyComponent.profileAge = 8;
+ let visitList = await TestUtils.waitForCondition(() =>
+ historyComponent.cards?.[0]?.querySelector("fxview-tab-list")
+ );
+ info("The list should show a visit from the new tab.");
+ await TestUtils.waitForCondition(() => visitList.rowEls.length === 1);
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ document,
+ "visibilitychange"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ await promiseHidden;
+ const { date } = await PlacesUtils.history
+ .fetch(NEW_TAB_URL, {
+ includeVisits: true,
+ })
+ .then(({ visits }) => visits[0]);
+ await addHistoryItems(date);
+ is(
+ visitList.rowEls.length,
+ 1,
+ "The list does not update when Firefox View is hidden."
+ );
+
+ info("The list should update when Firefox View is visible.");
+ await openFirefoxViewTab(browser.ownerGlobal);
+ visitList = await TestUtils.waitForCondition(() =>
+ historyComponent.cards?.[0]?.querySelector("fxview-tab-list")
+ );
+ await TestUtils.waitForCondition(() => visitList.rowEls.length > 1);
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(async function test_show_all_history_telemetry() {
+ await PlacesUtils.history.clear();
+ await addHistoryItems(today);
+ await addHistoryItems(yesterday);
+ await addHistoryItems(twoDaysAgo);
+ await addHistoryItems(threeDaysAgo);
+ await addHistoryItems(fourDaysAgo);
+ await addHistoryItems(oneMonthAgo);
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ await navigateToCategoryAndWait(document, "history");
+
+ let historyComponent = document.querySelector("view-history");
+ historyComponent.profileAge = 8;
+ await historyComponentReady(historyComponent);
+
+ await clearAllParentTelemetryEvents();
+ let showAllHistoryBtn = historyComponent.showAllHistoryBtn;
+ showAllHistoryBtn.scrollIntoView();
+ await EventUtils.synthesizeMouseAtCenter(showAllHistoryBtn, {}, content);
+ await showAllHistoryTelemetry();
+
+ // Make sure library window is shown
+ await TestUtils.waitForCondition(() =>
+ Services.wm.getMostRecentWindow("Places:Organizer")
+ );
+ let library = Services.wm.getMostRecentWindow("Places:Organizer");
+ await BrowserTestUtils.closeWindow(library);
+ gBrowser.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function test_search_history() {
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToCategoryAndWait(document, "history");
+ const historyComponent = document.querySelector("view-history");
+ historyComponent.profileAge = 8;
+ await historyComponentReady(historyComponent);
+ const searchTextbox = await TestUtils.waitForCondition(
+ () => historyComponent.searchTextbox,
+ "The search textbox is displayed."
+ );
+
+ info("Input a search query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
+ EventUtils.sendString("Example Domain 1", content);
+ await BrowserTestUtils.waitForMutationCondition(
+ historyComponent.shadowRoot,
+ { childList: true, subtree: true },
+ () =>
+ historyComponent.cards.length === 1 &&
+ document.l10n.getAttributes(
+ historyComponent.cards[0].querySelector("[slot=header]")
+ ).id === "firefoxview-search-results-header"
+ );
+ await TestUtils.waitForCondition(() => {
+ const { rowEls } = historyComponent.lists[0];
+ return rowEls.length === 1 && rowEls[0].mainEl.href === URLs[0];
+ }, "There is one matching search result.");
+
+ info("Input a bogus search query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
+ EventUtils.sendString("Bogus Query", content);
+ await TestUtils.waitForCondition(() => {
+ const tabList = historyComponent.lists[0];
+ return tabList?.shadowRoot.querySelector("fxview-empty-state");
+ }, "There are no matching search results.");
+
+ info("Clear the search query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox.clearButton, {}, content);
+ await BrowserTestUtils.waitForMutationCondition(
+ historyComponent.shadowRoot,
+ { childList: true, subtree: true },
+ () =>
+ historyComponent.cards.length ===
+ historyComponent.historyMapByDate.length
+ );
+ searchTextbox.blur();
+
+ info("Input a bogus search query with keyboard.");
+ EventUtils.synthesizeKey("f", { accelKey: true }, content);
+ EventUtils.sendString("Bogus Query", content);
+ await TestUtils.waitForCondition(() => {
+ const tabList = historyComponent.lists[0];
+ return tabList?.shadowRoot.querySelector("fxview-empty-state");
+ }, "There are no matching search results.");
+
+ info("Clear the search query with keyboard.");
+ is(
+ historyComponent.shadowRoot.activeElement,
+ searchTextbox,
+ "Search input is focused"
+ );
+ EventUtils.synthesizeKey("KEY_Tab", {}, content);
+ ok(
+ searchTextbox.clearButton.matches(":focus-visible"),
+ "Clear Search button is focused"
+ );
+ EventUtils.synthesizeKey("KEY_Enter", {}, content);
+ await BrowserTestUtils.waitForMutationCondition(
+ historyComponent.shadowRoot,
+ { childList: true, subtree: true },
+ () =>
+ historyComponent.cards.length ===
+ historyComponent.historyMapByDate.length
+ );
+ });
+});
+
+add_task(async function test_persist_collapse_card_after_view_change() {
+ await PlacesUtils.history.clear();
+ await addHistoryItems(today);
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToCategoryAndWait(document, "history");
+ const historyComponent = document.querySelector("view-history");
+ historyComponent.profileAge = 8;
+ await TestUtils.waitForCondition(
+ () =>
+ [...historyComponent.allHistoryItems.values()].reduce(
+ (acc, { length }) => acc + length,
+ 0
+ ) === 4
+ );
+ let firstHistoryCard = historyComponent.cards[0];
+ ok(
+ firstHistoryCard.isExpanded,
+ "The first history card is expanded initially."
+ );
+
+ // Collapse history card
+ EventUtils.synthesizeMouseAtCenter(firstHistoryCard.summaryEl, {}, content);
+ is(
+ firstHistoryCard.detailsEl.hasAttribute("open"),
+ false,
+ "The first history card is now collapsed."
+ );
+
+ // Switch to a new view and then back to History
+ await navigateToCategoryAndWait(document, "syncedtabs");
+ await navigateToCategoryAndWait(document, "history");
+
+ // Check that first history card is still collapsed after changing view
+ ok(
+ !firstHistoryCard.isExpanded,
+ "The first history card is still collapsed after changing view."
+ );
+
+ await PlacesUtils.history.clear();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ });
+});