diff options
Diffstat (limited to 'browser/components/firefoxview')
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); |