summaryrefslogtreecommitdiffstats
path: root/browser/components/firefoxview
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:29 +0000
commit59203c63bb777a3bacec32fb8830fba33540e809 (patch)
tree58298e711c0ff0575818c30485b44a2f21bf28a0 /browser/components/firefoxview
parentAdding upstream version 126.0.1. (diff)
downloadfirefox-59203c63bb777a3bacec32fb8830fba33540e809.tar.xz
firefox-59203c63bb777a3bacec32fb8830fba33540e809.zip
Adding upstream version 127.0.upstream/127.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/firefoxview')
-rw-r--r--browser/components/firefoxview/HistoryController.mjs188
-rw-r--r--browser/components/firefoxview/HistoryController.sys.mjs383
-rw-r--r--browser/components/firefoxview/SyncedTabsController.sys.mjs2
-rw-r--r--browser/components/firefoxview/card-container.mjs131
-rw-r--r--browser/components/firefoxview/firefox-view-notification-manager.sys.mjs112
-rw-r--r--browser/components/firefoxview/firefox-view-places-query.sys.mjs187
-rw-r--r--browser/components/firefoxview/firefoxview.css11
-rw-r--r--browser/components/firefoxview/firefoxview.html1
-rw-r--r--browser/components/firefoxview/fxview-empty-state.mjs31
-rw-r--r--browser/components/firefoxview/fxview-search-textbox.mjs2
-rw-r--r--browser/components/firefoxview/fxview-tab-list.mjs12
-rw-r--r--browser/components/firefoxview/helpers.mjs21
-rw-r--r--browser/components/firefoxview/history.mjs67
-rw-r--r--browser/components/firefoxview/jar.mn2
-rw-r--r--browser/components/firefoxview/opentabs.mjs2
-rw-r--r--browser/components/firefoxview/recentlyclosed.mjs7
-rw-r--r--browser/components/firefoxview/search-helpers.mjs24
-rw-r--r--browser/components/firefoxview/syncedtabs.mjs1
-rw-r--r--browser/components/firefoxview/tests/browser/browser.toml3
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview.js39
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js4
-rw-r--r--browser/components/firefoxview/tests/browser/browser_history_firefoxview.js83
-rw-r--r--browser/components/firefoxview/tests/browser/browser_notification_dot.js392
-rw-r--r--browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js160
-rw-r--r--browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html21
25 files changed, 806 insertions, 1080 deletions
diff --git a/browser/components/firefoxview/HistoryController.mjs b/browser/components/firefoxview/HistoryController.mjs
deleted file mode 100644
index d2bda5cec2..0000000000
--- a/browser/components/firefoxview/HistoryController.mjs
+++ /dev/null
@@ -1,188 +0,0 @@
-/* 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/. */
-
-const lazy = {};
-
-ChromeUtils.defineESModuleGetters(lazy, {
- FirefoxViewPlacesQuery:
- "resource:///modules/firefox-view-places-query.sys.mjs",
- PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
-});
-
-let XPCOMUtils = ChromeUtils.importESModule(
- "resource://gre/modules/XPCOMUtils.sys.mjs"
-).XPCOMUtils;
-
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- "maxRowsPref",
- "browser.firefox-view.max-history-rows",
- -1
-);
-
-const HISTORY_MAP_L10N_IDS = {
- sidebar: {
- "history-date-today": "sidebar-history-date-today",
- "history-date-yesterday": "sidebar-history-date-yesterday",
- "history-date-this-month": "sidebar-history-date-this-month",
- "history-date-prev-month": "sidebar-history-date-prev-month",
- },
- firefoxview: {
- "history-date-today": "firefoxview-history-date-today",
- "history-date-yesterday": "firefoxview-history-date-yesterday",
- "history-date-this-month": "firefoxview-history-date-this-month",
- "history-date-prev-month": "firefoxview-history-date-prev-month",
- },
-};
-
-export class HistoryController {
- host;
- allHistoryItems;
- historyMapByDate;
- historyMapBySite;
- searchQuery;
- searchResults;
- sortOption;
-
- constructor(host, options) {
- this.allHistoryItems = new Map();
- this.historyMapByDate = [];
- this.historyMapBySite = [];
- this.placesQuery = new lazy.FirefoxViewPlacesQuery();
- this.searchQuery = "";
- this.searchResults = null;
- this.sortOption = "date";
- this.searchResultsLimit = options?.searchResultsLimit || 300;
- this.component = HISTORY_MAP_L10N_IDS?.[options?.component]
- ? options?.component
- : "firefoxview";
- this.host = host;
-
- host.addController(this);
- }
-
- async hostConnected() {
- this.placesQuery.observeHistory(data => this.updateAllHistoryItems(data));
- await this.updateHistoryData();
- this.createHistoryMaps();
- }
-
- hostDisconnected() {
- this.placesQuery.close();
- }
-
- deleteFromHistory() {
- lazy.PlacesUtils.history.remove(this.host.triggerNode.url);
- }
-
- async onSearchQuery(e) {
- this.searchQuery = e.detail.query;
- await this.updateSearchResults();
- this.host.requestUpdate();
- }
-
- async onChangeSortOption(e) {
- this.sortOption = e.target.value;
- await this.updateHistoryData();
- await this.updateSearchResults();
- this.host.requestUpdate();
- }
-
- async updateHistoryData() {
- this.allHistoryItems = await this.placesQuery.getHistory({
- daysOld: 60,
- limit: lazy.maxRowsPref,
- sortBy: this.sortOption,
- });
- }
-
- async updateAllHistoryItems(allHistoryItems) {
- if (allHistoryItems) {
- this.allHistoryItems = allHistoryItems;
- } else {
- await this.updateHistoryData();
- }
- this.resetHistoryMaps();
- this.host.requestUpdate();
- await this.updateSearchResults();
- }
-
- async updateSearchResults() {
- if (this.searchQuery) {
- try {
- this.searchResults = await this.placesQuery.searchHistory(
- this.searchQuery,
- this.searchResultsLimit
- );
- } catch (e) {
- // Connection interrupted, ignore.
- }
- } else {
- this.searchResults = null;
- }
- }
-
- resetHistoryMaps() {
- this.historyMapByDate = [];
- this.historyMapBySite = [];
- }
-
- createHistoryMaps() {
- if (!this.historyMapByDate.length) {
- const {
- visitsFromToday,
- visitsFromYesterday,
- visitsByDay,
- visitsByMonth,
- } = this.placesQuery;
-
- // Add visits from today and yesterday.
- if (visitsFromToday.length) {
- this.historyMapByDate.push({
- l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-today"],
- items: visitsFromToday,
- });
- }
- if (visitsFromYesterday.length) {
- this.historyMapByDate.push({
- l10nId:
- HISTORY_MAP_L10N_IDS[this.component]["history-date-yesterday"],
- items: visitsFromYesterday,
- });
- }
-
- // Add visits from this month, grouped by day.
- visitsByDay.forEach(visits => {
- this.historyMapByDate.push({
- l10nId:
- HISTORY_MAP_L10N_IDS[this.component]["history-date-this-month"],
- items: visits,
- });
- });
-
- // Add visits from previous months, grouped by month.
- visitsByMonth.forEach(visits => {
- this.historyMapByDate.push({
- l10nId:
- HISTORY_MAP_L10N_IDS[this.component]["history-date-prev-month"],
- items: visits,
- });
- });
- } else if (
- this.sortOption === "site" &&
- !this.historyMapBySite.length &&
- this.component === "firefoxview"
- ) {
- this.historyMapBySite = Array.from(
- this.allHistoryItems.entries(),
- ([domain, items]) => ({
- domain,
- items,
- l10nId: domain ? null : "firefoxview-history-site-localhost",
- })
- ).sort((a, b) => a.domain.localeCompare(b.domain));
- }
- this.host.requestUpdate();
- }
-}
diff --git a/browser/components/firefoxview/HistoryController.sys.mjs b/browser/components/firefoxview/HistoryController.sys.mjs
new file mode 100644
index 0000000000..b6f316e8e7
--- /dev/null
+++ b/browser/components/firefoxview/HistoryController.sys.mjs
@@ -0,0 +1,383 @@
+/* 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/. */
+
+const lazy = {};
+
+import { getLogger } from "chrome://browser/content/firefoxview/helpers.mjs";
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PlacesQuery: "resource://gre/modules/PlacesQuery.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+let XPCOMUtils = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+).XPCOMUtils;
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "maxRowsPref",
+ "browser.firefox-view.max-history-rows",
+ -1
+);
+
+const HISTORY_MAP_L10N_IDS = {
+ sidebar: {
+ "history-date-today": "sidebar-history-date-today",
+ "history-date-yesterday": "sidebar-history-date-yesterday",
+ "history-date-this-month": "sidebar-history-date-this-month",
+ "history-date-prev-month": "sidebar-history-date-prev-month",
+ },
+ firefoxview: {
+ "history-date-today": "firefoxview-history-date-today",
+ "history-date-yesterday": "firefoxview-history-date-yesterday",
+ "history-date-this-month": "firefoxview-history-date-this-month",
+ "history-date-prev-month": "firefoxview-history-date-prev-month",
+ },
+};
+
+/**
+ * A list of visits displayed on a card.
+ *
+ * @typedef {object} CardEntry
+ *
+ * @property {string} domain
+ * @property {HistoryVisit[]} items
+ * @property {string} l10nId
+ */
+
+export class HistoryController {
+ /**
+ * @type {{ entries: CardEntry[]; searchQuery: string; sortOption: string; }}
+ */
+ historyCache;
+ host;
+ searchQuery;
+ sortOption;
+ #todaysDate;
+ #yesterdaysDate;
+
+ constructor(host, options) {
+ this.placesQuery = new lazy.PlacesQuery();
+ this.searchQuery = "";
+ this.sortOption = "date";
+ this.searchResultsLimit = options?.searchResultsLimit || 300;
+ this.component = HISTORY_MAP_L10N_IDS?.[options?.component]
+ ? options?.component
+ : "firefoxview";
+ this.historyCache = {
+ entries: [],
+ searchQuery: null,
+ sortOption: null,
+ };
+ this.host = host;
+
+ host.addController(this);
+ }
+
+ hostConnected() {
+ this.placesQuery.observeHistory(historyMap => this.updateCache(historyMap));
+ }
+
+ hostDisconnected() {
+ ChromeUtils.idleDispatch(() => this.placesQuery.close());
+ }
+
+ deleteFromHistory() {
+ lazy.PlacesUtils.history.remove(this.host.triggerNode.url);
+ }
+
+ onSearchQuery(e) {
+ this.searchQuery = e.detail.query;
+ this.updateCache();
+ }
+
+ onChangeSortOption(e) {
+ this.sortOption = e.target.value;
+ this.updateCache();
+ }
+
+ get historyVisits() {
+ return this.historyCache.entries;
+ }
+
+ get searchResults() {
+ return this.historyCache.searchQuery
+ ? this.historyCache.entries[0].items
+ : null;
+ }
+
+ get totalVisitsCount() {
+ return this.historyVisits.reduce(
+ (count, entry) => count + entry.items.length,
+ 0
+ );
+ }
+
+ get isHistoryEmpty() {
+ return !this.historyVisits.length;
+ }
+
+ /**
+ * Update cached history.
+ *
+ * @param {Map<CacheKey, HistoryVisit[]>} [historyMap]
+ * If provided, performs an update using the given data (instead of fetching
+ * it from the db).
+ */
+ async updateCache(historyMap) {
+ const { searchQuery, sortOption } = this;
+ const entries = searchQuery
+ ? await this.#getVisitsForSearchQuery(searchQuery)
+ : await this.#getVisitsForSortOption(sortOption, historyMap);
+ if (this.searchQuery !== searchQuery || this.sortOption !== sortOption) {
+ // This query is stale, discard results and do not update the cache / UI.
+ return;
+ }
+ for (const { items } of entries) {
+ for (const item of items) {
+ this.#normalizeVisit(item);
+ }
+ }
+ this.historyCache = { entries, searchQuery, sortOption };
+ this.host.requestUpdate();
+ }
+
+ /**
+ * Normalize data for fxview-tabs-list.
+ *
+ * @param {HistoryVisit} visit
+ * The visit to format.
+ */
+ #normalizeVisit(visit) {
+ visit.time = visit.date.getTime();
+ visit.title = visit.title || visit.url;
+ visit.icon = `page-icon:${visit.url}`;
+ visit.primaryL10nId = "fxviewtabrow-tabs-list-tab";
+ visit.primaryL10nArgs = JSON.stringify({
+ targetURI: visit.url,
+ });
+ visit.secondaryL10nId = "fxviewtabrow-options-menu-button";
+ visit.secondaryL10nArgs = JSON.stringify({
+ tabTitle: visit.title || visit.url,
+ });
+ }
+
+ async #getVisitsForSearchQuery(searchQuery) {
+ let items = [];
+ try {
+ items = await this.placesQuery.searchHistory(
+ searchQuery,
+ this.searchResultsLimit
+ );
+ } catch (e) {
+ getLogger("HistoryController").warn(
+ "There is a new search query in progress, so cancelling this one.",
+ e
+ );
+ }
+ return [{ items }];
+ }
+
+ async #getVisitsForSortOption(sortOption, historyMap) {
+ if (!historyMap) {
+ historyMap = await this.#fetchHistory();
+ }
+ switch (sortOption) {
+ case "date":
+ this.#setTodaysDate();
+ return this.#getVisitsForDate(historyMap);
+ case "site":
+ return this.#getVisitsForSite(historyMap);
+ default:
+ return [];
+ }
+ }
+
+ #setTodaysDate() {
+ const now = new Date();
+ this.#todaysDate = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate()
+ );
+ this.#yesterdaysDate = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate() - 1
+ );
+ }
+
+ /**
+ * Get a list of visits, sorted by date, in reverse chronological order.
+ *
+ * @param {Map<number, HistoryVisit[]>} historyMap
+ * @returns {CardEntry[]}
+ */
+ #getVisitsForDate(historyMap) {
+ const entries = [];
+ const visitsFromToday = this.#getVisitsFromToday(historyMap);
+ const visitsFromYesterday = this.#getVisitsFromYesterday(historyMap);
+ const visitsByDay = this.#getVisitsByDay(historyMap);
+ const visitsByMonth = this.#getVisitsByMonth(historyMap);
+
+ // Add visits from today and yesterday.
+ if (visitsFromToday.length) {
+ entries.push({
+ l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-today"],
+ items: visitsFromToday,
+ });
+ }
+ if (visitsFromYesterday.length) {
+ entries.push({
+ l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-yesterday"],
+ items: visitsFromYesterday,
+ });
+ }
+
+ // Add visits from this month, grouped by day.
+ visitsByDay.forEach(visits => {
+ entries.push({
+ l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-this-month"],
+ items: visits,
+ });
+ });
+
+ // Add visits from previous months, grouped by month.
+ visitsByMonth.forEach(visits => {
+ entries.push({
+ l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-prev-month"],
+ items: visits,
+ });
+ });
+ return entries;
+ }
+
+ #getVisitsFromToday(cachedHistory) {
+ const mapKey = this.placesQuery.getStartOfDayTimestamp(this.#todaysDate);
+ const visits = cachedHistory.get(mapKey) ?? [];
+ return [...visits];
+ }
+
+ #getVisitsFromYesterday(cachedHistory) {
+ const mapKey = this.placesQuery.getStartOfDayTimestamp(
+ this.#yesterdaysDate
+ );
+ const visits = cachedHistory.get(mapKey) ?? [];
+ return [...visits];
+ }
+
+ /**
+ * Get a list of visits per day for each day on this month, excluding today
+ * and yesterday.
+ *
+ * @param {Map<number, HistoryVisit[]>} cachedHistory
+ * The history cache to process.
+ * @returns {HistoryVisit[][]}
+ * A list of visits for each day.
+ */
+ #getVisitsByDay(cachedHistory) {
+ const visitsPerDay = [];
+ for (const [time, visits] of cachedHistory.entries()) {
+ const date = new Date(time);
+ if (
+ this.#isSameDate(date, this.#todaysDate) ||
+ this.#isSameDate(date, this.#yesterdaysDate)
+ ) {
+ continue;
+ } else if (!this.#isSameMonth(date, this.#todaysDate)) {
+ break;
+ } else {
+ visitsPerDay.push(visits);
+ }
+ }
+ return visitsPerDay;
+ }
+
+ /**
+ * Get a list of visits per month for each month, excluding this one, and
+ * excluding yesterday's visits if yesterday happens to fall on the previous
+ * month.
+ *
+ * @param {Map<number, HistoryVisit[]>} cachedHistory
+ * The history cache to process.
+ * @returns {HistoryVisit[][]}
+ * A list of visits for each month.
+ */
+ #getVisitsByMonth(cachedHistory) {
+ const visitsPerMonth = [];
+ let previousMonth = null;
+ for (const [time, visits] of cachedHistory.entries()) {
+ const date = new Date(time);
+ if (
+ this.#isSameMonth(date, this.#todaysDate) ||
+ this.#isSameDate(date, this.#yesterdaysDate)
+ ) {
+ continue;
+ }
+ const month = this.placesQuery.getStartOfMonthTimestamp(date);
+ if (month !== previousMonth) {
+ visitsPerMonth.push(visits);
+ } else {
+ visitsPerMonth[visitsPerMonth.length - 1] = visitsPerMonth
+ .at(-1)
+ .concat(visits);
+ }
+ previousMonth = month;
+ }
+ return visitsPerMonth;
+ }
+
+ /**
+ * Given two date instances, check if their dates are equivalent.
+ *
+ * @param {Date} dateToCheck
+ * @param {Date} date
+ * @returns {boolean}
+ * Whether both date instances have equivalent dates.
+ */
+ #isSameDate(dateToCheck, date) {
+ return (
+ dateToCheck.getDate() === date.getDate() &&
+ this.#isSameMonth(dateToCheck, date)
+ );
+ }
+
+ /**
+ * Given two date instances, check if their months are equivalent.
+ *
+ * @param {Date} dateToCheck
+ * @param {Date} month
+ * @returns {boolean}
+ * Whether both date instances have equivalent months.
+ */
+ #isSameMonth(dateToCheck, month) {
+ return (
+ dateToCheck.getMonth() === month.getMonth() &&
+ dateToCheck.getFullYear() === month.getFullYear()
+ );
+ }
+
+ /**
+ * Get a list of visits, sorted by site, in alphabetical order.
+ *
+ * @param {Map<string, HistoryVisit[]>} historyMap
+ * @returns {CardEntry[]}
+ */
+ #getVisitsForSite(historyMap) {
+ return Array.from(historyMap.entries(), ([domain, items]) => ({
+ domain,
+ items,
+ l10nId: domain ? null : "firefoxview-history-site-localhost",
+ })).sort((a, b) => a.domain.localeCompare(b.domain));
+ }
+
+ async #fetchHistory() {
+ return this.placesQuery.getHistory({
+ daysOld: 60,
+ limit: lazy.maxRowsPref,
+ sortBy: this.sortOption,
+ });
+ }
+}
diff --git a/browser/components/firefoxview/SyncedTabsController.sys.mjs b/browser/components/firefoxview/SyncedTabsController.sys.mjs
index 6ab8249bfe..9462766545 100644
--- a/browser/components/firefoxview/SyncedTabsController.sys.mjs
+++ b/browser/components/firefoxview/SyncedTabsController.sys.mjs
@@ -10,7 +10,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
import { SyncedTabsErrorHandler } from "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs";
import { TabsSetupFlowManager } from "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs";
-import { searchTabList } from "chrome://browser/content/firefoxview/helpers.mjs";
+import { searchTabList } from "chrome://browser/content/firefoxview/search-helpers.mjs";
const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
diff --git a/browser/components/firefoxview/card-container.mjs b/browser/components/firefoxview/card-container.mjs
index 1755d97555..84c6acc5c4 100644
--- a/browser/components/firefoxview/card-container.mjs
+++ b/browser/components/firefoxview/card-container.mjs
@@ -132,76 +132,71 @@ class CardContainer extends MozLitElement {
rel="stylesheet"
href="chrome://browser/content/firefoxview/card-container.css"
/>
- <section
- aria-labelledby="header"
- aria-label=${ifDefined(this.sectionLabel)}
- >
- ${when(
- this.toggleDisabled,
- () => html`<div
- class=${classMap({
- "card-container": true,
- inner: this.isInnerCard,
- "empty-state": this.isEmptyState && !this.isInnerCard,
- })}
+ ${when(
+ this.toggleDisabled,
+ () => html`<div
+ class=${classMap({
+ "card-container": true,
+ inner: this.isInnerCard,
+ "empty-state": this.isEmptyState && !this.isInnerCard,
+ })}
+ >
+ <span
+ id="header"
+ class="card-container-header"
+ ?hidden=${ifDefined(this.hideHeader)}
+ toggleDisabled
+ ?withViewAll=${this.showViewAll}
>
- <span
- id="header"
- class="card-container-header"
- ?hidden=${ifDefined(this.hideHeader)}
- toggleDisabled
- ?withViewAll=${this.showViewAll}
- >
- <slot name="header"></slot>
- <slot name="secondary-header"></slot>
- </span>
- <a
- href="about:firefoxview#${this.shortPageName}"
- @click=${this.viewAllClicked}
- class="view-all-link"
- data-l10n-id="firefoxview-view-all-link"
- ?hidden=${!this.showViewAll}
- ></a>
- <slot name="main"></slot>
- <slot name="footer" class="card-container-footer"></slot>
- </div>`,
- () => html`<details
- class=${classMap({
- "card-container": true,
- inner: this.isInnerCard,
- "empty-state": this.isEmptyState && !this.isInnerCard,
- })}
- ?open=${this.isExpanded}
- ?isOpenTabsView=${this.removeBlockEndMargin}
- @toggle=${this.onToggleContainer}
+ <slot name="header"></slot>
+ <slot name="secondary-header"></slot>
+ </span>
+ <a
+ href="about:firefoxview#${this.shortPageName}"
+ @click=${this.viewAllClicked}
+ class="view-all-link"
+ data-l10n-id="firefoxview-view-all-link"
+ ?hidden=${!this.showViewAll}
+ ></a>
+ <slot name="main"></slot>
+ <slot name="footer" class="card-container-footer"></slot>
+ </div>`,
+ () => html`<details
+ class=${classMap({
+ "card-container": true,
+ inner: this.isInnerCard,
+ "empty-state": this.isEmptyState && !this.isInnerCard,
+ })}
+ ?open=${this.isExpanded}
+ ?isOpenTabsView=${this.removeBlockEndMargin}
+ @toggle=${this.onToggleContainer}
+ role=${this.isInnerCard ? "presentation" : "group"}
+ >
+ <summary
+ class="card-container-header"
+ ?hidden=${ifDefined(this.hideHeader)}
+ ?withViewAll=${this.showViewAll}
>
- <summary
- id="header"
- class="card-container-header"
- ?hidden=${ifDefined(this.hideHeader)}
- ?withViewAll=${this.showViewAll}
- >
- <span
- class="icon chevron-icon"
- aria-role="presentation"
- data-l10n-id="firefoxview-collapse-button-${this.isExpanded
- ? "hide"
- : "show"}"
- ></span>
- <slot name="header"></slot>
- </summary>
- <a
- href="about:firefoxview#${this.shortPageName}"
- @click=${this.viewAllClicked}
- class="view-all-link"
- data-l10n-id="firefoxview-view-all-link"
- ?hidden=${!this.showViewAll}
- ></a>
- <slot name="main"></slot>
- <slot name="footer" class="card-container-footer"></slot>
- </details>`
- )}
- </section>
+ <span
+ class="icon chevron-icon"
+ role="presentation"
+ data-l10n-id="firefoxview-collapse-button-${this.isExpanded
+ ? "hide"
+ : "show"}"
+ ></span>
+ <slot name="header"></slot>
+ </summary>
+ <a
+ href="about:firefoxview#${this.shortPageName}"
+ @click=${this.viewAllClicked}
+ class="view-all-link"
+ data-l10n-id="firefoxview-view-all-link"
+ ?hidden=${!this.showViewAll}
+ ></a>
+ <slot name="main"></slot>
+ <slot name="footer" class="card-container-footer"></slot>
+ </details>`
+ )}
`;
}
}
diff --git a/browser/components/firefoxview/firefox-view-notification-manager.sys.mjs b/browser/components/firefoxview/firefox-view-notification-manager.sys.mjs
deleted file mode 100644
index 3f9056a7cd..0000000000
--- a/browser/components/firefoxview/firefox-view-notification-manager.sys.mjs
+++ /dev/null
@@ -1,112 +0,0 @@
-/* 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/. */
-
-/**
- * This module exports the FirefoxViewNotificationManager singleton, which manages the notification state
- * for the Firefox View button
- */
-
-const RECENT_TABS_SYNC = "services.sync.lastTabFetch";
-const SHOULD_NOTIFY_FOR_TABS = "browser.tabs.firefox-view.notify-for-tabs";
-const lazy = {};
-
-import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
-
-ChromeUtils.defineESModuleGetters(lazy, {
- BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
- SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
-});
-
-export const FirefoxViewNotificationManager = new (class {
- #currentlyShowing;
- constructor() {
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- "lastTabFetch",
- RECENT_TABS_SYNC,
- 0,
- () => {
- this.handleTabSync();
- }
- );
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- "shouldNotifyForTabs",
- SHOULD_NOTIFY_FOR_TABS,
- false
- );
- // Need to access the pref variable for the observer to start observing
- // See the defineLazyPreferenceGetter function header
- this.lastTabFetch;
-
- Services.obs.addObserver(this, "firefoxview-notification-dot-update");
-
- this.#currentlyShowing = false;
- }
-
- async handleTabSync() {
- if (!this.shouldNotifyForTabs) {
- return;
- }
- let newSyncedTabs = await lazy.SyncedTabs.getRecentTabs(3);
- this.#currentlyShowing = this.tabsListChanged(newSyncedTabs);
- this.showNotificationDot();
- this.syncedTabs = newSyncedTabs;
- }
-
- showNotificationDot() {
- if (this.#currentlyShowing) {
- Services.obs.notifyObservers(
- null,
- "firefoxview-notification-dot-update",
- "true"
- );
- }
- }
-
- observe(sub, topic, data) {
- if (topic === "firefoxview-notification-dot-update" && data === "false") {
- this.#currentlyShowing = false;
- }
- }
-
- tabsListChanged(newTabs) {
- // The first time the tabs list is changed this.tabs is undefined because we haven't synced yet.
- // We don't want to show the badge here because it's not an actual change,
- // we are just syncing for the first time.
- if (!this.syncedTabs) {
- return false;
- }
-
- // We loop through all windows to see if any window has currentURI "about:firefoxview" and
- // the window is visible because we don't want to show the notification badge in that case
- for (let window of lazy.BrowserWindowTracker.orderedWindows) {
- // if the url is "about:firefoxview" and the window visible we don't want to show the notification badge
- if (
- window.FirefoxViewHandler.tab?.selected &&
- !window.isFullyOccluded &&
- window.windowState !== window.STATE_MINIMIZED
- ) {
- return false;
- }
- }
-
- if (newTabs.length > this.syncedTabs.length) {
- return true;
- }
- for (let i = 0; i < newTabs.length; i++) {
- let newTab = newTabs[i];
- let oldTab = this.syncedTabs[i];
-
- if (newTab?.url !== oldTab?.url) {
- return true;
- }
- }
- return false;
- }
-
- shouldNotificationDotBeShowing() {
- return this.#currentlyShowing;
- }
-})();
diff --git a/browser/components/firefoxview/firefox-view-places-query.sys.mjs b/browser/components/firefoxview/firefox-view-places-query.sys.mjs
deleted file mode 100644
index 8923905769..0000000000
--- a/browser/components/firefoxview/firefox-view-places-query.sys.mjs
+++ /dev/null
@@ -1,187 +0,0 @@
-/* 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/. */
-
-import { PlacesQuery } from "resource://gre/modules/PlacesQuery.sys.mjs";
-
-/**
- * Extension of PlacesQuery which provides additional caches for Firefox View.
- */
-export class FirefoxViewPlacesQuery extends PlacesQuery {
- /** @type {Date} */
- #todaysDate = null;
- /** @type {Date} */
- #yesterdaysDate = null;
-
- get visitsFromToday() {
- if (this.cachedHistory == null || this.#todaysDate == null) {
- return [];
- }
- const mapKey = this.getStartOfDayTimestamp(this.#todaysDate);
- return this.cachedHistory.get(mapKey) ?? [];
- }
-
- get visitsFromYesterday() {
- if (this.cachedHistory == null || this.#yesterdaysDate == null) {
- return [];
- }
- const mapKey = this.getStartOfDayTimestamp(this.#yesterdaysDate);
- return this.cachedHistory.get(mapKey) ?? [];
- }
-
- /**
- * Get a list of visits per day for each day on this month, excluding today
- * and yesterday.
- *
- * @returns {HistoryVisit[][]}
- * A list of visits for each day.
- */
- get visitsByDay() {
- const visitsPerDay = [];
- for (const [time, visits] of this.cachedHistory.entries()) {
- const date = new Date(time);
- if (
- this.#isSameDate(date, this.#todaysDate) ||
- this.#isSameDate(date, this.#yesterdaysDate)
- ) {
- continue;
- } else if (!this.#isSameMonth(date, this.#todaysDate)) {
- break;
- } else {
- visitsPerDay.push(visits);
- }
- }
- return visitsPerDay;
- }
-
- /**
- * Get a list of visits per month for each month, excluding this one, and
- * excluding yesterday's visits if yesterday happens to fall on the previous
- * month.
- *
- * @returns {HistoryVisit[][]}
- * A list of visits for each month.
- */
- get visitsByMonth() {
- const visitsPerMonth = [];
- let previousMonth = null;
- for (const [time, visits] of this.cachedHistory.entries()) {
- const date = new Date(time);
- if (
- this.#isSameMonth(date, this.#todaysDate) ||
- this.#isSameDate(date, this.#yesterdaysDate)
- ) {
- continue;
- }
- const month = this.getStartOfMonthTimestamp(date);
- if (month !== previousMonth) {
- visitsPerMonth.push(visits);
- } else {
- visitsPerMonth[visitsPerMonth.length - 1] = visitsPerMonth
- .at(-1)
- .concat(visits);
- }
- previousMonth = month;
- }
- return visitsPerMonth;
- }
-
- formatRowAsVisit(row) {
- const visit = super.formatRowAsVisit(row);
- this.#normalizeVisit(visit);
- return visit;
- }
-
- formatEventAsVisit(event) {
- const visit = super.formatEventAsVisit(event);
- this.#normalizeVisit(visit);
- return visit;
- }
-
- /**
- * Normalize data for fxview-tabs-list.
- *
- * @param {HistoryVisit} visit
- * The visit to format.
- */
- #normalizeVisit(visit) {
- visit.time = visit.date.getTime();
- visit.title = visit.title || visit.url;
- visit.icon = `page-icon:${visit.url}`;
- visit.primaryL10nId = "fxviewtabrow-tabs-list-tab";
- visit.primaryL10nArgs = JSON.stringify({
- targetURI: visit.url,
- });
- visit.secondaryL10nId = "fxviewtabrow-options-menu-button";
- visit.secondaryL10nArgs = JSON.stringify({
- tabTitle: visit.title || visit.url,
- });
- }
-
- async fetchHistory() {
- await super.fetchHistory();
- if (this.cachedHistoryOptions.sortBy === "date") {
- this.#setTodaysDate();
- }
- }
-
- handlePageVisited(event) {
- const visit = super.handlePageVisited(event);
- if (!visit) {
- return;
- }
- if (
- this.cachedHistoryOptions.sortBy === "date" &&
- (this.#todaysDate == null ||
- (visit.date.getTime() > this.#todaysDate.getTime() &&
- !this.#isSameDate(visit.date, this.#todaysDate)))
- ) {
- // If today's date has passed (or is null), it should be updated now.
- this.#setTodaysDate();
- }
- }
-
- #setTodaysDate() {
- const now = new Date();
- this.#todaysDate = new Date(
- now.getFullYear(),
- now.getMonth(),
- now.getDate()
- );
- this.#yesterdaysDate = new Date(
- now.getFullYear(),
- now.getMonth(),
- now.getDate() - 1
- );
- }
-
- /**
- * Given two date instances, check if their dates are equivalent.
- *
- * @param {Date} dateToCheck
- * @param {Date} date
- * @returns {boolean}
- * Whether both date instances have equivalent dates.
- */
- #isSameDate(dateToCheck, date) {
- return (
- dateToCheck.getDate() === date.getDate() &&
- this.#isSameMonth(dateToCheck, date)
- );
- }
-
- /**
- * Given two date instances, check if their months are equivalent.
- *
- * @param {Date} dateToCheck
- * @param {Date} month
- * @returns {boolean}
- * Whether both date instances have equivalent months.
- */
- #isSameMonth(dateToCheck, month) {
- return (
- dateToCheck.getMonth() === month.getMonth() &&
- dateToCheck.getFullYear() === month.getFullYear()
- );
- }
-}
diff --git a/browser/components/firefoxview/firefoxview.css b/browser/components/firefoxview/firefoxview.css
index a91c90c39e..0789c887bf 100644
--- a/browser/components/firefoxview/firefoxview.css
+++ b/browser/components/firefoxview/firefoxview.css
@@ -31,17 +31,6 @@
--newtab-background-color: #F9F9FB;
--fxview-card-header-font-weight: 500;
-
- /* Make the attention dot color match the browser UI on Linux, and on HCM
- * with a lightweight theme. */
- &[lwtheme] {
- --attention-dot-color: light-dark(#2ac3a2, #54ffbd);
- }
- @media (-moz-platform: linux) {
- &:not([lwtheme]) {
- --attention-dot-color: AccentColor;
- }
- }
}
@media (prefers-color-scheme: dark) {
diff --git a/browser/components/firefoxview/firefoxview.html b/browser/components/firefoxview/firefoxview.html
index 5bffb5a1d8..bdaa41bd7c 100644
--- a/browser/components/firefoxview/firefoxview.html
+++ b/browser/components/firefoxview/firefoxview.html
@@ -13,7 +13,6 @@
<meta name="color-scheme" content="light dark" />
<title data-l10n-id="firefoxview-page-title"></title>
<link rel="localization" href="branding/brand.ftl" />
- <link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="browser/firefoxView.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<link rel="localization" href="browser/migrationWizard.ftl" />
diff --git a/browser/components/firefoxview/fxview-empty-state.mjs b/browser/components/firefoxview/fxview-empty-state.mjs
index 9e6bc488fa..3e94767043 100644
--- a/browser/components/firefoxview/fxview-empty-state.mjs
+++ b/browser/components/firefoxview/fxview-empty-state.mjs
@@ -53,7 +53,6 @@ class FxviewEmptyState extends MozLitElement {
return html``;
}
return html` <a
- aria-details="card-container"
data-l10n-name=${descriptionLink.name}
href=${descriptionLink.url}
target=${descriptionLink?.sameTarget ? "_self" : "_blank"}
@@ -68,7 +67,7 @@ class FxviewEmptyState extends MozLitElement {
/>
<card-container hideHeader="true" exportparts="image" ?isInnerCard="${
this.isInnerCard
- }" id="card-container" isEmptyState="true">
+ }" id="card-container" isEmptyState="true" role="group" aria-labelledby="header" aria-describedby="description">
<div slot="main" class=${classMap({
selectedTab: this.isSelectedTab,
imageHidden: !this.mainImageUrl,
@@ -98,19 +97,21 @@ class FxviewEmptyState extends MozLitElement {
data-l10n-args="${JSON.stringify(this.headerArgs)}">
</span>
</h2>
- ${repeat(
- this.descriptionLabels,
- descLabel => descLabel,
- (descLabel, index) => html`<p
- class=${classMap({
- description: true,
- secondary: index !== 0,
- })}
- data-l10n-id="${descLabel}"
- >
- ${this.linkTemplate(this.descriptionLink)}
- </p>`
- )}
+ <span id="description">
+ ${repeat(
+ this.descriptionLabels,
+ descLabel => descLabel,
+ (descLabel, index) => html`<p
+ class=${classMap({
+ description: true,
+ secondary: index !== 0,
+ })}
+ data-l10n-id="${descLabel}"
+ >
+ ${this.linkTemplate(this.descriptionLink)}
+ </p>`
+ )}
+ </span>
<slot name="primary-action"></slot>
</div>
</div>
diff --git a/browser/components/firefoxview/fxview-search-textbox.mjs b/browser/components/firefoxview/fxview-search-textbox.mjs
index 1332f5f3f6..107aa8f7a4 100644
--- a/browser/components/firefoxview/fxview-search-textbox.mjs
+++ b/browser/components/firefoxview/fxview-search-textbox.mjs
@@ -20,7 +20,7 @@ const SEARCH_DEBOUNCE_TIMEOUT_MS = 1000;
*
* There is no actual searching done here. That needs to be implemented by the
* `fxview-search-textbox-query` event handler. `searchTabList()` from
- * `helpers.mjs` can be used as a starting point.
+ * `search-helpers.mjs` can be used as a starting point.
*
* @property {string} placeholder
* The placeholder text for the search box.
diff --git a/browser/components/firefoxview/fxview-tab-list.mjs b/browser/components/firefoxview/fxview-tab-list.mjs
index 57181e3bea..63be9379db 100644
--- a/browser/components/firefoxview/fxview-tab-list.mjs
+++ b/browser/components/firefoxview/fxview-tab-list.mjs
@@ -11,7 +11,7 @@ import {
when,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
-import { escapeRegExp } from "./helpers.mjs";
+import { escapeRegExp } from "./search-helpers.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-button.mjs";
@@ -49,7 +49,6 @@ if (!window.IS_STORYBOOK) {
* @property {number} maxTabsLength - The max number of tabs for the list
* @property {Array} tabItems - Items to show in the tab list
* @property {string} searchQuery - The query string to highlight, if provided.
- * @property {string} searchInProgress - Whether a search has been initiated.
* @property {string} secondaryActionClass - The class used to style the secondary action element
* @property {string} tertiaryActionClass - The class used to style the tertiary action element
*/
@@ -65,7 +64,6 @@ export class FxviewTabListBase extends MozLitElement {
this.maxTabsLength = 25;
this.tabItems = [];
this.compactRows = false;
- this.searchInProgress = false;
this.updatesPaused = true;
this.#register();
}
@@ -80,12 +78,12 @@ export class FxviewTabListBase extends MozLitElement {
tabItems: { type: Array },
updatesPaused: { type: Boolean },
searchQuery: { type: String },
- searchInProgress: { type: Boolean },
secondaryActionClass: { type: String },
tertiaryActionClass: { type: String },
};
static queries = {
+ emptyState: "fxview-empty-state",
rowEls: {
all: "fxview-tab-row",
},
@@ -308,11 +306,7 @@ export class FxviewTabListBase extends MozLitElement {
}
render() {
- if (
- this.searchQuery &&
- this.tabItems.length === 0 &&
- !this.searchInProgress
- ) {
+ if (this.searchQuery && !this.tabItems.length) {
return this.emptySearchResultsTemplate();
}
return html`
diff --git a/browser/components/firefoxview/helpers.mjs b/browser/components/firefoxview/helpers.mjs
index b206deef18..fb41fac0e1 100644
--- a/browser/components/firefoxview/helpers.mjs
+++ b/browser/components/firefoxview/helpers.mjs
@@ -126,27 +126,6 @@ export function isSearchEnabled() {
}
/**
- * Escape special characters for regular expressions from a string.
- *
- * @param {string} string
- * The string to sanitize.
- * @returns {string} The sanitized string.
- */
-export function escapeRegExp(string) {
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-}
-
-/**
- * Search a tab list for items that match the given query.
- */
-export function searchTabList(query, tabList) {
- const regex = RegExp(escapeRegExp(query), "i");
- return tabList.filter(
- ({ title, url }) => regex.test(title) || regex.test(url)
- );
-}
-
-/**
* Get or create a logger, whose log-level is controlled by a pref
*
* @param {string} loggerName - Creating named loggers helps differentiate log messages from different
diff --git a/browser/components/firefoxview/history.mjs b/browser/components/firefoxview/history.mjs
index 478422d49b..4919f94e9c 100644
--- a/browser/components/firefoxview/history.mjs
+++ b/browser/components/firefoxview/history.mjs
@@ -15,13 +15,13 @@ import {
import { ViewPage } from "./viewpage.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/migration/migration-wizard.mjs";
-import { HistoryController } from "./HistoryController.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-button.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ HistoryController: "resource:///modules/HistoryController.sys.mjs",
ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
});
@@ -47,7 +47,7 @@ class HistoryInView extends ViewPage {
this.cumulativeSearches = 0;
}
- controller = new HistoryController(this, {
+ controller = new lazy.HistoryController(this, {
searchResultsLimit: SEARCH_RESULTS_LIMIT,
});
@@ -57,7 +57,7 @@ class HistoryInView extends ViewPage {
}
this._started = true;
- this.controller.updateAllHistoryItems();
+ this.controller.updateCache();
this.toggleVisibilityInCardContainer();
}
@@ -170,8 +170,8 @@ class HistoryInView extends ViewPage {
this.recordContextMenuTelemetry("delete-from-history", e);
}
- async onChangeSortOption(e) {
- await this.controller.onChangeSortOption(e);
+ onChangeSortOption(e) {
+ this.controller.onChangeSortOption(e);
Services.telemetry.recordEvent(
"firefoxview_next",
"sort_history",
@@ -184,8 +184,8 @@ class HistoryInView extends ViewPage {
);
}
- async onSearchQuery(e) {
- await this.controller.onSearchQuery(e);
+ onSearchQuery(e) {
+ this.controller.onSearchQuery(e);
this.cumulativeSearches = this.controller.searchQuery
? this.cumulativeSearches + 1
: 0;
@@ -287,7 +287,7 @@ class HistoryInView extends ViewPage {
get cardsTemplate() {
if (this.controller.searchResults) {
return this.#searchResultsTemplate();
- } else if (this.controller.allHistoryItems.size) {
+ } else if (!this.controller.isHistoryEmpty) {
return this.#historyCardsTemplate();
}
return this.#emptyMessageTemplate();
@@ -295,14 +295,11 @@ class HistoryInView extends ViewPage {
#historyCardsTemplate() {
let cardsTemplate = [];
- if (
- this.controller.sortOption === "date" &&
- this.controller.historyMapByDate.length
- ) {
- this.controller.historyMapByDate.forEach(historyItem => {
- if (historyItem.items.length) {
+ switch (this.controller.sortOption) {
+ case "date":
+ cardsTemplate = this.controller.historyVisits.map(historyItem => {
let dateArg = JSON.stringify({ date: historyItem.items[0].time });
- cardsTemplate.push(html`<card-container>
+ return html`<card-container>
<h3
slot="header"
data-l10n-id=${historyItem.l10nId}
@@ -316,19 +313,18 @@ class HistoryInView extends ViewPage {
: "time"}
hasPopup="menu"
maxTabsLength=${this.maxTabsLength}
- .tabItems=${[...historyItem.items]}
+ .tabItems=${historyItem.items}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
${this.panelListTemplate()}
</fxview-tab-list>
- </card-container>`);
- }
- });
- } else if (this.controller.historyMapBySite.length) {
- this.controller.historyMapBySite.forEach(historyItem => {
- if (historyItem.items.length) {
- cardsTemplate.push(html`<card-container>
+ </card-container>`;
+ });
+ break;
+ case "site":
+ cardsTemplate = this.controller.historyVisits.map(historyItem => {
+ return html`<card-container>
<h3 slot="header" data-l10n-id="${ifDefined(historyItem.l10nId)}">
${historyItem.domain}
</h3>
@@ -338,15 +334,15 @@ class HistoryInView extends ViewPage {
dateTimeFormat="dateTime"
hasPopup="menu"
maxTabsLength=${this.maxTabsLength}
- .tabItems=${[...historyItem.items]}
+ .tabItems=${historyItem.items}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
${this.panelListTemplate()}
</fxview-tab-list>
- </card-container>`);
- }
- });
+ </card-container>`;
+ });
+ break;
}
return cardsTemplate;
}
@@ -420,7 +416,6 @@ class HistoryInView extends ViewPage {
.tabItems=${this.controller.searchResults}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
- .searchInProgress=${this.controller.placesQuery.searchInProgress}
>
${this.panelListTemplate()}
</fxview-tab-list>
@@ -491,12 +486,19 @@ class HistoryInView extends ViewPage {
class="import-history-banner"
hideHeader="true"
?hidden=${!this.shouldShowImportBanner()}
+ role="group"
+ aria-labelledby="header"
+ aria-describedby="description"
>
<div slot="main">
<div class="banner-text">
- <span data-l10n-id="firefoxview-import-history-header"></span>
+ <span
+ data-l10n-id="firefoxview-import-history-header"
+ id="header"
+ ></span>
<span
data-l10n-id="firefoxview-import-history-description"
+ id="description"
></span>
</div>
<div class="buttons">
@@ -518,7 +520,7 @@ class HistoryInView extends ViewPage {
</div>
<div
class="show-all-history-footer"
- ?hidden=${!this.controller.allHistoryItems.size}
+ ?hidden=${this.controller.isHistoryEmpty}
>
<button
class="show-all-history-button"
@@ -532,11 +534,6 @@ class HistoryInView extends ViewPage {
willUpdate() {
this.fullyUpdated = false;
- if (this.controller.allHistoryItems.size) {
- // onChangeSortOption() will update history data once it has been fetched
- // from the API.
- this.controller.createHistoryMaps();
- }
}
}
customElements.define("view-history", HistoryInView);
diff --git a/browser/components/firefoxview/jar.mn b/browser/components/firefoxview/jar.mn
index 8bf3597aa5..6eee0b8ffd 100644
--- a/browser/components/firefoxview/jar.mn
+++ b/browser/components/firefoxview/jar.mn
@@ -9,7 +9,6 @@ browser.jar:
content/browser/firefoxview/firefoxview.mjs
content/browser/firefoxview/history.css
content/browser/firefoxview/history.mjs
- content/browser/firefoxview/HistoryController.mjs
content/browser/firefoxview/opentabs.mjs
content/browser/firefoxview/view-opentabs.css
content/browser/firefoxview/syncedtabs.mjs
@@ -19,6 +18,7 @@ browser.jar:
content/browser/firefoxview/fxview-empty-state.css
content/browser/firefoxview/fxview-empty-state.mjs
content/browser/firefoxview/helpers.mjs
+ content/browser/firefoxview/search-helpers.mjs
content/browser/firefoxview/fxview-search-textbox.css
content/browser/firefoxview/fxview-search-textbox.mjs
content/browser/firefoxview/fxview-tab-list.css
diff --git a/browser/components/firefoxview/opentabs.mjs b/browser/components/firefoxview/opentabs.mjs
index fb84553e26..10845374bc 100644
--- a/browser/components/firefoxview/opentabs.mjs
+++ b/browser/components/firefoxview/opentabs.mjs
@@ -13,9 +13,9 @@ import {
getLogger,
isSearchEnabled,
placeLinkOnClipboard,
- searchTabList,
MAX_TABS_FOR_RECENT_BROWSING,
} from "./helpers.mjs";
+import { searchTabList } from "./search-helpers.mjs";
import { ViewPage, ViewPageContent } from "./viewpage.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/firefoxview/opentabs-tab-list.mjs";
diff --git a/browser/components/firefoxview/recentlyclosed.mjs b/browser/components/firefoxview/recentlyclosed.mjs
index 7efd8d09f2..6b3ed711c4 100644
--- a/browser/components/firefoxview/recentlyclosed.mjs
+++ b/browser/components/firefoxview/recentlyclosed.mjs
@@ -8,11 +8,8 @@ import {
ifDefined,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
-import {
- isSearchEnabled,
- searchTabList,
- MAX_TABS_FOR_RECENT_BROWSING,
-} from "./helpers.mjs";
+import { isSearchEnabled, MAX_TABS_FOR_RECENT_BROWSING } from "./helpers.mjs";
+import { searchTabList } from "./search-helpers.mjs";
import { ViewPage } from "./viewpage.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/firefoxview/card-container.mjs";
diff --git a/browser/components/firefoxview/search-helpers.mjs b/browser/components/firefoxview/search-helpers.mjs
new file mode 100644
index 0000000000..3a8c1e580c
--- /dev/null
+++ b/browser/components/firefoxview/search-helpers.mjs
@@ -0,0 +1,24 @@
+/* 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/. */
+
+/**
+ * Escape special characters for regular expressions from a string.
+ *
+ * @param {string} string
+ * The string to sanitize.
+ * @returns {string} The sanitized string.
+ */
+export function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+/**
+ * Search a tab list for items that match the given query.
+ */
+export function searchTabList(query, tabList) {
+ const regex = RegExp(escapeRegExp(query), "i");
+ return tabList.filter(
+ ({ title, url }) => regex.test(title) || regex.test(url)
+ );
+}
diff --git a/browser/components/firefoxview/syncedtabs.mjs b/browser/components/firefoxview/syncedtabs.mjs
index 1c65650c10..e71cce465e 100644
--- a/browser/components/firefoxview/syncedtabs.mjs
+++ b/browser/components/firefoxview/syncedtabs.mjs
@@ -170,7 +170,6 @@ class SyncedTabsInView extends ViewPage {
data-l10n-id="${ifDefined(buttonLabel)}"
data-action="${action}"
@click=${e => this.controller.handleEvent(e)}
- aria-details="empty-container"
></button>
</fxview-empty-state>
`;
diff --git a/browser/components/firefoxview/tests/browser/browser.toml b/browser/components/firefoxview/tests/browser/browser.toml
index db8b2ea25c..c9036286d7 100644
--- a/browser/components/firefoxview/tests/browser/browser.toml
+++ b/browser/components/firefoxview/tests/browser/browser.toml
@@ -43,9 +43,6 @@ skip-if = ["true"] # Bug 1869605 and # Bug 1870296
["browser_history_firefoxview.js"]
-["browser_notification_dot.js"]
-skip-if = ["true"] # Bug 1851453
-
["browser_opentabs_cards.js"]
["browser_opentabs_changes.js"]
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_firefoxview.js
index 1a51d61f42..00083d7c91 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview.js
@@ -31,20 +31,47 @@ add_task(async function test_aria_roles() {
);
let recentlyClosedEmptyState = recentlyClosedComponent.emptyState;
let descriptionEls = recentlyClosedEmptyState.descriptionEls;
+ const recentlyClosedCard = SpecialPowers.wrap(
+ recentlyClosedEmptyState
+ ).openOrClosedShadowRoot.querySelector("card-container");
is(
- descriptionEls[1].querySelector("a").getAttribute("aria-details"),
- "card-container",
- "The link within the recently closed empty state has the expected 'aria-details' attribute."
+ recentlyClosedCard.getAttribute("aria-labelledby"),
+ "header",
+ "The recently closed empty state container has the expected 'aria-labelledby' attribute."
+ );
+ is(
+ recentlyClosedCard.getAttribute("aria-describedby"),
+ "description",
+ "The recently closed empty state container has the expected 'aria-describedby' attribute."
+ );
+ is(
+ recentlyClosedCard.getAttribute("role"),
+ "group",
+ "The recently closed empty state container has the expected 'role' attribute."
);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs[slot=syncedtabs]"
);
let syncedTabsEmptyState = syncedTabsComponent.emptyState;
+ const syncedCard =
+ SpecialPowers.wrap(
+ syncedTabsEmptyState
+ ).openOrClosedShadowRoot.querySelector("card-container");
+ is(
+ syncedCard.getAttribute("aria-labelledby"),
+ "header",
+ "The synced tabs empty state container has the expected 'aria-labelledby' attribute."
+ );
+ is(
+ syncedCard.getAttribute("aria-describedby"),
+ "description",
+ "The synced tabs empty state container has the expected 'aria-describedby' attribute."
+ );
is(
- syncedTabsEmptyState.querySelector("button").getAttribute("aria-details"),
- "empty-container",
- "The button within the synced tabs empty state has the expected 'aria-details' attribute."
+ syncedCard.getAttribute("role"),
+ "group",
+ "The synced tabs empty state container has the expected 'role' attribute."
);
// Test keyboard navigation from card-container summary
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js
index bf53796ef7..e9502079d9 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js
@@ -32,7 +32,9 @@ add_task(async function test_max_render_count_on_win_resize() {
await navigateToViewAndWait(document, "history");
let historyComponent = document.querySelector("view-history");
- let tabList = historyComponent.lists[0];
+ let tabList = await TestUtils.waitForCondition(
+ () => historyComponent.lists[0]
+ );
let rootVirtualList = tabList.rootVirtualListEl;
const initialHeight = window.outerHeight;
diff --git a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
index 847ce4d9fd..0bbc009eab 100644
--- a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
@@ -57,15 +57,11 @@ function isElInViewport(element) {
async function historyComponentReady(historyComponent, expectedHistoryItems) {
await TestUtils.waitForCondition(
- () =>
- [...historyComponent.controller.allHistoryItems.values()].reduce(
- (acc, { length }) => acc + length,
- 0
- ) === expectedHistoryItems,
+ () => historyComponent.controller.totalVisitsCount === expectedHistoryItems,
"History component ready"
);
- let expected = historyComponent.controller.historyMapByDate.length;
+ let expected = historyComponent.controller.historyVisits.length;
let actual = historyComponent.cards.length;
is(expected, actual, `Total number of cards should be ${expected}`);
@@ -242,8 +238,7 @@ add_task(async function test_list_ordering() {
await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
await sortHistoryTelemetry(sortHistoryEvent);
- let expectedNumOfCards =
- historyComponent.controller.historyMapBySite.length;
+ let expectedNumOfCards = historyComponent.controller.historyVisits.length;
info(`Total number of cards should be ${expectedNumOfCards}`);
await BrowserTestUtils.waitForMutationCondition(
@@ -475,7 +470,7 @@ add_task(async function test_search_history() {
EventUtils.sendString("Bogus Query", content);
await TestUtils.waitForCondition(() => {
const tabList = historyComponent.lists[0];
- return tabList?.shadowRoot.querySelector("fxview-empty-state");
+ return tabList?.emptyState;
}, "There are no matching search results.");
info("Clear the search query.");
@@ -485,7 +480,7 @@ add_task(async function test_search_history() {
{ childList: true, subtree: true },
() =>
historyComponent.cards.length ===
- historyComponent.controller.historyMapByDate.length
+ historyComponent.controller.historyVisits.length
);
searchTextbox.blur();
@@ -494,7 +489,7 @@ add_task(async function test_search_history() {
EventUtils.sendString("Bogus Query", content);
await TestUtils.waitForCondition(() => {
const tabList = historyComponent.lists[0];
- return tabList?.shadowRoot.querySelector("fxview-empty-state");
+ return tabList?.emptyState;
}, "There are no matching search results.");
info("Clear the search query with keyboard.");
@@ -514,11 +509,69 @@ add_task(async function test_search_history() {
{ childList: true, subtree: true },
() =>
historyComponent.cards.length ===
- historyComponent.controller.historyMapByDate.length
+ historyComponent.controller.historyVisits.length
);
});
});
+add_task(async function test_search_ignores_stale_queries() {
+ await PlacesUtils.history.clear();
+ const historyEntries = createHistoryEntries();
+ await PlacesUtils.history.insertMany(historyEntries);
+
+ let bogusQueryInProgress = false;
+ const searchDeferred = Promise.withResolvers();
+ const realDatabase = await PlacesUtils.promiseLargeCacheDBConnection();
+ const mockDatabase = {
+ executeCached: async (sql, options) => {
+ if (options.query === "Bogus Query") {
+ bogusQueryInProgress = true;
+ await searchDeferred.promise;
+ }
+ return realDatabase.executeCached(sql, options);
+ },
+ interrupt: () => searchDeferred.reject(),
+ };
+ const stub = sinon
+ .stub(PlacesUtils, "promiseLargeCacheDBConnection")
+ .resolves(mockDatabase);
+
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "history");
+ const historyComponent = document.querySelector("view-history");
+ historyComponent.profileAge = 8;
+ await historyComponentReady(historyComponent, historyEntries.length);
+ const searchTextbox = await TestUtils.waitForCondition(
+ () => historyComponent.searchTextbox,
+ "The search textbox is displayed."
+ );
+
+ info("Input a bogus search query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
+ EventUtils.sendString("Bogus Query", content);
+ await TestUtils.waitForCondition(() => bogusQueryInProgress);
+
+ info("Clear the bogus query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox.clearButton, {}, content);
+ await searchTextbox.updateComplete;
+
+ info("Input a real search query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
+ EventUtils.sendString("Example Domain 1", content);
+ await TestUtils.waitForCondition(() => {
+ const { rowEls } = historyComponent.lists[0];
+ return rowEls.length === 1 && rowEls[0].mainEl.href === URLs[1];
+ }, "There is one matching search result.");
+ searchDeferred.resolve();
+ await TestUtils.waitForTick();
+ const tabList = historyComponent.lists[0];
+ ok(!tabList.emptyState, "Empty state should not be shown.");
+ });
+
+ stub.restore();
+});
+
add_task(async function test_persist_collapse_card_after_view_change() {
await PlacesUtils.history.clear();
await addHistoryItems(today);
@@ -528,11 +581,7 @@ add_task(async function test_persist_collapse_card_after_view_change() {
const historyComponent = document.querySelector("view-history");
historyComponent.profileAge = 8;
await TestUtils.waitForCondition(
- () =>
- [...historyComponent.controller.allHistoryItems.values()].reduce(
- (acc, { length }) => acc + length,
- 0
- ) === 4
+ () => historyComponent.controller.totalVisitsCount === 4
);
let firstHistoryCard = historyComponent.cards[0];
ok(
diff --git a/browser/components/firefoxview/tests/browser/browser_notification_dot.js b/browser/components/firefoxview/tests/browser/browser_notification_dot.js
deleted file mode 100644
index 0fa747d40f..0000000000
--- a/browser/components/firefoxview/tests/browser/browser_notification_dot.js
+++ /dev/null
@@ -1,392 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const tabsList1 = syncedTabsData1[0].tabs;
-const tabsList2 = syncedTabsData1[1].tabs;
-const BADGE_TOP_RIGHT = "75% 25%";
-
-const { SyncedTabs } = ChromeUtils.importESModule(
- "resource://services-sync/SyncedTabs.sys.mjs"
-);
-
-function setupRecentDeviceListMocks() {
- const sandbox = sinon.createSandbox();
- sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => [
- {
- id: 1,
- name: "My desktop",
- isCurrentDevice: true,
- type: "desktop",
- tabs: [],
- },
- {
- id: 2,
- name: "My iphone",
- type: "mobile",
- tabs: [],
- },
- ]);
-
- sandbox.stub(UIState, "get").returns({
- status: UIState.STATUS_SIGNED_IN,
- syncEnabled: true,
- });
-
- return sandbox;
-}
-
-function waitForWindowActive(win, active) {
- info("Waiting for window activation");
- return Promise.all([
- BrowserTestUtils.waitForEvent(win, active ? "focus" : "blur"),
- BrowserTestUtils.waitForEvent(win, active ? "activate" : "deactivate"),
- ]);
-}
-
-async function waitForNotificationBadgeToBeShowing(fxViewButton) {
- info("Waiting for attention attribute to be set");
- await BrowserTestUtils.waitForMutationCondition(
- fxViewButton,
- { attributes: true },
- () => fxViewButton.hasAttribute("attention")
- );
- return fxViewButton.hasAttribute("attention");
-}
-
-async function waitForNotificationBadgeToBeHidden(fxViewButton) {
- info("Waiting for attention attribute to be removed");
- await BrowserTestUtils.waitForMutationCondition(
- fxViewButton,
- { attributes: true },
- () => !fxViewButton.hasAttribute("attention")
- );
- return !fxViewButton.hasAttribute("attention");
-}
-
-async function clickFirefoxViewButton(win) {
- await BrowserTestUtils.synthesizeMouseAtCenter(
- "#firefox-view-button",
- { type: "mousedown" },
- win.browsingContext
- );
-}
-
-function getBackgroundPositionForElement(ele) {
- let style = ele.ownerGlobal.getComputedStyle(ele);
- return style.getPropertyValue("background-position");
-}
-
-let previousFetchTime = 0;
-
-async function resetSyncedTabsLastFetched() {
- Services.prefs.clearUserPref("services.sync.lastTabFetch");
- previousFetchTime = 0;
- await TestUtils.waitForTick();
-}
-
-async function initTabSync() {
- let recentFetchTime = Math.floor(Date.now() / 1000);
- // ensure we don't try to set the pref with the same value, which will not produce
- // the expected pref change effects
- while (recentFetchTime == previousFetchTime) {
- await TestUtils.waitForTick();
- recentFetchTime = Math.floor(Date.now() / 1000);
- }
- Assert.greater(
- recentFetchTime,
- previousFetchTime,
- "The new lastTabFetch value is greater than the previous"
- );
-
- info("initTabSync, updating lastFetch:" + recentFetchTime);
- Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
- previousFetchTime = recentFetchTime;
- await TestUtils.waitForTick();
-}
-
-add_setup(async function () {
- await resetSyncedTabsLastFetched();
- await SpecialPowers.pushPrefEnv({
- set: [["browser.tabs.firefox-view.notify-for-tabs", true]],
- });
-
- // Clear any synced tabs from previous tests
- FirefoxViewNotificationManager.syncedTabs = null;
- Services.obs.notifyObservers(
- null,
- "firefoxview-notification-dot-update",
- "false"
- );
-});
-
-/**
- * Test that the notification badge will show and hide in the correct cases
- */
-add_task(async function testNotificationDot() {
- const sandbox = setupRecentDeviceListMocks();
- const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
- sandbox.spy(SyncedTabs, "syncTabs");
-
- let win = await BrowserTestUtils.openNewBrowserWindow();
- let fxViewBtn = win.document.getElementById("firefox-view-button");
- ok(fxViewBtn, "Got the Firefox View button");
-
- // Initiate a synced tabs update with new tabs
- syncedTabsMock.returns(tabsList1);
- await initTabSync();
-
- ok(
- BrowserTestUtils.isVisible(fxViewBtn),
- "The Firefox View button is showing"
- );
-
- info(
- "testNotificationDot, button is showing, badge should be initially hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing initially"
- );
-
- // Initiate a synced tabs update with new tabs
- syncedTabsMock.returns(tabsList2);
- await initTabSync();
-
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing after first tab sync"
- );
-
- // check that switching to the firefoxviewtab removes the badge
- await clickFirefoxViewButton(win);
-
- info(
- "testNotificationDot, after clicking the button, badge should become hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing after going to Firefox View"
- );
-
- await BrowserTestUtils.waitForCondition(() => {
- return SyncedTabs.syncTabs.calledOnce;
- });
-
- ok(SyncedTabs.syncTabs.calledOnce, "SyncedTabs.syncTabs() was called once");
-
- syncedTabsMock.returns(tabsList1);
- // Initiate a synced tabs update with new tabs
- await initTabSync();
-
- // The noti badge would show but we are on a Firefox View page so no need to show the noti badge
- info(
- "testNotificationDot, after updating the recent tabs, badge should be hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing after tab sync while Firefox View is focused"
- );
-
- let newTab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
- syncedTabsMock.returns(tabsList2);
- await initTabSync();
-
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing after navigation to a new tab"
- );
-
- // check that switching back to the Firefox View tab removes the badge
- await clickFirefoxViewButton(win);
-
- info(
- "testNotificationDot, after switching back to fxview, badge should be hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing after focusing the Firefox View tab"
- );
-
- await BrowserTestUtils.switchTab(win.gBrowser, newTab);
-
- // Initiate a synced tabs update with no new tabs
- await initTabSync();
-
- info(
- "testNotificationDot, after switching back to fxview with no new tabs, badge should be hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing after a tab sync with the same tabs"
- );
-
- await BrowserTestUtils.closeWindow(win);
-
- sandbox.restore();
-});
-
-/**
- * Tests the notification badge with multiple windows
- */
-add_task(async function testNotificationDotOnMultipleWindows() {
- const sandbox = setupRecentDeviceListMocks();
-
- await resetSyncedTabsLastFetched();
- const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
-
- // Create a new window
- let win1 = await BrowserTestUtils.openNewBrowserWindow();
- await win1.delayedStartupPromise;
- let fxViewBtn = win1.document.getElementById("firefox-view-button");
- ok(fxViewBtn, "Got the Firefox View button");
-
- syncedTabsMock.returns(tabsList1);
- // Initiate a synced tabs update
- await initTabSync();
-
- // Create another window
- let win2 = await BrowserTestUtils.openNewBrowserWindow();
- await win2.delayedStartupPromise;
- let fxViewBtn2 = win2.document.getElementById("firefox-view-button");
-
- await clickFirefoxViewButton(win2);
-
- // Make sure the badge doesn't show on any window
- info(
- "testNotificationDotOnMultipleWindows, badge is initially hidden on window 1"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing in the inital window"
- );
- info(
- "testNotificationDotOnMultipleWindows, badge is initially hidden on window 2"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn2),
- "The notification badge is not showing in the second window"
- );
-
- // Minimize the window.
- win2.minimize();
-
- await TestUtils.waitForCondition(
- () => !win2.gBrowser.selectedBrowser.docShellIsActive,
- "Waiting for docshell to be marked as inactive after minimizing the window"
- );
-
- syncedTabsMock.returns(tabsList2);
- info("Initiate a synced tabs update with new tabs");
- await initTabSync();
-
- // The badge will show because the View tab is minimized
- // Make sure the badge shows on all windows
- info(
- "testNotificationDotOnMultipleWindows, after new synced tabs and minimized win2, badge is visible on window 1"
- );
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing in the initial window"
- );
- info(
- "testNotificationDotOnMultipleWindows, after new synced tabs and minimized win2, badge is visible on window 2"
- );
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn2),
- "The notification badge is showing in the second window"
- );
-
- win2.restore();
- await TestUtils.waitForCondition(
- () => win2.gBrowser.selectedBrowser.docShellIsActive,
- "Waiting for docshell to be marked as active after restoring the window"
- );
-
- await BrowserTestUtils.closeWindow(win1);
- await BrowserTestUtils.closeWindow(win2);
-
- sandbox.restore();
-});
-
-/**
- * Tests the notification badge is in the correct spot and that the badge shows when opening a new window
- * if another window is showing the badge
- */
-add_task(async function testNotificationDotLocation() {
- const sandbox = setupRecentDeviceListMocks();
- await resetSyncedTabsLastFetched();
- const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
-
- syncedTabsMock.returns(tabsList1);
-
- let win1 = await BrowserTestUtils.openNewBrowserWindow();
- let fxViewBtn = win1.document.getElementById("firefox-view-button");
- ok(fxViewBtn, "Got the Firefox View button");
-
- // Initiate a synced tabs update
- await initTabSync();
- syncedTabsMock.returns(tabsList2);
- // Initiate another synced tabs update
- await initTabSync();
-
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing initially"
- );
-
- // Create a new window
- let win2 = await BrowserTestUtils.openNewBrowserWindow();
- await win2.delayedStartupPromise;
-
- // Make sure the badge is showing on the new window
- let fxViewBtn2 = win2.document.getElementById("firefox-view-button");
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn2),
- "The notification badge is showing in the second window after opening"
- );
-
- // Make sure the badge is below and center now
- isnot(
- getBackgroundPositionForElement(fxViewBtn),
- BADGE_TOP_RIGHT,
- "The notification badge is not showing in the top right in the initial window"
- );
- isnot(
- getBackgroundPositionForElement(fxViewBtn2),
- BADGE_TOP_RIGHT,
- "The notification badge is not showing in the top right in the second window"
- );
-
- CustomizableUI.addWidgetToArea(
- "firefox-view-button",
- CustomizableUI.AREA_NAVBAR
- );
-
- // Make sure both windows still have the notification badge
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing in the initial window"
- );
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn2),
- "The notification badge is showing in the second window"
- );
-
- // Make sure the badge is in the top right now
- is(
- getBackgroundPositionForElement(fxViewBtn),
- BADGE_TOP_RIGHT,
- "The notification badge is showing in the top right in the initial window"
- );
- is(
- getBackgroundPositionForElement(fxViewBtn2),
- BADGE_TOP_RIGHT,
- "The notification badge is showing in the top right in the second window"
- );
-
- CustomizableUI.reset();
- await BrowserTestUtils.closeWindow(win1);
- await BrowserTestUtils.closeWindow(win2);
-
- sandbox.restore();
-});
diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
index 1bf387f578..872efd37a0 100644
--- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
@@ -13,6 +13,11 @@ add_setup(async function () {
registerCleanupFunction(async function () {
await tearDown(gSandbox);
});
+
+ // set tab sync false so we don't skip setup states
+ await SpecialPowers.pushPrefEnv({
+ set: [["services.sync.engine.tabs", false]],
+ });
});
async function promiseTabListsUpdated({ tabLists }) {
@@ -748,3 +753,158 @@ add_task(async function search_synced_tabs_recent_browsing() {
await SpecialPowers.popPrefEnv();
await tearDown(sandbox);
});
+
+add_task(async function test_mobile_connected() {
+ Services.prefs.setBoolPref("services.sync.engine.tabs", false);
+ const sandbox = setupMocks({
+ state: UIState.STATUS_SIGNED_IN,
+ fxaDevices: [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ tabs: [],
+ },
+ {
+ id: 2,
+ name: "Other Device",
+ type: "mobile",
+ tabs: [],
+ },
+ ],
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ // ensure tab sync is false so we don't skip onto next step
+ ok(
+ !Services.prefs.getBoolPref("services.sync.engine.tabs", false),
+ "services.sync.engine.tabs is initially false"
+ );
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ await navigateToViewAndWait(document, "syncedtabs");
+
+ is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
+ ok(
+ fxAccounts.device.recentDeviceList?.some(
+ device => device.type == "mobile"
+ ),
+ "A connected device is type:mobile"
+ );
+ });
+ await tearDown(sandbox);
+ Services.prefs.setBoolPref("services.sync.engine.tabs", true);
+});
+
+add_task(async function test_tablet_connected() {
+ Services.prefs.setBoolPref("services.sync.engine.tabs", false);
+ const sandbox = setupMocks({
+ state: UIState.STATUS_SIGNED_IN,
+ fxaDevices: [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ tabs: [],
+ },
+ {
+ id: 2,
+ name: "Other Device",
+ type: "tablet",
+ tabs: [],
+ },
+ ],
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ // ensure tab sync is false so we don't skip onto next step
+ ok(
+ !Services.prefs.getBoolPref("services.sync.engine.tabs", false),
+ "services.sync.engine.tabs is initially false"
+ );
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ await navigateToViewAndWait(document, "syncedtabs");
+
+ is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
+ ok(
+ fxAccounts.device.recentDeviceList?.some(
+ device => device.type == "tablet"
+ ),
+ "A connected device is type:tablet"
+ );
+ });
+ await tearDown(sandbox);
+ Services.prefs.setBoolPref("services.sync.engine.tabs", true);
+});
+
+add_task(async function test_tab_sync_enabled() {
+ const sandbox = setupMocks({
+ state: UIState.STATUS_SIGNED_IN,
+ fxaDevices: [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ tabs: [],
+ },
+ {
+ id: 2,
+ name: "Other Device",
+ type: "mobile",
+ tabs: [],
+ },
+ ],
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+
+ // test initial state, with the pref not enabled
+ await navigateToViewAndWait(document, "syncedtabs");
+ // test with the pref toggled on
+ Services.prefs.setBoolPref("services.sync.engine.tabs", true);
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "Synced tabs component is fully updated."
+ );
+ ok(!syncedTabsComponent.emptyState, "No empty state is being displayed.");
+
+ // reset and test clicking the action button
+ Services.prefs.setBoolPref("services.sync.engine.tabs", false);
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "Synced tabs component is fully updated."
+ );
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.emptyState,
+ "The empty state is rendered."
+ );
+
+ const actionButton = syncedTabsComponent.emptyState?.querySelector(
+ "button[data-action=sync-tabs-disabled]"
+ );
+ EventUtils.synthesizeMouseAtCenter(actionButton, {}, browser.contentWindow);
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "Synced tabs component is fully updated."
+ );
+ await TestUtils.waitForCondition(
+ () => !syncedTabsComponent.emptyState,
+ "The empty state is rendered."
+ );
+
+ ok(true, "The empty state is no longer displayed when sync is enabled");
+ ok(
+ Services.prefs.getBoolPref("services.sync.engine.tabs", false),
+ "tab sync pref should be enabled after button click"
+ );
+ });
+ await tearDown(sandbox);
+});
diff --git a/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html b/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html
index 52ddc277c7..abea8725ee 100644
--- a/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html
+++ b/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html
@@ -37,8 +37,8 @@
const { BrowserTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/BrowserTestUtils.sys.mjs"
);
- const { FirefoxViewPlacesQuery } = ChromeUtils.importESModule(
- "resource:///modules/firefox-view-places-query.sys.mjs"
+ const { PlacesQuery } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesQuery.sys.mjs"
);
const { PlacesUtils } = ChromeUtils.importESModule(
"resource://gre/modules/PlacesUtils.sys.mjs"
@@ -52,7 +52,7 @@
const fxviewTabList = document.querySelector("fxview-tab-list");
let tabItems = [];
- const placesQuery = new FirefoxViewPlacesQuery();
+ const placesQuery = new PlacesQuery();
const URLs = [
"http://mochi.test:8888/browser/",
@@ -106,7 +106,20 @@
});
await historyUpdated.promise;
- fxviewTabList.tabItems = [...history.values()].flat();
+ fxviewTabList.tabItems = Array.from(history.values()).flat().map(visit => ({
+ ...visit,
+ time: visit.date.getTime(),
+ title: visit.title || visit.url,
+ icon: `page-icon:${visit.url}`,
+ primaryL10nId: "fxviewtabrow-tabs-list-tab",
+ primaryL10nArgs: JSON.stringify({
+ targetURI: visit.url,
+ }),
+ secondaryL10nId: "fxviewtabrow-options-menu-button",
+ secondaryL10nArgs: JSON.stringify({
+ tabTitle: visit.title || visit.url,
+ }),
+ }));
await fxviewTabList.getUpdateComplete();
tabItems = Array.from(fxviewTabList.rowEls);