diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:27 +0000 |
commit | 40a355a42d4a9444dc753c04c6608dade2f06a23 (patch) | |
tree | 871fc667d2de662f171103ce5ec067014ef85e61 /browser/components/firefoxview | |
parent | Adding upstream version 124.0.1. (diff) | |
download | firefox-40a355a42d4a9444dc753c04c6608dade2f06a23.tar.xz firefox-40a355a42d4a9444dc753c04c6608dade2f06a23.zip |
Adding upstream version 125.0.1.upstream/125.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/firefoxview')
48 files changed, 2535 insertions, 1605 deletions
diff --git a/browser/components/firefoxview/OpenTabs.sys.mjs b/browser/components/firefoxview/OpenTabs.sys.mjs index ac247f5e8f..0771bf9e65 100644 --- a/browser/components/firefoxview/OpenTabs.sys.mjs +++ b/browser/components/firefoxview/OpenTabs.sys.mjs @@ -36,6 +36,8 @@ const TAB_RECENCY_CHANGE_EVENTS = Object.freeze([ "TabAttrModified", "TabClose", "TabOpen", + "TabPinned", + "TabUnpinned", "TabSelect", "TabAttrModified", ]); @@ -329,17 +331,21 @@ class OpenTabsTarget extends EventTarget { return []; } + /** + * Get an aggregated list of tabs from all the same-privateness browser windows. + * + * @returns {MozTabbrowserTab[]} + */ + getAllTabs() { + return this.currentWindows.flatMap(win => this.getTabsForWindow(win)); + } + /* * @returns {Array<Tab>} * A by-recency-sorted, aggregated list of tabs from all the same-privateness browser windows. */ getRecentTabs() { - const tabs = []; - for (let win of this.currentWindows) { - tabs.push(...this.getTabsForWindow(win)); - } - tabs.sort(lastSeenActiveSort); - return tabs; + return this.getAllTabs().sort(lastSeenActiveSort); } handleEvent({ detail, target, type }) { diff --git a/browser/components/firefoxview/content/category-history.svg b/browser/components/firefoxview/content/view-history.svg index a6dc259483..a6dc259483 100644 --- a/browser/components/firefoxview/content/category-history.svg +++ b/browser/components/firefoxview/content/view-history.svg diff --git a/browser/components/firefoxview/content/category-opentabs.svg b/browser/components/firefoxview/content/view-opentabs.svg index 2172558a42..2172558a42 100644 --- a/browser/components/firefoxview/content/category-opentabs.svg +++ b/browser/components/firefoxview/content/view-opentabs.svg diff --git a/browser/components/firefoxview/content/category-recentbrowsing.svg b/browser/components/firefoxview/content/view-recentbrowsing.svg index f4c523dafa..f4c523dafa 100644 --- a/browser/components/firefoxview/content/category-recentbrowsing.svg +++ b/browser/components/firefoxview/content/view-recentbrowsing.svg diff --git a/browser/components/firefoxview/content/category-recentlyclosed.svg b/browser/components/firefoxview/content/view-recentlyclosed.svg index 7cac65ac58..7cac65ac58 100644 --- a/browser/components/firefoxview/content/category-recentlyclosed.svg +++ b/browser/components/firefoxview/content/view-recentlyclosed.svg diff --git a/browser/components/firefoxview/content/category-syncedtabs.svg b/browser/components/firefoxview/content/view-syncedtabs.svg index bd9749743c..bd9749743c 100644 --- a/browser/components/firefoxview/content/category-syncedtabs.svg +++ b/browser/components/firefoxview/content/view-syncedtabs.svg diff --git a/browser/components/firefoxview/firefoxview.css b/browser/components/firefoxview/firefoxview.css index 48cf5a9490..6811ca54c4 100644 --- a/browser/components/firefoxview/firefoxview.css +++ b/browser/components/firefoxview/firefoxview.css @@ -16,6 +16,7 @@ --fxview-text-color-hover: var(--newtab-text-primary-color); --fxview-primary-action-background: var(--newtab-primary-action-background, #0061e0); --fxview-border: var(--fc-border-light, #CFCFD8); + --fxview-indicator-stroke-color-hover: #DEDDDE; /* ensure utility button hover states match those of the rest of the page */ --in-content-button-background-hover: var(--fxview-element-background-hover); @@ -37,6 +38,7 @@ --fxview-element-background-hover: color-mix(in srgb, var(--fxview-background-color) 80%, white); --fxview-element-background-active: color-mix(in srgb, var(--fxview-background-color) 60%, white); --fxview-border: #8F8F9D; + --fxview-indicator-stroke-color-hover:#5D5C66; /* copy over newtab colors from activity-stream-[os].css files */ --newtab-background-color: #2B2A33; @@ -69,6 +71,10 @@ body { grid-template-columns: var(--fxview-sidebar-width) 1fr; background-color: var(--fxview-background-color); color: var(--fxview-text-primary-color); + + @media (max-width: 52rem) { + display: flex; + } } .main-container { @@ -88,34 +94,6 @@ body { margin: 0; } -fxview-category-button:focus-visible { - outline-offset: var(--in-content-focus-outline-inset); -} - -fxview-category-button[name="recentbrowsing"]::part(icon) { - background-image: url("chrome://browser/content/firefoxview/category-recentbrowsing.svg"); -} -fxview-category-button[name="opentabs"]::part(icon) { - background-image: url("chrome://browser/content/firefoxview/category-opentabs.svg"); -} -fxview-category-button[name="recentlyclosed"]::part(icon) { - background-image: url("chrome://browser/content/firefoxview/category-recentlyclosed.svg"); -} -fxview-category-button[name="syncedtabs"]::part(icon) { - background-image: url("chrome://browser/content/firefoxview/category-syncedtabs.svg"); -} -fxview-category-button[name="history"]::part(icon) { - background-image: url("chrome://browser/content/firefoxview/category-history.svg"); -} - -fxview-tab-list.with-dismiss-button::part(secondary-button) { - background-image: url("chrome://global/skin/icons/close.svg"); -} - -fxview-tab-list.with-context-menu::part(secondary-button) { - background-image: url("chrome://global/skin/icons/more.svg"); -} - .sticky-container { position: sticky; top: 0; @@ -170,18 +148,6 @@ panel-item::part(button):hover:active { background-color: var(--fxview-element-background-active); } -panel-list { - overflow-y: visible; -} - -fxview-category-navigation { - overflow-y: auto; -} - -fxview-category-navigation h1 { - margin-block: 0; -} - fxview-empty-state:not([isSelectedTab]) button[slot="primary-action"] { margin-inline-start: 0; } diff --git a/browser/components/firefoxview/firefoxview.html b/browser/components/firefoxview/firefoxview.html index 1f53a1d0c9..6fa0f59a8f 100644 --- a/browser/components/firefoxview/firefoxview.html +++ b/browser/components/firefoxview/firefoxview.html @@ -15,8 +15,6 @@ <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="branding/brand.ftl" /> - <link rel="localization" href="toolkit/branding/accounts.ftl" /> <link rel="localization" href="toolkit/branding/brandings.ftl" /> <link rel="localization" href="browser/migrationWizard.ftl" /> <link @@ -41,54 +39,51 @@ ></script> <script type="module" - src="chrome://browser/content/firefoxview/fxview-category-navigation.mjs" + src="chrome://browser/content/firefoxview/syncedtabs.mjs" ></script> <script type="module" - src="chrome://browser/content/firefoxview/syncedtabs.mjs" + src="chrome://global/content/elements/moz-page-nav.mjs" ></script> <script src="chrome://browser/content/contentTheme.js"></script> </head> <body> - <fxview-category-navigation> - <h1 slot="category-nav-header" data-l10n-id="firefoxview-page-title"></h1> - <fxview-category-button - class="category" - slot="category-button" - name="recentbrowsing" + <moz-page-nav + data-l10n-id="firefoxview-page-heading" + data-l10n-attrs="heading" + > + <moz-page-nav-button + view="recentbrowsing" data-l10n-id="firefoxview-overview-nav" + iconSrc="chrome://browser/content/firefoxview/view-recentbrowsing.svg" > - </fxview-category-button> - <fxview-category-button - class="category" - slot="category-button" - name="opentabs" + </moz-page-nav-button> + <moz-page-nav-button + view="opentabs" data-l10n-id="firefoxview-opentabs-nav" + iconSrc="chrome://browser/content/firefoxview/view-opentabs.svg" > - </fxview-category-button> - <fxview-category-button - class="category" - slot="category-button" - name="recentlyclosed" + </moz-page-nav-button> + <moz-page-nav-button + view="recentlyclosed" data-l10n-id="firefoxview-recently-closed-nav" + iconSrc="chrome://browser/content/firefoxview/view-recentlyclosed.svg" > - </fxview-category-button> - <fxview-category-button - class="category" - slot="category-button" - name="syncedtabs" + </moz-page-nav-button> + <moz-page-nav-button + view="syncedtabs" data-l10n-id="firefoxview-synced-tabs-nav" + iconSrc="chrome://browser/content/firefoxview/view-syncedtabs.svg" > - </fxview-category-button> - <fxview-category-button - class="category" - slot="category-button" - name="history" + </moz-page-nav-button> + <moz-page-nav-button + view="history" data-l10n-id="firefoxview-history-nav" + iconSrc="chrome://browser/content/firefoxview/view-history.svg" > - </fxview-category-button> - </fxview-category-navigation> + </moz-page-nav-button> + </moz-page-nav> <main id="pages" role="application" data-l10n-id="firefoxview-page-label"> <div class="main-container"> <named-deck> diff --git a/browser/components/firefoxview/firefoxview.mjs b/browser/components/firefoxview/firefoxview.mjs index 77f4c06cc7..3e61482cc0 100644 --- a/browser/components/firefoxview/firefoxview.mjs +++ b/browser/components/firefoxview/firefoxview.mjs @@ -3,35 +3,29 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ let pageList = []; -let categoryPagesDeck = null; -let categoryNavigation = null; +let viewsDeck = null; +let pageNav = null; let activeComponent = null; let searchKeyboardShortcut = null; const { topChromeWindow } = window.browsingContext; function onHashChange() { - let page = document.location?.hash.substring(1); - if (!page || !pageList.includes(page)) { - page = "recentbrowsing"; + let view = document.location?.hash.substring(1); + if (!view || !pageList.includes(view)) { + view = "recentbrowsing"; } - changePage(page); + changeView(view); } -function changePage(page) { - categoryPagesDeck.selectedViewName = page; - categoryNavigation.currentCategory = page; - if (categoryNavigation.categoryButtons.includes(document.activeElement)) { - let currentCategoryButton = categoryNavigation.categoryButtons.find( - categoryButton => categoryButton.name === page - ); - (currentCategoryButton || categoryNavigation.categoryButtons[0]).focus(); - } +function changeView(view) { + viewsDeck.selectedViewName = view; + pageNav.currentView = view; } -function onPagesDeckViewChange() { - for (const child of categoryPagesDeck.children) { - if (child.getAttribute("name") == categoryPagesDeck.selectedViewName) { +function onViewsDeckViewChange() { + for (const child of viewsDeck.children) { + if (child.getAttribute("name") == viewsDeck.selectedViewName) { child.enter(); activeComponent = child; } else { @@ -41,11 +35,11 @@ function onPagesDeckViewChange() { } function recordNavigationTelemetry(source, eventTarget) { - let page = "recentbrowsing"; + let view = "recentbrowsing"; if (source === "category-navigation") { - page = eventTarget.parentNode.currentCategory; + view = eventTarget.parentNode.currentView; } else if (source === "view-all") { - page = eventTarget.shortPageName; + view = eventTarget.shortPageName; } // Record telemetry Services.telemetry.recordEvent( @@ -54,7 +48,7 @@ function recordNavigationTelemetry(source, eventTarget) { "navigation", null, { - page, + page: view, source, } ); @@ -73,7 +67,7 @@ async function updateSearchTextboxSize() { const placeholder = msg.attributes[0].value; maxLength = Math.max(maxLength, placeholder.length); } - for (const child of categoryPagesDeck.children) { + for (const child of viewsDeck.children) { child.searchTextboxSize = maxLength; } } @@ -89,15 +83,15 @@ async function updateSearchKeyboardShortcut() { window.addEventListener("DOMContentLoaded", async () => { recordEnteredTelemetry(); - categoryNavigation = document.querySelector("fxview-category-navigation"); - categoryPagesDeck = document.querySelector("named-deck"); + pageNav = document.querySelector("moz-page-nav"); + viewsDeck = document.querySelector("named-deck"); - for (const item of categoryNavigation.categoryButtons) { - pageList.push(item.getAttribute("name")); + for (const item of pageNav.pageNavButtons) { + pageList.push(item.getAttribute("view")); } window.addEventListener("hashchange", onHashChange); - window.addEventListener("change-category", function (event) { - location.hash = event.target.getAttribute("name"); + window.addEventListener("change-view", function (event) { + location.hash = event.target.getAttribute("view"); window.scrollTo(0, 0); recordNavigationTelemetry("category-navigation", event.target); }); @@ -105,11 +99,11 @@ window.addEventListener("DOMContentLoaded", async () => { recordNavigationTelemetry("view-all", event.originalTarget); }); - categoryPagesDeck.addEventListener("view-changed", onPagesDeckViewChange); + viewsDeck.addEventListener("view-changed", onViewsDeckViewChange); // set the initial state onHashChange(); - onPagesDeckViewChange(); + onViewsDeckViewChange(); await updateSearchTextboxSize(); await updateSearchKeyboardShortcut(); diff --git a/browser/components/firefoxview/fxview-category-button.css b/browser/components/firefoxview/fxview-category-button.css deleted file mode 100644 index 1bce29f343..0000000000 --- a/browser/components/firefoxview/fxview-category-button.css +++ /dev/null @@ -1,125 +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/. */ - -:host { - border-radius: 4px; -} - -button { - background-color: initial; - border: 1px solid var(--in-content-primary-button-border-color); - -moz-context-properties: fill, fill-opacity; - fill: currentColor; - display: grid; - grid-template-columns: min-content 1fr; - gap: 12px; - align-items: center; - font-size: inherit; - width: 100%; - font-weight: normal; - border-radius: 4px; - color: inherit; - text-align: start; - transition: background-color 150ms; - padding: var(--fxviewcategorynav-button-padding); -} - -button:hover { - cursor: pointer; -} - -@media not (prefers-contrast) { - button { - border-inline-start: 2px solid transparent; - border-inline-end: none; - border-block: none; - } - - button:hover, - button[selected]:hover { - background-color: var(--in-content-button-background-hover); - border-color: var(--in-content-button-border-color-hover); - } - - button[selected]:hover { - border-inline-start-color: inherit; - } - - button[selected], - button[selected]:hover { - border-inline-start: 2px solid; - } - - button[selected]:not(:focus-visible) { - border-start-start-radius: 0; - border-end-start-radius: 0; - } - - button[selected]:not(:hover) { - color: var(--in-content-accent-color); - background-color: color-mix(in srgb, var(--fxview-primary-action-background) 5%, transparent); - border-inline-start-color: var(--in-content-accent-color); - } -} - -@media (prefers-color-scheme: dark) { - button[selected] { - background-color: color-mix(in srgb, var(--fxview-primary-action-background) 12%, transparent); - } -} - -button:focus-visible, -button[selected]:focus-visible { - outline: var(--focus-outline); - outline-offset: var(--focus-outline-offset); -} - -.category-icon { - background-color: initial; - background-size: 20px; - background-repeat: no-repeat; - background-position: center; - height: 20px; - width: 20px; - -moz-context-properties: fill; - fill: currentColor; -} - -@media (prefers-contrast) { - button { - transition: none; - border-color: ButtonText; - background-color: var(--in-content-button-background); - } - - button:hover { - color: SelectedItem; - } - - button[selected] { - color: SelectedItemText; - background-color: SelectedItem; - border-color: SelectedItem; - } -} - -slot { - font-size: 1.13em; - line-height: 1.4; - margin: 0; - padding-inline-start: 0; - user-select: none; -} - -@media (max-width: 52rem) { - button { - grid-template-columns: min-content; - justify-content: center; - margin-inline: 0; - } - - slot { - display: none; - } -} diff --git a/browser/components/firefoxview/fxview-category-navigation.css b/browser/components/firefoxview/fxview-category-navigation.css deleted file mode 100644 index 571059699b..0000000000 --- a/browser/components/firefoxview/fxview-category-navigation.css +++ /dev/null @@ -1,60 +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/. */ - -:host { - --fxviewcategorynav-button-padding: 8px; - margin-inline-start: 42px; - position: sticky; - top: 0; - height: 100vh; -} - -nav { - display: grid; - grid-template-rows: min-content 1fr auto; - gap: 25px; - margin-block-start: var(--fxview-margin-top); -} - -.category-nav-header { - /* Align the header text/icon with the category button icons */ - margin-inline-start: var(--fxviewcategorynav-button-padding); -} - -.category-nav-buttons, -::slotted(.category-nav-footer) { - display: grid; - grid-template-columns: 1fr; - grid-auto-rows: min-content; - gap: 4px; -} - -@media (prefers-contrast) { - .category-nav-buttons { - gap: 8px; - } -} - -@media (prefers-reduced-motion) { - /* (See Bug 1610081) Setting border-inline-end to add clear differentiation between side navigation and main content area */ - :host { - border-inline-end: 1px solid var(--in-content-border-color); - } -} - -@media (max-width: 52rem) { - :host { - grid-template-rows: 1fr auto; - } - - .category-nav-header { - display: none; - } - - .category-nav-buttons, - ::slotted(.category-nav-footer) { - justify-content: center; - grid-template-columns: min-content; - } -} diff --git a/browser/components/firefoxview/fxview-category-navigation.mjs b/browser/components/firefoxview/fxview-category-navigation.mjs deleted file mode 100644 index abacd17df1..0000000000 --- a/browser/components/firefoxview/fxview-category-navigation.mjs +++ /dev/null @@ -1,150 +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 { html } from "chrome://global/content/vendor/lit.all.mjs"; -import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; - -export default class FxviewCategoryNavigation extends MozLitElement { - static properties = { - currentCategory: { type: String }, - }; - - static queries = { - categoryButtonsSlot: "slot[name=category-button]", - }; - - get categoryButtons() { - return this.categoryButtonsSlot - .assignedNodes() - .filter(node => !node.hidden); - } - - onChangeCategory(e) { - this.currentCategory = e.target.name; - } - - handleFocus(e) { - if (e.key == "ArrowDown" || e.key == "ArrowRight") { - e.preventDefault(); - this.focusNextCategory(); - } else if (e.key == "ArrowUp" || e.key == "ArrowLeft") { - e.preventDefault(); - this.focusPreviousCategory(); - } - } - - focusPreviousCategory() { - let categoryButtons = this.categoryButtons; - let currentIndex = categoryButtons.findIndex(b => b.selected); - let prev = categoryButtons[currentIndex - 1]; - if (prev) { - prev.activate(); - prev.focus(); - } - } - - focusNextCategory() { - let categoryButtons = this.categoryButtons; - let currentIndex = categoryButtons.findIndex(b => b.selected); - let next = categoryButtons[currentIndex + 1]; - if (next) { - next.activate(); - next.focus(); - } - } - - render() { - return html` - <link - rel="stylesheet" - href="chrome://browser/content/firefoxview/fxview-category-navigation.css" - /> - <nav> - <div class="category-nav-header"> - <slot name="category-nav-header"></slot> - </div> - <div - class="category-nav-buttons" - role="tablist" - aria-orientation="vertical" - > - <slot - name="category-button" - @change-category=${this.onChangeCategory} - @keydown=${this.handleFocus} - ></slot> - </div> - <div class="category-nav-footer"> - <slot name="category-nav-footer"></slot> - </div> - </nav> - `; - } - - updated() { - let categorySelected = false; - let assignedCategories = this.categoryButtons; - for (let button of assignedCategories) { - button.selected = button.name == this.currentCategory; - categorySelected = categorySelected || button.selected; - } - if (!categorySelected && assignedCategories.length) { - // Current category has no matching category, reset to the first category. - assignedCategories[0].activate(); - } - } -} -customElements.define("fxview-category-navigation", FxviewCategoryNavigation); - -export class FxviewCategoryButton extends MozLitElement { - static properties = { - selected: { type: Boolean }, - }; - - static queries = { - buttonEl: "button", - }; - - connectedCallback() { - super.connectedCallback(); - this.setAttribute("role", "tab"); - } - - get name() { - return this.getAttribute("name"); - } - - activate() { - this.dispatchEvent( - new CustomEvent("change-category", { - bubbles: true, - composed: true, - }) - ); - } - - render() { - return html` - <link - rel="stylesheet" - href="chrome://browser/content/firefoxview/fxview-category-button.css" - /> - <button - aria-hidden="true" - tabindex="-1" - ?selected=${this.selected} - @click=${this.activate} - > - <span class="category-icon" part="icon"></span> - <slot></slot> - </button> - `; - } - - updated() { - this.setAttribute("aria-selected", this.selected); - this.setAttribute("tabindex", this.selected ? 0 : -1); - } -} -customElements.define("fxview-category-button", FxviewCategoryButton); diff --git a/browser/components/firefoxview/fxview-tab-list.css b/browser/components/firefoxview/fxview-tab-list.css index d32d9c9c08..5a4bff023a 100644 --- a/browser/components/firefoxview/fxview-tab-list.css +++ b/browser/components/firefoxview/fxview-tab-list.css @@ -2,14 +2,33 @@ * 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/. */ - .fxview-tab-list { +:host { display: grid; - grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content; - gap: 6px; + row-gap: var(--space-xsmall); } -:host([compactRows]) .fxview-tab-list { - grid-template-columns: min-content 1fr min-content min-content min-content; +.fxview-tab-list { + display: grid; + grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content; + gap: var(--space-xsmall); + + &.pinned { + display: flex; + flex-wrap: wrap; + + > virtual-list { + display: block; + } + + > fxview-tab-row { + display: block; + margin-block-end: var(--space-xsmall); + } + } + + :host([compactRows]) & { + grid-template-columns: min-content 1fr min-content min-content min-content; + } } virtual-list { diff --git a/browser/components/firefoxview/fxview-tab-list.mjs b/browser/components/firefoxview/fxview-tab-list.mjs index 055540722a..978ab79724 100644 --- a/browser/components/firefoxview/fxview-tab-list.mjs +++ b/browser/components/firefoxview/fxview-tab-list.mjs @@ -45,8 +45,11 @@ if (!window.IS_STORYBOOK) { * @property {string} dateTimeFormat - Expected format for date and/or time * @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required * @property {number} maxTabsLength - The max number of tabs for the list + * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view * @property {Array} tabItems - Items to show in the tab list * @property {string} searchQuery - The query string to highlight, if provided. + * @property {string} secondaryActionClass - The class used to style the secondary action element + * @property {string} tertiaryActionClass - The class used to style the tertiary action element */ export default class FxviewTabList extends MozLitElement { constructor() { @@ -59,6 +62,9 @@ export default class FxviewTabList extends MozLitElement { this.dateTimeFormat = "relative"; this.maxTabsLength = 25; this.tabItems = []; + this.pinnedTabs = []; + this.pinnedTabsGridView = false; + this.unpinnedTabs = []; this.compactRows = false; this.updatesPaused = true; this.#register(); @@ -71,9 +77,12 @@ export default class FxviewTabList extends MozLitElement { dateTimeFormat: { type: String }, hasPopup: { type: String }, maxTabsLength: { type: Number }, + pinnedTabsGridView: { type: Boolean }, tabItems: { type: Array }, updatesPaused: { type: Boolean }, searchQuery: { type: String }, + secondaryActionClass: { type: String }, + tertiaryActionClass: { type: String }, }; static queries = { @@ -99,8 +108,20 @@ export default class FxviewTabList extends MozLitElement { } } - if (this.maxTabsLength > 0) { + // Move pinned tabs to the beginning of the list + if (this.pinnedTabsGridView) { // Can set maxTabsLength to -1 to have no max + this.unpinnedTabs = this.tabItems.filter( + tab => !tab.indicators?.includes("pinned") + ); + this.pinnedTabs = this.tabItems.filter(tab => + tab.indicators?.includes("pinned") + ); + if (this.maxTabsLength > 0) { + this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength); + } + this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs]; + } else if (this.maxTabsLength > 0) { this.tabItems = this.tabItems.slice(0, this.maxTabsLength); } } @@ -176,56 +197,93 @@ export default class FxviewTabList extends MozLitElement { if (e.code == "ArrowUp") { // Focus either the link or button of the previous row based on this.currentActiveElementId e.preventDefault(); - this.focusPrevRow(); + if ( + (this.pinnedTabsGridView && + this.activeIndex >= this.pinnedTabs.length) || + !this.pinnedTabsGridView + ) { + this.focusPrevRow(); + } } else if (e.code == "ArrowDown") { // Focus either the link or button of the next row based on this.currentActiveElementId e.preventDefault(); - this.focusNextRow(); + if ( + this.pinnedTabsGridView && + this.activeIndex < this.pinnedTabs.length + ) { + this.focusIndex(this.pinnedTabs.length); + } else { + this.focusNextRow(); + } } else if (e.code == "ArrowRight") { // Focus either the link or the button in the current row and // set this.currentActiveElementId to that element's ID e.preventDefault(); if (document.dir == "rtl") { - if ( - (fxviewTabRow.soundPlaying || fxviewTabRow.muted) && - this.currentActiveElementId === "fxview-tab-row-secondary-button" - ) { - this.currentActiveElementId = fxviewTabRow.focusMediaButton(); - } else { - this.currentActiveElementId = fxviewTabRow.focusLink(); - } - } else if ( - (fxviewTabRow.soundPlaying || fxviewTabRow.muted) && - this.currentActiveElementId === "fxview-tab-row-main" - ) { - this.currentActiveElementId = fxviewTabRow.focusMediaButton(); + this.moveFocusLeft(fxviewTabRow); } else { - this.currentActiveElementId = fxviewTabRow.focusButton(); + this.moveFocusRight(fxviewTabRow); } } else if (e.code == "ArrowLeft") { // Focus either the link or the button in the current row and // set this.currentActiveElementId to that element's ID e.preventDefault(); if (document.dir == "rtl") { - if ( - (fxviewTabRow.soundPlaying || fxviewTabRow.muted) && - this.currentActiveElementId === "fxview-tab-row-main" - ) { - this.currentActiveElementId = fxviewTabRow.focusMediaButton(); - } else { - this.currentActiveElementId = fxviewTabRow.focusButton(); - } - } else if ( - (fxviewTabRow.soundPlaying || fxviewTabRow.muted) && - this.currentActiveElementId === "fxview-tab-row-secondary-button" - ) { - this.currentActiveElementId = fxviewTabRow.focusMediaButton(); + this.moveFocusRight(fxviewTabRow); } else { - this.currentActiveElementId = fxviewTabRow.focusLink(); + this.moveFocusLeft(fxviewTabRow); } } } + moveFocusRight(fxviewTabRow) { + if ( + this.pinnedTabsGridView && + fxviewTabRow.indicators?.includes("pinned") + ) { + this.focusNextRow(); + } else if ( + (fxviewTabRow.indicators?.includes("soundplaying") || + fxviewTabRow.indicators?.includes("muted")) && + this.currentActiveElementId === "fxview-tab-row-main" + ) { + this.currentActiveElementId = fxviewTabRow.focusMediaButton(); + } else if ( + this.currentActiveElementId === "fxview-tab-row-media-button" || + this.currentActiveElementId === "fxview-tab-row-main" + ) { + this.currentActiveElementId = fxviewTabRow.focusSecondaryButton(); + } else if ( + fxviewTabRow.tertiaryButtonEl && + this.currentActiveElementId === "fxview-tab-row-secondary-button" + ) { + this.currentActiveElementId = fxviewTabRow.focusTertiaryButton(); + } + } + + moveFocusLeft(fxviewTabRow) { + if ( + this.pinnedTabsGridView && + (fxviewTabRow.indicators?.includes("pinned") || + (this.currentActiveElementId === "fxview-tab-row-main" && + this.activeIndex === this.pinnedTabs.length)) + ) { + this.focusPrevRow(); + } else if ( + this.currentActiveElementId === "fxview-tab-row-tertiary-button" + ) { + this.currentActiveElementId = fxviewTabRow.focusSecondaryButton(); + } else if ( + (fxviewTabRow.indicators?.includes("soundplaying") || + fxviewTabRow.indicators?.includes("muted")) && + this.currentActiveElementId === "fxview-tab-row-secondary-button" + ) { + this.currentActiveElementId = fxviewTabRow.focusMediaButton(); + } else { + this.currentActiveElementId = fxviewTabRow.focusLink(); + } + } + focusPrevRow() { this.focusIndex(this.activeIndex - 1); } @@ -236,12 +294,18 @@ export default class FxviewTabList extends MozLitElement { async focusIndex(index) { // Focus link or button of item - if (lazy.virtualListEnabledPref) { - let row = this.rootVirtualListEl.getItem(index); + if ( + ((this.pinnedTabsGridView && index > this.pinnedTabs.length) || + !this.pinnedTabsGridView) && + lazy.virtualListEnabledPref + ) { + let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length); if (!row) { return; } - let subList = this.rootVirtualListEl.getSubListForItem(index); + let subList = this.rootVirtualListEl.getSubListForItem( + index - this.pinnedTabs.length + ); if (!subList) { return; } @@ -286,23 +350,30 @@ export default class FxviewTabList extends MozLitElement { return html` <fxview-tab-row exportparts="secondary-button" + class=${classMap({ + pinned: + this.pinnedTabsGridView && tabItem.indicators?.includes("pinned"), + })} ?active=${i == this.activeIndex} ?compact=${this.compactRows} .hasPopup=${this.hasPopup} - .containerObj=${tabItem.containerObj} + .containerObj=${ifDefined(tabItem.containerObj)} .currentActiveElementId=${this.currentActiveElementId} .dateTimeFormat=${this.dateTimeFormat} .favicon=${tabItem.icon} - .isBookmark=${ifDefined(tabItem.isBookmark)} - .muted=${ifDefined(tabItem.muted)} - .pinned=${ifDefined(tabItem.pinned)} + .indicators=${ifDefined(tabItem.indicators)} + .pinnedTabsGridView=${ifDefined(this.pinnedTabsGridView)} .primaryL10nId=${tabItem.primaryL10nId} .primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)} - role="listitem" + role=${this.pinnedTabsGridView && tabItem.indicators?.includes("pinned") + ? "none" + : "listitem"} .secondaryL10nId=${tabItem.secondaryL10nId} .secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)} - .attention=${ifDefined(tabItem.attention)} - .soundPlaying=${ifDefined(tabItem.soundPlaying)} + .tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)} + .tertiaryL10nArgs=${ifDefined(tabItem.tertiaryL10nArgs)} + .secondaryActionClass=${this.secondaryActionClass} + .tertiaryActionClass=${ifDefined(this.tertiaryActionClass)} .sourceClosedId=${ifDefined(tabItem.sourceClosedId)} .sourceWindowId=${ifDefined(tabItem.sourceWindowId)} .closedId=${ifDefined(tabItem.closedId || tabItem.closedId)} @@ -311,7 +382,6 @@ export default class FxviewTabList extends MozLitElement { .time=${ifDefined(time)} .timeMsPref=${ifDefined(this.timeMsPref)} .title=${tabItem.title} - .titleChanged=${ifDefined(tabItem.titleChanged)} .url=${tabItem.url} ></fxview-tab-row> `; @@ -326,9 +396,26 @@ export default class FxviewTabList extends MozLitElement { rel="stylesheet" href="chrome://browser/content/firefoxview/fxview-tab-list.css" /> + ${when( + this.pinnedTabsGridView && this.pinnedTabs.length, + () => html` + <div + id="fxview-tab-list" + class="fxview-tab-list pinned" + data-l10n-id="firefoxview-pinned-tabs" + role="tablist" + @keydown=${this.handleFocusElementInRow} + > + ${this.pinnedTabs.map((tabItem, i) => + this.itemTemplate(tabItem, i) + )} + </div> + ` + )} <div id="fxview-tab-list" class="fxview-tab-list" + data-l10n-id="firefoxview-tabs" role="list" @keydown=${this.handleFocusElementInRow} > @@ -337,7 +424,12 @@ export default class FxviewTabList extends MozLitElement { () => html` <virtual-list .activeIndex=${this.activeIndex} - .items=${this.tabItems} + .pinnedTabsIndexOffset=${this.pinnedTabsGridView + ? this.pinnedTabs.length + : 0} + .items=${this.pinnedTabsGridView + ? this.unpinnedTabs + : this.tabItems} .template=${this.itemTemplate} ></virtual-list> ` @@ -374,23 +466,23 @@ customElements.define("fxview-tab-list", FxviewTabList); * @property {string} currentActiveElementId - ID of currently focused element within each tab item * @property {string} dateTimeFormat - Expected format for date and/or time * @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required - * @property {boolean} isBookmark - Whether an open tab is bookmarked + * @property {string} indicators - An array of tab indicators if any are present * @property {number} closedId - The tab ID for when the tab item was closed. * @property {number} sourceClosedId - The closedId of the closed window its from if applicable * @property {number} sourceWindowId - The sessionstore id of the window its from if applicable * @property {string} favicon - The favicon for the tab item. - * @property {boolean} muted - Whether an open tab is muted - * @property {boolean} pinned - Whether an open tab is pinned + * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view * @property {string} primaryL10nId - The l10n id used for the primary action element * @property {string} primaryL10nArgs - The l10n args used for the primary action element * @property {string} secondaryL10nId - The l10n id used for the secondary action button * @property {string} secondaryL10nArgs - The l10n args used for the secondary action element - * @property {boolean} attention - Whether to show a notification dot - * @property {boolean} soundPlaying - Whether an open tab has soundPlaying + * @property {string} secondaryActionClass - The class used to style the secondary action element + * @property {string} tertiaryL10nId - The l10n id used for the tertiary action button + * @property {string} tertiaryL10nArgs - The l10n args used for the tertiary action element + * @property {string} tertiaryActionClass - The class used to style the tertiary action element * @property {object} tabElement - The MozTabbrowserTab element for the tab item. * @property {number} time - The timestamp for when the tab was last accessed. * @property {string} title - The title for the tab item. - * @property {boolean} titleChanged - Whether the title has changed for an open tab * @property {string} url - The url for the tab item. * @property {number} timeMsPref - The frequency in milliseconds of updates to relative time * @property {string} searchQuery - The query string to highlight, if provided. @@ -410,31 +502,33 @@ export class FxviewTabRow extends MozLitElement { dateTimeFormat: { type: String }, favicon: { type: String }, hasPopup: { type: String }, - isBookmark: { type: Boolean }, - muted: { type: Boolean }, - pinned: { type: Boolean }, + indicators: { type: Array }, + pinnedTabsGridView: { type: Boolean }, primaryL10nId: { type: String }, primaryL10nArgs: { type: String }, secondaryL10nId: { type: String }, secondaryL10nArgs: { type: String }, - soundPlaying: { type: Boolean }, + secondaryActionClass: { type: String }, + tertiaryL10nId: { type: String }, + tertiaryL10nArgs: { type: String }, + tertiaryActionClass: { type: String }, closedId: { type: Number }, sourceClosedId: { type: Number }, sourceWindowId: { type: String }, tabElement: { type: Object }, time: { type: Number }, title: { type: String }, - titleChanged: { type: Boolean }, - attention: { type: Boolean }, timeMsPref: { type: Number }, url: { type: String }, searchQuery: { type: String }, }; static queries = { - mainEl: ".fxview-tab-row-main", - buttonEl: "#fxview-tab-row-secondary-button:not([hidden])", + mainEl: "#fxview-tab-row-main", + secondaryButtonEl: "#fxview-tab-row-secondary-button:not([hidden])", + tertiaryButtonEl: "#fxview-tab-row-tertiary-button", mediaButtonEl: "#fxview-tab-row-media-button", + pinnedTabButtonEl: "button#fxview-tab-row-main", }; get currentFocusable() { @@ -445,13 +539,40 @@ export class FxviewTabRow extends MozLitElement { return focusItem; } + connectedCallback() { + super.connectedCallback(); + this.addEventListener("keydown", this.handleKeydown); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.removeEventListener("keydown", this.handleKeydown); + } + + handleKeydown(e) { + if ( + this.active && + this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + e.key === "m" && + e.ctrlKey + ) { + this.muteOrUnmuteTab(); + } + } + focus() { this.currentFocusable.focus(); } - focusButton() { - this.buttonEl.focus(); - return this.buttonEl.id; + focusSecondaryButton() { + this.secondaryButtonEl.focus(); + return this.secondaryButtonEl.id; + } + + focusTertiaryButton() { + this.tertiaryButtonEl.focus(); + return this.tertiaryButtonEl.id; } focusMediaButton() { @@ -562,6 +683,9 @@ export class FxviewTabRow extends MozLitElement { secondaryActionHandler(event) { if ( + (this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + event.type == "contextmenu") || (event.type == "click" && event.detail && !event.altKey) || // detail=0 is from keyboard (event.type == "click" && !event.detail) @@ -577,12 +701,108 @@ export class FxviewTabRow extends MozLitElement { } } - muteOrUnmuteTab() { + tertiaryActionHandler(event) { + if ( + (event.type == "click" && event.detail && !event.altKey) || + // detail=0 is from keyboard + (event.type == "click" && !event.detail) + ) { + event.preventDefault(); + this.dispatchEvent( + new CustomEvent("fxview-tab-list-tertiary-action", { + bubbles: true, + composed: true, + detail: { originalEvent: event, item: this }, + }) + ); + } + } + + muteOrUnmuteTab(e) { + e?.preventDefault(); + // If the tab has no sound playing, the mute/unmute button will be removed when toggled. + // We should move the focus to the right in that case. This does not apply to pinned tabs + // on the Open Tabs page. + let shouldMoveFocus = + (!this.pinnedTabsGridView || + (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) && + this.mediaButtonEl && + !this.indicators.includes("soundplaying") && + this.currentActiveElementId === "fxview-tab-row-media-button"; + + // detail=0 is from keyboard + if (e?.type == "click" && !e?.detail && shouldMoveFocus) { + let tabList = this.getRootNode().host; + if (document.dir == "rtl") { + tabList.moveFocusLeft(this); + } else { + tabList.moveFocusRight(this); + } + } this.tabElement.toggleMuteAudio(); - this.muted = !this.muted; } - render() { + #faviconTemplate() { + return html`<span + class="${classMap({ + "fxview-tab-row-favicon-wrapper": true, + pinned: this.indicators?.includes("pinned"), + pinnedOnNewTab: this.indicators?.includes("pinnedOnNewTab"), + attention: this.indicators?.includes("attention"), + bookmark: this.indicators?.includes("bookmark"), + })}" + > + <span + class="fxview-tab-row-favicon icon" + id="fxview-tab-row-favicon" + style=${styleMap({ + backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`, + })} + ></span> + ${when( + this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + (this.indicators?.includes("muted") || + this.indicators?.includes("soundplaying")), + () => html` + <button + class="fxview-tab-row-pinned-media-button ghost-button icon-button" + id="fxview-tab-row-media-button" + tabindex="-1" + data-l10n-id=${this.indicators?.includes("muted") + ? "fxviewtabrow-unmute-tab-button-no-context" + : "fxviewtabrow-mute-tab-button-no-context"} + muted=${this.indicators?.includes("muted")} + soundplaying=${this.indicators?.includes("soundplaying") && + !this.indicators?.includes("muted")} + @click=${this.muteOrUnmuteTab} + ></button> + ` + )} + </span>`; + } + + #pinnedTabItemTemplate() { + return html` <button + class="fxview-tab-row-main ghost-button semi-transparent" + id="fxview-tab-row-main" + aria-haspopup=${ifDefined(this.hasPopup)} + data-l10n-id=${ifDefined(this.primaryL10nId)} + data-l10n-args=${ifDefined(this.primaryL10nArgs)} + tabindex=${this.active && + this.currentActiveElementId === "fxview-tab-row-main" + ? "0" + : "-1"} + role="tab" + @click=${this.primaryActionHandler} + @keydown=${this.primaryActionHandler} + @contextmenu=${this.secondaryActionHandler} + > + ${this.#faviconTemplate()} + </button>`; + } + + #unpinnedTabItemTemplate() { const title = this.title; const relativeString = this.relativeTime( this.time, @@ -598,25 +818,8 @@ export class FxviewTabRow extends MozLitElement { const timeString = this.timeFluentId(this.dateTimeFormat); const time = this.time; const timeArgs = JSON.stringify({ time }); - return html` - ${when( - this.containerObj, - () => html` - <link - rel="stylesheet" - href="chrome://browser/content/usercontext/usercontext.css" - /> - ` - )} - <link - rel="stylesheet" - href="chrome://global/skin/in-content/common.css" - /> - <link - rel="stylesheet" - href="chrome://browser/content/firefoxview/fxview-tab-row.css" - /> - <a + + return html`<a href=${ifDefined(this.url)} class="fxview-tab-row-main" id="fxview-tab-row-main" @@ -628,29 +831,9 @@ export class FxviewTabRow extends MozLitElement { data-l10n-args=${ifDefined(this.primaryL10nArgs)} @click=${this.primaryActionHandler} @keydown=${this.primaryActionHandler} + title=${!this.primaryL10nId ? this.url : null} > - <span - class="${classMap({ - "fxview-tab-row-favicon-wrapper": true, - bookmark: this.isBookmark && !this.attention, - notification: this.pinned - ? this.attention || this.titleChanged - : this.attention, - soundplaying: this.soundPlaying && !this.muted && this.pinned, - muted: this.muted && this.pinned, - })}" - > - <span - class="fxview-tab-row-favicon icon" - id="fxview-tab-row-favicon" - style=${styleMap({ - backgroundImage: `url(${this.getImageUrl( - this.favicon, - this.url - )})`, - })} - ></span> - </span> + ${this.#faviconTemplate()} <span class="fxview-tab-row-title text-truncated-ellipsis" id="fxview-tab-row-title" @@ -701,34 +884,42 @@ export class FxviewTabRow extends MozLitElement { </span> </a> ${when( - (this.soundPlaying || this.muted) && !this.pinned, + this.indicators?.includes("soundplaying") || + this.indicators?.includes("muted"), () => html`<button - class=fxview-tab-row-button ghost-button icon-button semi-transparent" - id="fxview-tab-row-media-button" - data-l10n-id=${ - this.muted - ? "fxviewtabrow-unmute-tab-button" - : "fxviewtabrow-mute-tab-button" - } - data-l10n-args=${JSON.stringify({ tabTitle: title })} - muted=${ifDefined(this.muted)} - soundplaying=${this.soundPlaying && !this.muted} - @click=${this.muteOrUnmuteTab} - tabindex="${ - this.active && - this.currentActiveElementId === "fxview-tab-row-media-button" - ? "0" - : "-1" - }" - ></button>`, + class=fxview-tab-row-button ghost-button icon-button semi-transparent" + id="fxview-tab-row-media-button" + data-l10n-id=${ + this.indicators?.includes("muted") + ? "fxviewtabrow-unmute-tab-button-no-context" + : "fxviewtabrow-mute-tab-button-no-context" + } + muted=${this.indicators?.includes("muted")} + soundplaying=${ + this.indicators?.includes("soundplaying") && + !this.indicators?.includes("muted") + } + @click=${this.muteOrUnmuteTab} + tabindex="${ + this.active && + this.currentActiveElementId === "fxview-tab-row-media-button" + ? "0" + : "-1" + }" + ></button>`, () => html`<span></span>` )} ${when( this.secondaryL10nId && this.secondaryActionHandler, () => html`<button - class="fxview-tab-row-button ghost-button icon-button semi-transparent" + class=${classMap({ + "fxview-tab-row-button": true, + "ghost-button": true, + "icon-button": true, + "semi-transparent": true, + [this.secondaryActionClass]: this.secondaryActionClass, + })} id="fxview-tab-row-secondary-button" - part="secondary-button" data-l10n-id=${this.secondaryL10nId} data-l10n-args=${ifDefined(this.secondaryL10nArgs)} aria-haspopup=${ifDefined(this.hasPopup)} @@ -739,6 +930,53 @@ export class FxviewTabRow extends MozLitElement { : "-1"}" ></button>` )} + ${when( + this.tertiaryL10nId && this.tertiaryActionHandler, + () => html`<button + class=${classMap({ + "fxview-tab-row-button": true, + "ghost-button": true, + "icon-button": true, + "semi-transparent": true, + [this.tertiaryActionClass]: this.tertiaryActionClass, + })} + id="fxview-tab-row-tertiary-button" + data-l10n-id=${this.tertiaryL10nId} + data-l10n-args=${ifDefined(this.tertiaryL10nArgs)} + aria-haspopup=${ifDefined(this.hasPopup)} + @click=${this.tertiaryActionHandler} + tabindex="${this.active && + this.currentActiveElementId === "fxview-tab-row-tertiary-button" + ? "0" + : "-1"}" + ></button>` + )}`; + } + + render() { + return html` + ${when( + this.containerObj, + () => html` + <link + rel="stylesheet" + href="chrome://browser/content/usercontext/usercontext.css" + /> + ` + )} + <link + rel="stylesheet" + href="chrome://global/skin/in-content/common.css" + /> + <link + rel="stylesheet" + href="chrome://browser/content/firefoxview/fxview-tab-row.css" + /> + ${when( + this.pinnedTabsGridView && this.indicators?.includes("pinned"), + this.#pinnedTabItemTemplate.bind(this), + this.#unpinnedTabItemTemplate.bind(this) + )} `; } @@ -780,6 +1018,7 @@ export class VirtualList extends MozLitElement { isAlwaysVisible: { type: Boolean }, isVisible: { type: Boolean, state: true }, isSubList: { type: Boolean }, + pinnedTabsIndexOffset: { type: Number }, }; createRenderRoot() { @@ -790,6 +1029,7 @@ export class VirtualList extends MozLitElement { super(); this.activeIndex = 0; this.itemOffset = 0; + this.pinnedTabsIndexOffset = 0; this.items = []; this.subListItems = []; this.itemHeightEstimate = FXVIEW_ROW_HEIGHT_PX; @@ -893,14 +1133,16 @@ export class VirtualList extends MozLitElement { .template=${this.template} .items=${data} .itemHeightEstimate=${this.itemHeightEstimate} - .itemOffset=${i * this.maxRenderCountEstimate} + .itemOffset=${i * this.maxRenderCountEstimate + + this.pinnedTabsIndexOffset} .isAlwaysVisible=${i == parseInt(this.activeIndex / this.maxRenderCountEstimate, 10)} isSubList ></virtual-list>`; }; - itemTemplate = (data, i) => this.template(data, this.itemOffset + i); + itemTemplate = (data, i) => + this.template(data, this.itemOffset + i + this.pinnedTabsIndexOffset); render() { if (this.isAlwaysVisible || this.isVisible) { diff --git a/browser/components/firefoxview/fxview-tab-row.css b/browser/components/firefoxview/fxview-tab-row.css index ceb059a33b..219d7e8aa2 100644 --- a/browser/components/firefoxview/fxview-tab-row.css +++ b/browser/components/firefoxview/fxview-tab-row.css @@ -31,6 +31,12 @@ user-select: none; cursor: pointer; text-decoration: none; + + :host(.pinned) & { + padding: var(--space-small); + min-width: unset; + margin: 0; + } } .fxview-tab-row-main, @@ -44,6 +50,10 @@ .fxview-tab-row-button.ghost-button.icon-button:enabled:hover { background-color: var(--fxviewtabrow-element-background-hover); color: var(--fxviewtabrow-text-color-hover); + + & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after { + stroke: var(--fxview-indicator-stroke-color-hover); + } } .fxview-tab-row-main:hover:active, @@ -52,15 +62,16 @@ } @media (prefers-contrast) { - .fxview-tab-row-main, - .fxview-tab-row-main:hover, - .fxview-tab-row-main:active { + a.fxview-tab-row-main, + a.fxview-tab-row-main:hover, + a.fxview-tab-row-main:active { background-color: transparent; border: 1px solid LinkText; color: LinkText; } - .fxview-tab-row-main:visited .fxview-tab-row-main:visited:hover { + a.fxview-tab-row-main:visited, + a.fxview-tab-row-main:visited:hover { border: 1px solid VisitedText; color: VisitedText; } @@ -68,14 +79,17 @@ .fxview-tab-row-favicon-wrapper { height: 16px; + position: relative; - .fxview-tab-row-favicon::after { + .fxview-tab-row-favicon::after, + .fxview-tab-row-button::after, + &.pinned .fxview-tab-row-pinned-media-button { display: block; content: ""; background-size: 12px; background-position: center; background-repeat: no-repeat; - position: absolute; + position: relative; height: 12px; width: 12px; -moz-context-properties: fill, stroke; @@ -83,36 +97,47 @@ stroke: var(--fxview-background-color-secondary); } - &.bookmark .fxview-tab-row-favicon::after { - background-image: url("chrome://browser/skin/bookmark-12.svg"); + &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after { inset-block-start: 9px; inset-inline-end: -6px; + } + + &.pinnedOnNewTab .fxview-tab-row-favicon::after, + &.pinnedOnNewTab .fxview-tab-row-button::after { + background-image: url("chrome://browser/skin/pin-12.svg"); + } + + &.bookmark .fxview-tab-row-favicon::after, + &.bookmark .fxview-tab-row-button::after { + background-image: url("chrome://browser/skin/bookmark-12.svg"); fill: var(--fxview-primary-action-background); } - &.notification .fxview-tab-row-favicon::after { + &.attention .fxview-tab-row-favicon::after, + &.attention .fxview-tab-row-button::after { background-image: radial-gradient(circle, light-dark(rgb(42, 195, 162), rgb(84, 255, 189)), light-dark(rgb(42, 195, 162), rgb(84, 255, 189)) 2px, transparent 2px); height: 4px; width: 100%; inset-block-start: 20px; } - &.soundplaying .fxview-tab-row-favicon::after { - background-image: url("chrome://global/skin/media/audio.svg"); - inset-block-start: -5px; - inset-inline-end: -7px; + &.pinned .fxview-tab-row-pinned-media-button { + inset-block-start: -10px; + inset-inline-end: -10px; border-radius: 100%; background-color: var(--fxview-background-color-secondary); - padding: 2px; - } + padding: 6px; + min-width: 0; + min-height: 0; + position: absolute; - &.muted .fxview-tab-row-favicon::after { - background-image: url("chrome://global/skin/media/audio-muted.svg"); - inset-block-start: -5px; - inset-inline-end: -7px; - border-radius: 100%; - background-color: var(--fxview-background-color-secondary); - padding: 2px; + &[muted="true"] { + background-image: url("chrome://global/skin/media/audio-muted.svg"); + } + + &[soundplaying="true"] { + background-image: url("chrome://global/skin/media/audio.svg"); + } } } @@ -179,26 +204,40 @@ &[soundplaying="true"] { background-image: url("chrome://global/skin/media/audio.svg"); } + + &.dismiss-button { + background-image: url("chrome://global/skin/icons/close.svg"); + } + + &.options-button { + background-image: url("chrome://global/skin/icons/more.svg"); + } } @media (prefers-contrast) { - .fxview-tab-row-button { + .fxview-tab-row-button, + button.fxview-tab-row-main { border: 1px solid ButtonText; color: ButtonText; } - .fxview-tab-row-button.ghost-button.icon-button:enabled:hover { + .fxview-tab-row-button.ghost-button.icon-button:enabled:hover, + button.fxview-tab-row-main:enabled:hover { border: 1px solid SelectedItem; color: SelectedItem; } - .fxview-tab-row-button.ghost-button.icon-button:enabled:active { + .fxview-tab-row-button.ghost-button.icon-button:enabled:active, + button.fxview-tab-row-main:enabled:active { color: SelectedItem; } .fxview-tab-row-button.ghost-button.icon-button:enabled, .fxview-tab-row-button.ghost-button.icon-button:enabled:hover, - .fxview-tab-row-button.ghost-button.icon-button:enabled:active { + .fxview-tab-row-button.ghost-button.icon-button:enabled:active + button.fxview-tab-row-main:enabled, + button.fxview-tab-row-main:enabled:hover, + button.fxview-tab-row-main:enabled:active { background-color: ButtonFace; } } diff --git a/browser/components/firefoxview/history.mjs b/browser/components/firefoxview/history.mjs index 935cc037e9..1fe028449b 100644 --- a/browser/components/firefoxview/history.mjs +++ b/browser/components/firefoxview/history.mjs @@ -418,7 +418,7 @@ class HistoryInView extends ViewPage { ></h3> <fxview-tab-list slot="main" - class="with-context-menu" + secondaryActionClass="options-button" dateTimeFormat=${historyItem.l10nId.includes("prev-month") ? "dateTime" : "time"} @@ -442,7 +442,7 @@ class HistoryInView extends ViewPage { </h3> <fxview-tab-list slot="main" - class="with-context-menu" + secondaryActionClass="options-button" dateTimeFormat="dateTime" hasPopup="menu" maxTabsLength=${this.maxTabsLength} @@ -520,7 +520,7 @@ class HistoryInView extends ViewPage { )} <fxview-tab-list slot="main" - class="with-context-menu" + secondaryActionClass="options-button" dateTimeFormat="dateTime" hasPopup="menu" maxTabsLength="-1" diff --git a/browser/components/firefoxview/jar.mn b/browser/components/firefoxview/jar.mn index 27eeaaef80..1e5cc3e690 100644 --- a/browser/components/firefoxview/jar.mn +++ b/browser/components/firefoxview/jar.mn @@ -15,9 +15,6 @@ browser.jar: content/browser/firefoxview/view-syncedtabs.css content/browser/firefoxview/recentbrowsing.mjs content/browser/firefoxview/firefoxview.css - content/browser/firefoxview/fxview-category-button.css - content/browser/firefoxview/fxview-category-navigation.css - content/browser/firefoxview/fxview-category-navigation.mjs content/browser/firefoxview/fxview-empty-state.css content/browser/firefoxview/fxview-empty-state.mjs content/browser/firefoxview/helpers.mjs @@ -29,12 +26,12 @@ browser.jar: content/browser/firefoxview/recentlyclosed.mjs content/browser/firefoxview/viewpage.mjs content/browser/firefoxview/history-empty.svg (content/history-empty.svg) - content/browser/firefoxview/category-history.svg (content/category-history.svg) - content/browser/firefoxview/category-opentabs.svg (content/category-opentabs.svg) - content/browser/firefoxview/category-recentbrowsing.svg (content/category-recentbrowsing.svg) - content/browser/firefoxview/category-recentlyclosed.svg (content/category-recentlyclosed.svg) - content/browser/firefoxview/category-syncedtabs.svg (content/category-syncedtabs.svg) content/browser/firefoxview/recentlyclosed-empty.svg (content/recentlyclosed-empty.svg) content/browser/firefoxview/synced-tabs-error.svg (content/synced-tabs-error.svg) content/browser/callout-tab-pickup.svg (content/callout-tab-pickup.svg) content/browser/callout-tab-pickup-dark.svg (content/callout-tab-pickup-dark.svg) + content/browser/firefoxview/view-history.svg (content/view-history.svg) + content/browser/firefoxview/view-opentabs.svg (content/view-opentabs.svg) + content/browser/firefoxview/view-recentbrowsing.svg (content/view-recentbrowsing.svg) + content/browser/firefoxview/view-recentlyclosed.svg (content/view-recentlyclosed.svg) + content/browser/firefoxview/view-syncedtabs.svg (content/view-syncedtabs.svg) diff --git a/browser/components/firefoxview/opentabs.mjs b/browser/components/firefoxview/opentabs.mjs index 6ac63a4b3f..8d7723e931 100644 --- a/browser/components/firefoxview/opentabs.mjs +++ b/browser/components/firefoxview/opentabs.mjs @@ -21,8 +21,10 @@ import { ViewPage, ViewPageContent } from "./viewpage.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + BookmarkList: "resource://gre/modules/BookmarkList.sys.mjs", ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", NonPrivateTabs: "resource:///modules/OpenTabs.sys.mjs", getTabsTargetForWindow: "resource:///modules/OpenTabs.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", @@ -106,6 +108,10 @@ class OpenTabsInView extends ViewPage { this ); } + + this.bookmarkList = new lazy.BookmarkList(this.#getAllTabUrls(), () => + this.viewCards.forEach(card => card.requestUpdate()) + ); } shouldUpdate(changedProperties) { @@ -141,6 +147,8 @@ class OpenTabsInView extends ViewPage { this ); } + + this.bookmarkList.removeListeners(); } viewVisibleCallback() { @@ -161,6 +169,13 @@ class OpenTabsInView extends ViewPage { } } + #getAllTabUrls() { + return this.openTabsTarget + .getAllTabs() + .map(({ linkedBrowser }) => linkedBrowser?.currentURI?.spec) + .filter(Boolean); + } + render() { if (this.recentBrowsing) { return this.getRecentBrowsingTemplate(); @@ -268,6 +283,7 @@ class OpenTabsInView extends ViewPage { winID: currentWindowIndex, })}" .searchQuery=${this.searchQuery} + .bookmarkList=${this.bookmarkList} ></view-opentabs-card> ` )} @@ -282,6 +298,7 @@ class OpenTabsInView extends ViewPage { data-l10n-id="firefoxview-opentabs-window-header" data-l10n-args="${JSON.stringify({ winID })}" .searchQuery=${this.searchQuery} + .bookmarkList=${this.bookmarkList} ></view-opentabs-card> ` )} @@ -318,6 +335,7 @@ class OpenTabsInView extends ViewPage { .recentBrowsing=${true} .paused=${this.paused} .searchQuery=${this.searchQuery} + .bookmarkList=${this.bookmarkList} ></view-opentabs-card>`; } @@ -330,13 +348,9 @@ class OpenTabsInView extends ViewPage { switch (type) { case "TabRecencyChange": case "TabChange": - // if we're switching away from our tab, we can halt any updates immediately - if (!this.isSelectedBrowserTab) { - this.stop(); - return; - } windowIds = detail.windowIds; this._updateWindowList(); + this.bookmarkList.setTrackedUrls(this.#getAllTabUrls()); break; } if (this.recentBrowsing) { @@ -390,6 +404,7 @@ class OpenTabsInViewCard extends ViewPageContent { searchResults: { type: Array }, showAll: { type: Boolean }, cumulativeSearches: { type: Number }, + bookmarkList: { type: Object }, }; static MAX_TABS_FOR_COMPACT_HEIGHT = 7; @@ -470,6 +485,14 @@ class OpenTabsInViewCard extends ViewPageContent { } onTabListRowClick(event) { + // Don't open pinned tab if mute/unmute indicator button selected + if ( + Array.from(event.explicitOriginalTarget.classList).includes( + "fxview-tab-row-pinned-media-button" + ) + ) { + return; + } const tab = event.originalTarget.tabElement; const browserWindow = tab.ownerGlobal; browserWindow.focus(); @@ -497,6 +520,18 @@ class OpenTabsInViewCard extends ViewPageContent { } } + closeTab(event) { + const tab = event.originalTarget.tabElement; + tab?.ownerGlobal.gBrowser.removeTab(tab); + + Services.telemetry.recordEvent( + "firefoxview_next", + "close_open_tab", + "tabs", + null + ); + } + viewVisibleCallback() { this.getRootNode().host.toggleVisibilityInCardContainer(true); } @@ -531,15 +566,18 @@ class OpenTabsInViewCard extends ViewPageContent { )} <div class="fxview-tab-list-container" slot="main"> <fxview-tab-list - class="with-context-menu" .hasPopup=${"menu"} ?compactRows=${this.classList.contains("width-limited")} @fxview-tab-list-primary-action=${this.onTabListRowClick} @fxview-tab-list-secondary-action=${this.openContextMenu} + @fxview-tab-list-tertiary-action=${this.closeTab} + secondaryActionClass="options-button" + tertiaryActionClass="dismiss-button" .maxTabsLength=${this.getMaxTabsLength()} - .tabItems=${this.searchResults || getTabListItems(this.tabs)} + .tabItems=${this.searchResults || + getTabListItems(this.tabs, this.recentBrowsing)} .searchQuery=${this.searchQuery} - .showTabIndicators=${true} + .pinnedTabsGridView=${!this.recentBrowsing} ><view-opentabs-contextmenu slot="menu"></view-opentabs-contextmenu> </fxview-tab-list> </div> @@ -590,6 +628,28 @@ class OpenTabsInViewCard extends ViewPageContent { ? searchTabList(this.searchQuery, getTabListItems(this.tabs)) : null; } + + updated() { + this.updateBookmarkStars(); + } + + async updateBookmarkStars() { + const tabItems = [...this.tabList.tabItems]; + for (const row of tabItems) { + const isBookmark = await this.bookmarkList.isBookmark(row.url); + if (isBookmark && !row.indicators.includes("bookmark")) { + row.indicators.push("bookmark"); + } + if (!isBookmark && row.indicators.includes("bookmark")) { + row.indicators = row.indicators.filter(i => i !== "bookmark"); + } + row.primaryL10nId = getPrimaryL10nId( + this.isRecentBrowsing, + row.indicators + ); + } + this.tabList.tabItems = tabItems; + } } customElements.define("view-opentabs-card", OpenTabsInViewCard); @@ -655,6 +715,29 @@ class OpenTabsContextMenu extends MozLitElement { this.ownerViewPage.recordContextMenuTelemetry("close-tab", e); } + pinTab(e) { + const tab = this.triggerNode.tabElement; + tab?.ownerGlobal.gBrowser.pinTab(tab); + this.ownerViewPage.recordContextMenuTelemetry("pin-tab", e); + } + + unpinTab(e) { + const tab = this.triggerNode.tabElement; + tab?.ownerGlobal.gBrowser.unpinTab(tab); + this.ownerViewPage.recordContextMenuTelemetry("unpin-tab", e); + } + + toggleAudio(e) { + const tab = this.triggerNode.tabElement; + tab.toggleMuteAudio(); + this.ownerViewPage.recordContextMenuTelemetry( + `${ + this.triggerNode.indicators.includes("muted") ? "unmute" : "mute" + }-tab`, + e + ); + } + moveTabsToStart(e) { const tab = this.triggerNode.tabElement; tab?.ownerGlobal.gBrowser.moveTabsToStart(tab); @@ -749,16 +832,25 @@ class OpenTabsContextMenu extends MozLitElement { /> <panel-list data-tab-type="opentabs"> <panel-item - data-l10n-id="fxviewtabrow-close-tab" - data-l10n-attrs="accesskey" - @click=${this.closeTab} - ></panel-item> - <panel-item data-l10n-id="fxviewtabrow-move-tab" data-l10n-attrs="accesskey" submenu="move-tab-menu" >${this.moveMenuTemplate()}</panel-item > + <panel-item + data-l10n-id=${tab.pinned + ? "fxviewtabrow-unpin-tab" + : "fxviewtabrow-pin-tab"} + data-l10n-attrs="accesskey" + @click=${tab.pinned ? this.unpinTab : this.pinTab} + ></panel-item> + <panel-item + data-l10n-id=${tab.hasAttribute("muted") + ? "fxviewtabrow-unmute-tab" + : "fxviewtabrow-mute-tab"} + data-l10n-attrs="accesskey" + @click=${this.toggleAudio} + ></panel-item> <hr /> <panel-item data-l10n-id="fxviewtabrow-copy-link" @@ -798,36 +890,140 @@ function getContainerObj(tab) { } /** + * Gets an array of tab indicators (if any) when normalizing for fxview-tab-list + * + * @param {MozTabbrowserTab[]} tab + * Tab to fetch container info on. + * @returns {Array[]} + * Array of named tab indicators + */ +function getIndicatorsForTab(tab) { + const url = tab.linkedBrowser?.currentURI?.spec || ""; + let tabIndicators = []; + let hasAttention = + (tab.pinned && + (tab.hasAttribute("attention") || tab.hasAttribute("titlechanged"))) || + (!tab.pinned && tab.hasAttribute("attention")); + + if (tab.pinned) { + tabIndicators.push("pinned"); + } + if (getContainerObj(tab)) { + tabIndicators.push("container"); + } + if (hasAttention) { + tabIndicators.push("attention"); + } + if (tab.hasAttribute("soundplaying") && !tab.hasAttribute("muted")) { + tabIndicators.push("soundplaying"); + } + if (tab.hasAttribute("muted")) { + tabIndicators.push("muted"); + } + if (checkIfPinnedNewTab(url)) { + tabIndicators.push("pinnedOnNewTab"); + } + + return tabIndicators; +} +/** + * Gets the primary l10n id for a tab when normalizing for fxview-tab-list + * + * @param {boolean} isRecentBrowsing + * Whether the tabs are going to be displayed on the Recent Browsing page or not + * @param {Array[]} tabIndicators + * Array of tab indicators for the given tab + * @returns {string} + * L10n ID string + */ +function getPrimaryL10nId(isRecentBrowsing, tabIndicators) { + let indicatorL10nId = null; + if (!isRecentBrowsing) { + if ( + tabIndicators?.includes("pinned") && + tabIndicators?.includes("bookmark") + ) { + indicatorL10nId = "firefoxview-opentabs-bookmarked-pinned-tab"; + } else if (tabIndicators?.includes("pinned")) { + indicatorL10nId = "firefoxview-opentabs-pinned-tab"; + } else if (tabIndicators?.includes("bookmark")) { + indicatorL10nId = "firefoxview-opentabs-bookmarked-tab"; + } + } + return indicatorL10nId; +} + +/** + * Gets the primary l10n args for a tab when normalizing for fxview-tab-list + * + * @param {MozTabbrowserTab[]} tab + * Tab to fetch container info on. + * @param {boolean} isRecentBrowsing + * Whether the tabs are going to be displayed on the Recent Browsing page or not + * @param {string} url + * URL for the given tab + * @returns {string} + * L10n ID args + */ +function getPrimaryL10nArgs(tab, isRecentBrowsing, url) { + return JSON.stringify({ tabTitle: tab.label, url }); +} + +/** + * Check if a given url is pinned on the new tab page + * + * @param {string} url + * url to check + * @returns {boolean} + * is tabbed pinned on new tab page + */ +function checkIfPinnedNewTab(url) { + return url && lazy.NewTabUtils.pinnedLinks.isPinned({ url }); +} + +/** * Convert a list of tabs into the format expected by the fxview-tab-list * component. * * @param {MozTabbrowserTab[]} tabs * Tabs to format. + * @param {boolean} isRecentBrowsing + * Whether the tabs are going to be displayed on the Recent Browsing page or not * @returns {object[]} * Formatted objects. */ -function getTabListItems(tabs) { - let filtered = tabs?.filter( - tab => !tab.closing && !tab.hidden && !tab.pinned - ); +function getTabListItems(tabs, isRecentBrowsing) { + let filtered = tabs?.filter(tab => !tab.closing && !tab.hidden); return filtered.map(tab => { - const url = tab.linkedBrowser?.currentURI?.spec || ""; + let tabIndicators = getIndicatorsForTab(tab); + let containerObj = getContainerObj(tab); + const url = tab?.linkedBrowser?.currentURI?.spec || ""; return { - attention: tab.hasAttribute("attention"), - containerObj: getContainerObj(tab), + containerObj, + indicators: tabIndicators, icon: tab.getAttribute("image"), - muted: tab.hasAttribute("muted"), - pinned: tab.pinned, - primaryL10nId: "firefoxview-opentabs-tab-row", - primaryL10nArgs: JSON.stringify({ url }), - secondaryL10nId: "fxviewtabrow-options-menu-button", - secondaryL10nArgs: JSON.stringify({ tabTitle: tab.label }), - soundPlaying: tab.hasAttribute("soundplaying"), + primaryL10nId: getPrimaryL10nId(isRecentBrowsing, tabIndicators), + primaryL10nArgs: getPrimaryL10nArgs(tab, isRecentBrowsing, url), + secondaryL10nId: + isRecentBrowsing || (!isRecentBrowsing && !tab.pinned) + ? "fxviewtabrow-options-menu-button" + : null, + secondaryL10nArgs: + isRecentBrowsing || (!isRecentBrowsing && !tab.pinned) + ? JSON.stringify({ tabTitle: tab.label }) + : null, + tertiaryL10nId: + isRecentBrowsing || (!isRecentBrowsing && !tab.pinned) + ? "fxviewtabrow-close-tab-button" + : null, + tertiaryL10nArgs: + isRecentBrowsing || (!isRecentBrowsing && !tab.pinned) + ? JSON.stringify({ tabTitle: tab.label }) + : null, tabElement: tab, time: tab.lastAccessed, title: tab.label, - titleChanged: tab.hasAttribute("titlechanged"), url, }; }); diff --git a/browser/components/firefoxview/recentlyclosed.mjs b/browser/components/firefoxview/recentlyclosed.mjs index 6e7e06c1f4..83c323256c 100644 --- a/browser/components/firefoxview/recentlyclosed.mjs +++ b/browser/components/firefoxview/recentlyclosed.mjs @@ -398,6 +398,7 @@ class RecentlyClosedTabsInView extends ViewPage { .tabItems=${this.searchResults || this.recentlyClosedTabs} @fxview-tab-list-secondary-action=${this.onDismissTab} @fxview-tab-list-primary-action=${this.onReopenTab} + secondaryActionClass="dismiss-button" ></fxview-tab-list> ` )} diff --git a/browser/components/firefoxview/syncedtabs.mjs b/browser/components/firefoxview/syncedtabs.mjs index 5320f8cb41..d64da45a30 100644 --- a/browser/components/firefoxview/syncedtabs.mjs +++ b/browser/components/firefoxview/syncedtabs.mjs @@ -419,13 +419,14 @@ class SyncedTabsInView extends ViewPage { </h3> <fxview-tab-list slot="main" - class="with-context-menu" + secondaryActionClass="options-button" hasPopup="menu" .tabItems=${ifDefined(tabItems)} .searchQuery=${this.searchQuery} maxTabsLength=${this.showAll ? -1 : this.maxTabsLength} @fxview-tab-list-primary-action=${this.onOpenLink} @fxview-tab-list-secondary-action=${this.onContextMenu} + secondaryActionClass="options-button" > ${this.panelListTemplate()} </fxview-tab-list>`; diff --git a/browser/components/firefoxview/tests/browser/FirefoxViewTestUtils.sys.mjs b/browser/components/firefoxview/tests/browser/FirefoxViewTestUtils.sys.mjs index 3fd2bf95e3..e1285c0396 100644 --- a/browser/components/firefoxview/tests/browser/FirefoxViewTestUtils.sys.mjs +++ b/browser/components/firefoxview/tests/browser/FirefoxViewTestUtils.sys.mjs @@ -21,6 +21,20 @@ function getFirefoxViewURL() { return "about:firefoxview"; } +/** + * Make the given window focused and active + */ +async function switchToWindow(win) { + await testScope.SimpleTest.promiseFocus(win); + if (Services.focus.activeWindow !== win) { + testScope.info("switchToWindow, waiting for activate event on the window"); + await BrowserTestUtils.waitForEvent(win, "activate"); + } else { + testScope.info("switchToWindow, win is already the activeWindow"); + } + testScope.info("switchToWindow, done"); +} + function assertFirefoxViewTab(win) { Assert.ok(win.FirefoxViewHandler.tab, "Firefox View tab exists"); Assert.ok(win.FirefoxViewHandler.tab?.hidden, "Firefox View tab is hidden"); @@ -48,7 +62,7 @@ async function openFirefoxViewTab(win) { "Must initialize FirefoxViewTestUtils with a test scope which has a SimpleTest property" ); } - await testScope.SimpleTest.promiseFocus(win); + await switchToWindow(win); let fxviewTab = win.FirefoxViewHandler.tab; let alreadyLoaded = fxviewTab?.linkedBrowser.currentURI.spec.includes(getFirefoxViewURL()) && @@ -167,6 +181,7 @@ function isFirefoxViewTabSelectedInWindow(win) { export { init, + switchToWindow, withFirefoxView, assertFirefoxViewTab, assertFirefoxViewTabSelected, diff --git a/browser/components/firefoxview/tests/browser/browser.toml b/browser/components/firefoxview/tests/browser/browser.toml index 8e2005760b..9f9c1c0176 100644 --- a/browser/components/firefoxview/tests/browser/browser.toml +++ b/browser/components/firefoxview/tests/browser/browser.toml @@ -27,48 +27,57 @@ skip-if = ["true"] # Bug 1869605 and # Bug 1870296 ["browser_firefoxview.js"] -["browser_firefoxview_tab.js"] - -["browser_notification_dot.js"] -skip-if = ["true"] # Bug 1851453 - -["browser_opentabs_changes.js"] - -["browser_reload_firefoxview.js"] - -["browser_tab_close_last_tab.js"] - -["browser_tab_on_close_warning.js"] - -["browser_firefoxview_paused.js"] - ["browser_firefoxview_general_telemetry.js"] ["browser_firefoxview_navigation.js"] +["browser_firefoxview_paused.js"] + ["browser_firefoxview_search_telemetry.js"] +["browser_firefoxview_tab.js"] + ["browser_firefoxview_virtual_list.js"] ["browser_history_firefoxview.js"] -skip-if = ["true"] # Bug 1877594 -["browser_opentabs_firefoxview.js"] +["browser_notification_dot.js"] +skip-if = ["true"] # Bug 1851453 ["browser_opentabs_cards.js"] + +["browser_opentabs_changes.js"] + +["browser_opentabs_firefoxview.js"] + +["browser_opentabs_more.js"] fail-if = ["a11y_checks"] # Bugs 1858041, 1854625, and 1872174 clicked Show all link is not accessible because it is "hidden" when clicked +skip-if = ["verify"] # Bug 1886017 + +["browser_opentabs_pinned_tabs.js"] ["browser_opentabs_recency.js"] skip-if = [ "os == 'win'", "os == 'mac' && verify", "os == 'linux'" -] # macos times out, see bug 1857293, skipped for windows, see bug 1858460, skipped for linux, see bug 1875877 +] # macos times out, see bug 1857293, skipped for windows, see bug 1858460, Bug 1875877 - frequent fails on linux. + +["browser_opentabs_search.js"] +fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable ["browser_opentabs_tab_indicators.js"] ["browser_recentlyclosed_firefoxview.js"] +["browser_reload_firefoxview.js"] + ["browser_syncedtabs_errors_firefoxview.js"] ["browser_syncedtabs_firefoxview.js"] + +["browser_tab_close_last_tab.js"] + +["browser_tab_list_keyboard_navigation.js"] + +["browser_tab_on_close_warning.js"] diff --git a/browser/components/firefoxview/tests/browser/browser_dragDrop_after_opening_fxViewTab.js b/browser/components/firefoxview/tests/browser/browser_dragDrop_after_opening_fxViewTab.js index 9ce547238a..0376a3886d 100644 --- a/browser/components/firefoxview/tests/browser/browser_dragDrop_after_opening_fxViewTab.js +++ b/browser/components/firefoxview/tests/browser/browser_dragDrop_after_opening_fxViewTab.js @@ -15,6 +15,7 @@ add_task(async function () { // window.RTL_UI doesn't update in existing windows when this pref is changed, // so we need to test in a new window. let win = await BrowserTestUtils.openNewBrowserWindow(); + await switchToWindow(win); const TEST_ROOT = getRootDirectory(gTestPath).replace( "chrome://mochitests/content", diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_firefoxview.js index 33467941a4..1a51d61f42 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview.js @@ -6,10 +6,7 @@ add_task(async function about_firefoxview_smoke_test() { const { document } = browser.contentWindow; // sanity check the important regions exist on this page - ok( - document.querySelector("fxview-category-navigation"), - "fxview-category-navigation element exists" - ); + ok(document.querySelector("moz-page-nav"), "moz-page-nav element exists"); ok(document.querySelector("named-deck"), "named-deck element exists"); }); }); diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_general_telemetry.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_general_telemetry.js index 51d5caa032..b70e6b938e 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview_general_telemetry.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_general_telemetry.js @@ -39,7 +39,7 @@ add_task(async function firefox_view_entered_telemetry() { enteredTelemetry[4] = { page: "recentlyclosed" }; enteredAndTabSelectedEvents = [tabSelectedTelemetry, enteredTelemetry]; - navigateToCategory(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); await clearAllParentTelemetryEvents(); await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots"); is( @@ -107,9 +107,9 @@ add_task(async function test_change_page_telemetry() { ], ]; await clearAllParentTelemetryEvents(); - navigateToCategory(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); await telemetryEvent(changePageEvent); - navigateToCategory(document, "recentbrowsing"); + await navigateToViewAndWait(document, "recentbrowsing"); let openTabsComponent = document.querySelector( "view-opentabs[slot=opentabs]" @@ -189,7 +189,7 @@ add_task(async function test_context_menu_new_window_telemetry() { ); // Test history context menu options - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); let historyComponent = document.querySelector("view-history"); await TestUtils.waitForCondition(() => historyComponent.fullyUpdated); await TestUtils.waitForCondition( @@ -198,7 +198,11 @@ add_task(async function test_context_menu_new_window_telemetry() { let firstTabList = historyComponent.lists[0]; let firstItem = firstTabList.rowEls[0]; let panelList = historyComponent.panelList; - EventUtils.synthesizeMouseAtCenter(firstItem.buttonEl, {}, content); + EventUtils.synthesizeMouseAtCenter( + firstItem.secondaryButtonEl, + {}, + content + ); await BrowserTestUtils.waitForEvent(panelList, "shown"); await clearAllParentTelemetryEvents(); let panelItems = Array.from(panelList.children).filter( @@ -245,7 +249,7 @@ add_task(async function test_context_menu_private_window_telemetry() { ); // Test history context menu options - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); let historyComponent = document.querySelector("view-history"); await TestUtils.waitForCondition(() => historyComponent.fullyUpdated); await TestUtils.waitForCondition( @@ -254,14 +258,22 @@ add_task(async function test_context_menu_private_window_telemetry() { let firstTabList = historyComponent.lists[0]; let firstItem = firstTabList.rowEls[0]; let panelList = historyComponent.panelList; - EventUtils.synthesizeMouseAtCenter(firstItem.buttonEl, {}, content); + EventUtils.synthesizeMouseAtCenter( + firstItem.secondaryButtonEl, + {}, + content + ); await BrowserTestUtils.waitForEvent(panelList, "shown"); await clearAllParentTelemetryEvents(); let panelItems = Array.from(panelList.children).filter( panelItem => panelItem.nodeName === "PANEL-ITEM" ); - EventUtils.synthesizeMouseAtCenter(firstItem.buttonEl, {}, content); + EventUtils.synthesizeMouseAtCenter( + firstItem.secondaryButtonEl, + {}, + content + ); info("Context menu button clicked."); await BrowserTestUtils.waitForEvent(panelList, "shown"); info("Context menu shown."); @@ -314,7 +326,7 @@ add_task(async function test_context_menu_delete_from_history_telemetry() { ); // Test history context menu options - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); let historyComponent = document.querySelector("view-history"); await TestUtils.waitForCondition(() => historyComponent.fullyUpdated); await TestUtils.waitForCondition( @@ -323,14 +335,22 @@ add_task(async function test_context_menu_delete_from_history_telemetry() { let firstTabList = historyComponent.lists[0]; let firstItem = firstTabList.rowEls[0]; let panelList = historyComponent.panelList; - EventUtils.synthesizeMouseAtCenter(firstItem.buttonEl, {}, content); + EventUtils.synthesizeMouseAtCenter( + firstItem.secondaryButtonEl, + {}, + content + ); await BrowserTestUtils.waitForEvent(panelList, "shown"); await clearAllParentTelemetryEvents(); let panelItems = Array.from(panelList.children).filter( panelItem => panelItem.nodeName === "PANEL-ITEM" ); - EventUtils.synthesizeMouseAtCenter(firstItem.buttonEl, {}, content); + EventUtils.synthesizeMouseAtCenter( + firstItem.secondaryButtonEl, + {}, + content + ); info("Context menu button clicked."); await BrowserTestUtils.waitForEvent(panelList, "shown"); info("Context menu shown."); diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_navigation.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_navigation.js index 80206dd945..281d969b39 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview_navigation.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_navigation.js @@ -3,15 +3,15 @@ const URL_BASE = `${getFirefoxViewURL()}#`; -function assertCorrectPage(document, name, event) { +function assertCorrectPage(document, view, event) { is( document.location.hash, - `#${name}`, - `Navigation button for ${name} navigates to ${URL_BASE + name} on ${event}.` + `#${view}`, + `Navigation button for ${view} navigates to ${URL_BASE + view} on ${event}.` ); is( document.querySelector("named-deck").selectedViewName, - name, + view, "The correct deck child is selected" ); } @@ -22,21 +22,21 @@ add_task(async function test_side_component_navigation_by_click() { const { document } = browser.contentWindow; let win = browser.ownerGlobal; - const categoryButtons = document.querySelectorAll("fxview-category-button"); + const pageNavButtons = document.querySelectorAll("moz-page-nav-button"); - for (let element of categoryButtons) { - const name = element.name; + for (let element of pageNavButtons) { + const view = element.view; let buttonClicked = BrowserTestUtils.waitForEvent( element.buttonEl, "click", win ); - info(`Clicking navigation button for ${name}`); + info(`Clicking navigation button for ${view}`); EventUtils.synthesizeMouseAtCenter(element.buttonEl, {}, content); await buttonClicked; - assertCorrectPage(document, name, "click"); + assertCorrectPage(document, view, "click"); } }); }); @@ -47,49 +47,49 @@ add_task(async function test_side_component_navigation_by_keyboard() { const { document } = browser.contentWindow; let win = browser.ownerGlobal; - const categoryButtons = document.querySelectorAll("fxview-category-button"); - const firstButton = categoryButtons[0]; + const pageNavButtons = document.querySelectorAll("moz-page-nav-button"); + const firstButton = pageNavButtons[0].buttonEl; firstButton.focus(); is( - document.activeElement, + document.activeElement.shadowRoot.activeElement, firstButton, - "The first category button has focus" + "The first page nav button has focus" ); - for (let element of Array.from(categoryButtons).slice(1)) { - const name = element.name; + for (let element of Array.from(pageNavButtons).slice(1)) { + const view = element.view; let buttonFocused = BrowserTestUtils.waitForEvent(element, "focus", win); - info(`Focus is on ${document.activeElement.name}`); - info(`Arrow down on navigation to ${name}`); + info(`Focus is on ${document.activeElement.view}`); + info(`Arrow down on navigation to ${view}`); EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); await buttonFocused; - assertCorrectPage(document, name, "key press"); + assertCorrectPage(document, view, "key press"); } }); }); -add_task(async function test_direct_navigation_to_correct_category() { +add_task(async function test_direct_navigation_to_correct_view() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - const categoryButtons = document.querySelectorAll("fxview-category-button"); + const pageNavButtons = document.querySelectorAll("moz-page-nav-button"); const namedDeck = document.querySelector("named-deck"); - for (let element of categoryButtons) { - const name = element.name; + for (let element of pageNavButtons) { + const view = element.view; - info(`Navigating to ${URL_BASE + name}`); - document.location.assign(URL_BASE + name); + info(`Navigating to ${URL_BASE + view}`); + document.location.assign(URL_BASE + view); await BrowserTestUtils.waitForCondition(() => { - return namedDeck.selectedViewName === name; + return namedDeck.selectedViewName === view; }, "Wait for navigation to complete"); is( namedDeck.selectedViewName, - name, - `The correct deck child for category ${name} is selected` + view, + `The correct deck child for view ${view} is selected` ); } }); diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js index c95ac4fcf5..e61b48b472 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js @@ -5,9 +5,6 @@ const tabURL1 = "data:,Tab1"; const tabURL2 = "data:,Tab2"; const tabURL3 = "data:,Tab3"; -const { NonPrivateTabs } = ChromeUtils.importESModule( - "resource:///modules/OpenTabs.sys.mjs" -); const TestTabs = {}; function getTopLevelViewElements(document) { @@ -194,6 +191,42 @@ async function checkFxRenderCalls(browser, elements, selectedView) { sandbox.restore(); } +function dragAndDrop( + tab1, + tab2, + initialWindow = window, + destWindow = window, + afterTab = true, + context +) { + let rect = tab2.getBoundingClientRect(); + let event = { + ctrlKey: false, + altKey: false, + clientX: rect.left + rect.width / 2 + 10 * (afterTab ? 1 : -1), + clientY: rect.top + rect.height / 2, + }; + + if (destWindow != initialWindow) { + // Make sure that both tab1 and tab2 are visible + initialWindow.focus(); + initialWindow.moveTo(rect.left, rect.top + rect.height * 3); + } + + EventUtils.synthesizeDrop( + tab1, + tab2, + null, + "move", + initialWindow, + destWindow, + event + ); + + // Ensure dnd suppression is cleared. + EventUtils.synthesizeMouseAtCenter(tab2, { type: "mouseup" }, context); +} + add_task(async function test_recentbrowsing() { await setupOpenAndClosedTabs(); @@ -322,7 +355,7 @@ add_task(async function test_opentabs() { const document = browser.contentDocument; const { openTabsView } = getTopLevelViewElements(document); - await navigateToCategoryAndWait(document, "opentabs"); + await navigateToViewAndWait(document, "opentabs"); const { openTabsList } = await getElements(document); ok(openTabsView, "Found the open tabs view"); @@ -387,7 +420,7 @@ add_task(async function test_recentlyclosed() { await withFirefoxView({}, async browser => { const document = browser.contentDocument; const { recentlyClosedView } = getTopLevelViewElements(document); - await navigateToCategoryAndWait(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); const { recentlyClosedList } = await getElements(document); ok(recentlyClosedView, "Found the recently-closed view"); @@ -405,3 +438,66 @@ add_task(async function test_recentlyclosed() { }); await BrowserTestUtils.removeTab(TestTabs.tab2); }); + +add_task(async function test_drag_drop_pinned_tab() { + await setupOpenAndClosedTabs(); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + let win1 = browser.ownerGlobal; + await navigateToViewAndWait(document, "opentabs"); + + let openTabs = document.querySelector("view-opentabs[name=opentabs]"); + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length + ); + await openTabs.openTabsTarget.readyWindowsPromise; + let card = openTabs.viewCards[0]; + let tabRows = card.tabList.rowEls; + let tabChangeRaised; + + // Pin first two tabs + for (var i = 0; i < 2; i++) { + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + let currentTabEl = tabRows[i]; + let currentTab = currentTabEl.tabElement; + info(`Pinning tab ${i + 1} with label: ${currentTab.label}`); + win1.gBrowser.pinTab(currentTab); + await tabChangeRaised; + await openTabs.updateComplete; + tabRows = card.tabList.rowEls; + currentTabEl = tabRows[i]; + + await TestUtils.waitForCondition( + () => currentTabEl.indicators.includes("pinned"), + `Tab ${i + 1} is pinned.` + ); + } + + info(`First two tabs are pinned.`); + + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards.length === 2, + "Two windows are shown for Open Tabs in in Fx View." + ); + + let pinnedTab = win1.gBrowser.visibleTabs[0]; + let newWindowTab = win2.gBrowser.visibleTabs[0]; + + dragAndDrop(newWindowTab, pinnedTab, win2, win1, true, content); + + await switchToFxViewTab(); + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards.length === 1, + "One window is shown for Open Tabs in in Fx View." + ); + }); + cleanupTabs(); +}); diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js index 2ea2429c15..c76a11d3ad 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js @@ -56,7 +56,7 @@ add_task(async function test_search_initiated_telemetry() { EventUtils.sendString("example.com", content); await telemetryEvent(searchEvent("recentbrowsing")); - await navigateToCategoryAndWait(document, "opentabs"); + await navigateToViewAndWait(document, "opentabs"); await clearAllParentTelemetryEvents(); is(document.location.hash, "#opentabs", "Searching within open tabs."); const openTabs = document.querySelector("named-deck > view-opentabs"); @@ -65,7 +65,7 @@ add_task(async function test_search_initiated_telemetry() { EventUtils.sendString("example.com", content); await telemetryEvent(searchEvent("opentabs")); - await navigateToCategoryAndWait(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); await clearAllParentTelemetryEvents(); is( document.location.hash, @@ -84,7 +84,7 @@ add_task(async function test_search_initiated_telemetry() { EventUtils.sendString("example.com", content); await telemetryEvent(searchEvent("recentlyclosed")); - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); await clearAllParentTelemetryEvents(); is(document.location.hash, "#syncedtabs", "Searching within synced tabs."); const syncedTabs = document.querySelector("named-deck > view-syncedtabs"); @@ -93,7 +93,7 @@ add_task(async function test_search_initiated_telemetry() { EventUtils.sendString("example.com", content); await telemetryEvent(searchEvent("syncedtabs")); - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); await clearAllParentTelemetryEvents(); is(document.location.hash, "#history", "Searching within history."); const history = document.querySelector("named-deck > view-history"); @@ -316,7 +316,7 @@ add_task(async function test_sort_history_search_telemetry() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); const historyComponent = document.querySelector("view-history"); const searchTextbox = await TestUtils.waitForCondition( @@ -432,7 +432,7 @@ add_task(async function test_cumulative_searches_recently_closed_telemetry() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); is( document.location.hash, "#recentlyclosed", @@ -477,7 +477,7 @@ add_task(async function test_cumulative_searches_open_tabs_telemetry() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "opentabs"); + await navigateToViewAndWait(document, "opentabs"); is(document.location.hash, "#opentabs", "Searching within open tabs."); const openTabs = document.querySelector("named-deck > view-opentabs"); @@ -523,7 +523,7 @@ add_task(async function test_cumulative_searches_history_telemetry() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); is(document.location.hash, "#history", "Searching within history."); const history = document.querySelector("named-deck > view-history"); const searchTextbox = await TestUtils.waitForCondition(() => { @@ -585,7 +585,7 @@ add_task(async function test_cumulative_searches_syncedtabs_telemetry() { const { document } = browser.contentWindow; Services.obs.notifyObservers(null, UIState.ON_UPDATE); - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); is(document.location.hash, "#syncedtabs", "Searching within synced tabs."); let syncedTabs = document.querySelector( "view-syncedtabs:not([slot=syncedtabs])" diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js index f1ac7d6742..037729ea7d 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js @@ -55,12 +55,6 @@ function triggerClickOn(target, options) { return promise; } -async function add_new_tab(URL) { - let tab = BrowserTestUtils.addTab(gBrowser, URL); - await BrowserTestUtils.browserLoaded(tab.linkedBrowser); - return tab; -} - add_task(async function aria_attributes() { let win = await BrowserTestUtils.openNewBrowserWindow(); is( 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 501deb8e68..bf53796ef7 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js @@ -29,7 +29,7 @@ add_task(async function test_max_render_count_on_win_resize() { "Firefox View is loaded to the Recent Browsing page." ); - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); let historyComponent = document.querySelector("view-history"); let tabList = historyComponent.lists[0]; diff --git a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js index a6c697e398..c4c096acff 100644 --- a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js @@ -15,8 +15,8 @@ const HISTORY_EVENT = [["firefoxview_next", "history", "visits", undefined]]; const SHOW_ALL_HISTORY_EVENT = [ ["firefoxview_next", "show_all_history", "tabs", undefined], ]; - const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart"; + const DAY_MS = 24 * 60 * 60 * 1000; const today = new Date(); const yesterday = new Date(Date.now() - DAY_MS); @@ -24,6 +24,14 @@ const twoDaysAgo = new Date(Date.now() - DAY_MS * 2); const threeDaysAgo = new Date(Date.now() - DAY_MS * 3); const fourDaysAgo = new Date(Date.now() - DAY_MS * 4); const oneMonthAgo = new Date(today); +const dates = [ + today, + yesterday, + twoDaysAgo, + threeDaysAgo, + fourDaysAgo, + oneMonthAgo, +]; // Set the date for the first day of the last month oneMonthAgo.setDate(1); @@ -47,13 +55,14 @@ function isElInViewport(element) { ); } -async function historyComponentReady(historyComponent) { +async function historyComponentReady(historyComponent, expectedHistoryItems) { await TestUtils.waitForCondition( () => [...historyComponent.allHistoryItems.values()].reduce( (acc, { length }) => acc + length, 0 - ) === 24 + ) === expectedHistoryItems, + "History component ready" ); let expected = historyComponent.historyMapByDate.length; @@ -148,6 +157,18 @@ async function addHistoryItems(dateAdded) { }); } +function createHistoryEntries() { + let historyEntries = []; + for (let i = 0; i < 4; i++) { + historyEntries.push({ + url: URLs[i], + title: `Example Domain ${i}`, + visits: dates.map(date => [{ date }]), + }); + } + return historyEntries; +} + add_setup(async () => { await SpecialPowers.pushPrefEnv({ set: [["browser.firefox-view.search.enabled", true]], @@ -160,21 +181,20 @@ add_setup(async () => { add_task(async function test_list_ordering() { await PlacesUtils.history.clear(); - await addHistoryItems(today); - await addHistoryItems(yesterday); - await addHistoryItems(twoDaysAgo); - await addHistoryItems(threeDaysAgo); - await addHistoryItems(fourDaysAgo); - await addHistoryItems(oneMonthAgo); + const historyEntries = createHistoryEntries(); + await PlacesUtils.history.insertMany(historyEntries); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); - let historyComponent = document.querySelector("view-history"); + let historyComponent = await TestUtils.waitForCondition( + () => document.querySelector("view-history"), + "History component rendered" + ); historyComponent.profileAge = 8; - await historyComponentReady(historyComponent); + await historyComponentReady(historyComponent, historyEntries.length); let firstCard = historyComponent.cards[0]; @@ -262,7 +282,7 @@ add_task(async function test_empty_states() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); let historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; @@ -350,7 +370,7 @@ add_task(async function test_observers_removed_when_view_is_hidden() { ); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); const historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; let visitList = await TestUtils.waitForCondition(() => @@ -390,20 +410,16 @@ add_task(async function test_observers_removed_when_view_is_hidden() { add_task(async function test_show_all_history_telemetry() { await PlacesUtils.history.clear(); - await addHistoryItems(today); - await addHistoryItems(yesterday); - await addHistoryItems(twoDaysAgo); - await addHistoryItems(threeDaysAgo); - await addHistoryItems(fourDaysAgo); - await addHistoryItems(oneMonthAgo); + const historyEntries = createHistoryEntries(); + await PlacesUtils.history.insertMany(historyEntries); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); let historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; - await historyComponentReady(historyComponent); + await historyComponentReady(historyComponent, historyEntries.length); await clearAllParentTelemetryEvents(); let showAllHistoryBtn = historyComponent.showAllHistoryBtn; @@ -422,12 +438,15 @@ add_task(async function test_show_all_history_telemetry() { }); add_task(async function test_search_history() { + await PlacesUtils.history.clear(); + const historyEntries = createHistoryEntries(); + await PlacesUtils.history.insertMany(historyEntries); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); const historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; - await historyComponentReady(historyComponent); + await historyComponentReady(historyComponent, historyEntries.length); const searchTextbox = await TestUtils.waitForCondition( () => historyComponent.searchTextbox, "The search textbox is displayed." @@ -447,7 +466,7 @@ add_task(async function test_search_history() { ); await TestUtils.waitForCondition(() => { const { rowEls } = historyComponent.lists[0]; - return rowEls.length === 1 && rowEls[0].mainEl.href === URLs[0]; + return rowEls.length === 1 && rowEls[0].mainEl.href === URLs[1]; }, "There is one matching search result."); info("Input a bogus search query."); @@ -504,7 +523,7 @@ add_task(async function test_persist_collapse_card_after_view_change() { await addHistoryItems(today); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "history"); const historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; await TestUtils.waitForCondition( @@ -529,8 +548,8 @@ add_task(async function test_persist_collapse_card_after_view_change() { ); // Switch to a new view and then back to History - await navigateToCategoryAndWait(document, "syncedtabs"); - await navigateToCategoryAndWait(document, "history"); + await navigateToViewAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "history"); // Check that first history card is still collapsed after changing view ok( diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js b/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js index d57aa3cad1..d4de3ae5a9 100644 --- a/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js @@ -7,36 +7,16 @@ const ROW_DATE_ID = "fxview-tab-row-date"; let gInitialTab; let gInitialTabURL; -const { NonPrivateTabs } = ChromeUtils.importESModule( - "resource:///modules/OpenTabs.sys.mjs" -); add_setup(function () { // This test opens a lot of windows and tabs and might run long on slower configurations - requestLongerTimeout(2); + requestLongerTimeout(3); gInitialTab = gBrowser.selectedTab; gInitialTabURL = gBrowser.selectedBrowser.currentURI.spec; }); -async function navigateToOpenTabs(browser) { - const document = browser.contentDocument; - if (document.querySelector("named-deck").selectedViewName != "opentabs") { - await navigateToCategoryAndWait(browser.contentDocument, "opentabs"); - } -} - -function getOpenTabsComponent(browser) { - return browser.contentDocument.querySelector("named-deck > view-opentabs"); -} - -function getCards(browser) { - return getOpenTabsComponent(browser).shadowRoot.querySelectorAll( - "view-opentabs-card" - ); -} - async function cleanup() { - await SimpleTest.promiseFocus(window); + await switchToWindow(window); await promiseAllButPrimaryWindowClosed(); await BrowserTestUtils.switchTab(gBrowser, gInitialTab); await closeFirefoxViewTab(window); @@ -58,11 +38,6 @@ async function cleanup() { ); } -async function getRowsForCard(card) { - await TestUtils.waitForCondition(() => card.tabList.rowEls.length); - return card.tabList.rowEls; -} - /** * Verify that there are the expected number of cards, and that each card has * the expected URLs in order. @@ -73,10 +48,10 @@ async function getRowsForCard(card) { * The expected URLs for each card. */ async function checkTabLists(browser, expected) { - const cards = getCards(browser); + const cards = getOpenTabsCards(getOpenTabsComponent(browser)); is(cards.length, expected.length, `There are ${expected.length} windows.`); for (let i = 0; i < cards.length; i++) { - const tabItems = await getRowsForCard(cards[i]); + const tabItems = await getTabRowsForCard(cards[i]); const actual = Array.from(tabItems).map(({ url }) => url); Assert.deepEqual( actual, @@ -87,11 +62,12 @@ async function checkTabLists(browser, expected) { } add_task(async function open_tab_same_window() { + let tabChangeRaised; await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; await navigateToOpenTabs(browser); const openTabs = getOpenTabsComponent(browser); - await openTabs.openTabsTarget.readyWindowsPromise; + await NonPrivateTabs.readyWindowsPromise; await openTabs.updateComplete; await checkTabLists(browser, [[gInitialTabURL]]); @@ -99,7 +75,7 @@ add_task(async function open_tab_same_window() { browser.contentDocument, "visibilitychange" ); - let tabChangeRaised = BrowserTestUtils.waitForEvent( + tabChangeRaised = BrowserTestUtils.waitForEvent( NonPrivateTabs, "TabChange" ); @@ -114,7 +90,7 @@ add_task(async function open_tab_same_window() { const browser = viewTab.linkedBrowser; const openTabs = getOpenTabsComponent(browser); setSortOption(openTabs, "tabStripOrder"); - await openTabs.openTabsTarget.readyWindowsPromise; + await NonPrivateTabs.readyWindowsPromise; await openTabs.updateComplete; await checkTabLists(browser, [[gInitialTabURL, TEST_URL]]); @@ -122,8 +98,8 @@ add_task(async function open_tab_same_window() { browser.contentDocument, "visibilitychange" ); - const cards = getCards(browser); - const tabItems = await getRowsForCard(cards[0]); + const cards = getOpenTabsCards(openTabs); + const tabItems = await getTabRowsForCard(cards[0]); tabItems[0].mainEl.click(); await promiseHidden; }); @@ -135,8 +111,11 @@ add_task(async function open_tab_same_window() { await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; - const cards = getCards(browser); - let tabItems = await getRowsForCard(cards[0]); + const openTabs = getOpenTabsComponent(browser); + await openTabs.updateComplete; + + const cards = getOpenTabsCards(openTabs); + let tabItems = await getTabRowsForCard(cards[0]); let promiseHidden = BrowserTestUtils.waitForEvent( browser.contentDocument, @@ -154,7 +133,13 @@ add_task(async function open_tab_same_window() { await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; - let tabChangeRaised = BrowserTestUtils.waitForEvent( + const openTabs = getOpenTabsComponent(browser); + await openTabs.updateComplete; + + // sanity-check current tab order before we change it + await checkTabLists(browser, [[gInitialTabURL, TEST_URL]]); + + tabChangeRaised = BrowserTestUtils.waitForEvent( NonPrivateTabs, "TabChange" ); @@ -162,18 +147,24 @@ add_task(async function open_tab_same_window() { info("Bring the new tab to the front."); gBrowser.moveTabTo(newTab, 0); + info("Waiting for tabChangeRaised to resolve from the tab move"); await tabChangeRaised; + await openTabs.updateComplete; + await checkTabLists(browser, [[TEST_URL, gInitialTabURL]]); tabChangeRaised = BrowserTestUtils.waitForEvent( NonPrivateTabs, "TabChange" ); + info("Remove the new tab"); await BrowserTestUtils.removeTab(newTab); + info("Waiting for tabChangeRaised to resolve from removing the tab"); await tabChangeRaised; + await openTabs.updateComplete; await checkTabLists(browser, [[gInitialTabURL]]); - const [card] = getCards(browser); - const [row] = await getRowsForCard(card); + const [card] = getOpenTabsCards(getOpenTabsComponent(browser)); + const [row] = await getTabRowsForCard(card); ok( !row.shadowRoot.getElementById("fxview-tab-row-url").hidden, "The URL is displayed, since we have one window." @@ -188,25 +179,29 @@ add_task(async function open_tab_same_window() { }); add_task(async function open_tab_new_window() { - const win = await BrowserTestUtils.openNewBrowserWindow(); - let winFocused; - await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + let winBecameActive; + let tabChangeRaised; + await switchToWindow(win2); + await NonPrivateTabs.readyWindowsPromise; + + await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, TEST_URL); info("Open fxview in new window"); - await openFirefoxViewTab(win).then(async viewTab => { + await openFirefoxViewTab(win2).then(async viewTab => { const browser = viewTab.linkedBrowser; await navigateToOpenTabs(browser); const openTabs = getOpenTabsComponent(browser); setSortOption(openTabs, "tabStripOrder"); - await openTabs.openTabsTarget.readyWindowsPromise; + await NonPrivateTabs.readyWindowsPromise; await openTabs.updateComplete; await checkTabLists(browser, [ [gInitialTabURL, TEST_URL], [gInitialTabURL], ]); - const cards = getCards(browser); - const originalWinRows = await getRowsForCard(cards[1]); + const cards = getOpenTabsCards(openTabs); + const originalWinRows = await getTabRowsForCard(cards[1]); const [row] = originalWinRows; ok( row.shadowRoot.getElementById("fxview-tab-row-url").hidden, @@ -217,47 +212,60 @@ add_task(async function open_tab_new_window() { "The date is hidden, since we have two windows." ); info("Select a tab from the original window."); - let tabChangeRaised = BrowserTestUtils.waitForEvent( + tabChangeRaised = BrowserTestUtils.waitForEvent( NonPrivateTabs, "TabRecencyChange" ); - winFocused = BrowserTestUtils.waitForEvent(window, "focus", true); - originalWinRows[0].mainEl.click(); + winBecameActive = Promise.all([ + BrowserTestUtils.waitForEvent(window, "focus", true), + BrowserTestUtils.waitForEvent(window, "activate"), + ]); + ok(row.tabElement, "The row has a tabElement property"); + is( + row.tabElement.ownerGlobal, + window, + "The tabElement's ownerGlobal is our original window" + ); + info(`Clicking on row with URL: ${row.url}`); + row.mainEl.click(); + info("Waiting for TabRecencyChange event"); await tabChangeRaised; }); - info("Wait for the original window to be focused"); - await winFocused; + info("Wait for the original window to be focused & active"); + await winBecameActive; await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; await navigateToOpenTabs(browser); const openTabs = getOpenTabsComponent(browser); - await openTabs.openTabsTarget.readyWindowsPromise; + await NonPrivateTabs.readyWindowsPromise; await openTabs.updateComplete; - const cards = getCards(browser); + const cards = getOpenTabsCards(openTabs); is(cards.length, 2, "There are two windows."); - const newWinRows = await getRowsForCard(cards[1]); + const newWinRows = await getTabRowsForCard(cards[1]); info("Select a tab from the new window."); - winFocused = BrowserTestUtils.waitForEvent(win, "focus", true); - let tabChangeRaised = BrowserTestUtils.waitForEvent( + winBecameActive = Promise.all([ + BrowserTestUtils.waitForEvent(win2, "focus", true), + BrowserTestUtils.waitForEvent(win2, "activate"), + ]); + tabChangeRaised = BrowserTestUtils.waitForEvent( NonPrivateTabs, "TabRecencyChange" ); newWinRows[0].mainEl.click(); await tabChangeRaised; }); - info("Wait for the new window to be focused"); - await winFocused; + info("Wait for the new window to be focused & active"); + await winBecameActive; await cleanup(); }); add_task(async function open_tab_new_private_window() { await BrowserTestUtils.openNewBrowserWindow({ private: true }); - await SimpleTest.promiseFocus(window); await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; await navigateToOpenTabs(browser); @@ -265,7 +273,7 @@ add_task(async function open_tab_new_private_window() { await openTabs.openTabsTarget.readyWindowsPromise; await openTabs.updateComplete; - const cards = getCards(browser); + const cards = getOpenTabsCards(openTabs); is(cards.length, 1, "The private window is not displayed."); }); await cleanup(); @@ -274,6 +282,7 @@ add_task(async function open_tab_new_private_window() { add_task(async function open_tab_new_window_sort_by_recency() { info("Open new tabs in a new window."); const newWindow = await BrowserTestUtils.openNewBrowserWindow(); + await switchToWindow(newWindow); const tabs = [ newWindow.gBrowser.selectedTab, await BrowserTestUtils.openNewForegroundTab(newWindow.gBrowser, URLs[0]), @@ -285,7 +294,7 @@ add_task(async function open_tab_new_window_sort_by_recency() { await navigateToOpenTabs(linkedBrowser); const openTabs = getOpenTabsComponent(linkedBrowser); setSortOption(openTabs, "recency"); - await openTabs.openTabsTarget.readyWindowsPromise; + await NonPrivateTabs.readyWindowsPromise; await openTabs.updateComplete; await checkTabLists(linkedBrowser, [ @@ -293,13 +302,13 @@ add_task(async function open_tab_new_window_sort_by_recency() { [URLs[1], URLs[0], gInitialTabURL], ]); info("Select tabs in the new window to trigger recency changes."); - await SimpleTest.promiseFocus(newWindow); + await switchToWindow(newWindow); await BrowserTestUtils.switchTab(newWindow.gBrowser, tabs[1]); await BrowserTestUtils.switchTab(newWindow.gBrowser, tabs[0]); - await SimpleTest.promiseFocus(window); + await switchToWindow(window); await TestUtils.waitForCondition(async () => { - const [, secondCard] = getCards(linkedBrowser); - const tabItems = await getRowsForCard(secondCard); + const [, secondCard] = getOpenTabsCards(openTabs); + const tabItems = await getTabRowsForCard(secondCard); return tabItems[0].url === gInitialTabURL; }); await checkTabLists(linkedBrowser, [ @@ -311,12 +320,13 @@ add_task(async function open_tab_new_window_sort_by_recency() { }); add_task(async function styling_for_multiple_windows() { + let tabChangeRaised; await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; await navigateToOpenTabs(browser); const openTabs = getOpenTabsComponent(browser); setSortOption(openTabs, "tabStripOrder"); - await openTabs.openTabsTarget.readyWindowsPromise; + await NonPrivateTabs.readyWindowsPromise; await openTabs.updateComplete; ok( @@ -325,13 +335,10 @@ add_task(async function styling_for_multiple_windows() { ); }); - await BrowserTestUtils.openNewBrowserWindow(); - let tabChangeRaised = BrowserTestUtils.waitForEvent( - NonPrivateTabs, - "TabChange" - ); + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + info("Switching to new window"); + await switchToWindow(win2); await NonPrivateTabs.readyWindowsPromise; - await tabChangeRaised; is( NonPrivateTabs.currentWindows.length, 2, @@ -339,290 +346,54 @@ add_task(async function styling_for_multiple_windows() { ); info("switch to firefox view in the first window"); - SimpleTest.promiseFocus(window); await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; const openTabs = getOpenTabsComponent(browser); - await openTabs.openTabsTarget.readyWindowsPromise; - await openTabs.updateComplete; + const cardContainer = openTabs.shadowRoot.querySelector( + ".view-opentabs-card-container" + ); + info("waiting for card-count to reflect 2 windows"); + await BrowserTestUtils.waitForCondition(() => { + return cardContainer.getAttribute("card-count") == "two"; + }); is( openTabs.openTabsTarget.currentWindows.length, 2, "There should be 2 current windows" ); - ok( - openTabs.shadowRoot.querySelector("[card-count=two]"), - "The container shows two columns when two windows are open." + is( + cardContainer.getAttribute("card-count"), + "two", + "The container shows two columns when two windows are open" ); }); - await BrowserTestUtils.openNewBrowserWindow(); + tabChangeRaised = BrowserTestUtils.waitForEvent(NonPrivateTabs, "TabChange"); + let win3 = await BrowserTestUtils.openNewBrowserWindow(); + await switchToWindow(win3); await NonPrivateTabs.readyWindowsPromise; await tabChangeRaised; is( NonPrivateTabs.currentWindows.length, 3, - "NonPrivateTabs now has 2 currentWindows" + "NonPrivateTabs now has 3 currentWindows" ); - SimpleTest.promiseFocus(window); - await openFirefoxViewTab(window).then(async viewTab => { - const browser = viewTab.linkedBrowser; - const openTabs = getOpenTabsComponent(browser); - await openTabs.openTabsTarget.readyWindowsPromise; - await openTabs.updateComplete; - - ok( - openTabs.shadowRoot.querySelector("[card-count=three-or-more]"), - "The container shows three columns when three windows are open." - ); - }); - await cleanup(); -}); - -add_task(async function toggle_show_more_link() { - const tabEntry = url => ({ - entries: [{ url, triggeringPrincipal_base64 }], - }); - const NUMBER_OF_WINDOWS = 4; - const NUMBER_OF_TABS = 42; - const browserState = { windows: [] }; - for (let windowIndex = 0; windowIndex < NUMBER_OF_WINDOWS; windowIndex++) { - const winData = { tabs: [] }; - let tabCount = windowIndex == NUMBER_OF_WINDOWS - 1 ? NUMBER_OF_TABS : 1; - for (let i = 0; i < tabCount; i++) { - winData.tabs.push(tabEntry(`data:,Window${windowIndex}-Tab${i}`)); - } - winData.selected = winData.tabs.length; - browserState.windows.push(winData); - } - // use Session restore to batch-open windows and tabs - await SessionStoreTestUtils.promiseBrowserState(browserState); - // restoring this state requires an update to the initial tab globals - // so cleanup expects the right thing - gInitialTab = gBrowser.selectedTab; - gInitialTabURL = gBrowser.selectedBrowser.currentURI.spec; - - const windows = Array.from(Services.wm.getEnumerator("navigator:browser")); - is(windows.length, NUMBER_OF_WINDOWS, "There are four browser windows."); - - const tab = (win = window) => { - info("Tab"); - EventUtils.synthesizeKey("KEY_Tab", {}, win); - }; - - const enter = (win = window) => { - info("Enter"); - EventUtils.synthesizeKey("KEY_Enter", {}, win); - }; - - let lastCard; - - SimpleTest.promiseFocus(window); + // switch back to the original window await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; - await navigateToOpenTabs(browser); const openTabs = getOpenTabsComponent(browser); - await openTabs.openTabsTarget.readyWindowsPromise; - await openTabs.updateComplete; - - const cards = getCards(browser); - is(cards.length, NUMBER_OF_WINDOWS, "There are four windows."); - lastCard = cards[NUMBER_OF_WINDOWS - 1]; - }); - - await openFirefoxViewTab(window).then(async viewTab => { - const browser = viewTab.linkedBrowser; - const openTabs = getOpenTabsComponent(browser); - await openTabs.openTabsTarget.readyWindowsPromise; - await openTabs.updateComplete; - Assert.less( - (await getRowsForCard(lastCard)).length, - NUMBER_OF_TABS, - "Not all tabs are shown yet." - ); - info("Toggle the Show More link."); - lastCard.shadowRoot.querySelector("div[slot=footer]").click(); - await BrowserTestUtils.waitForMutationCondition( - lastCard.shadowRoot, - { childList: true, subtree: true }, - async () => (await getRowsForCard(lastCard)).length === NUMBER_OF_TABS + const cardContainer = openTabs.shadowRoot.querySelector( + ".view-opentabs-card-container" ); - info("Toggle the Show Less link."); - lastCard.shadowRoot.querySelector("div[slot=footer]").click(); - await BrowserTestUtils.waitForMutationCondition( - lastCard.shadowRoot, - { childList: true, subtree: true }, - async () => (await getRowsForCard(lastCard)).length < NUMBER_OF_TABS - ); - - // Setting this pref allows the test to run as expected with a keyboard on MacOS - await SpecialPowers.pushPrefEnv({ - set: [["accessibility.tabfocus", 7]], + await BrowserTestUtils.waitForCondition(() => { + return cardContainer.getAttribute("card-count") == "three-or-more"; }); - - info("Toggle the Show More link with keyboard."); - lastCard.shadowRoot.querySelector("card-container").summaryEl.focus(); - // Tab to first item in the list - tab(); - // Tab to the footer - tab(); - enter(); - await BrowserTestUtils.waitForMutationCondition( - lastCard.shadowRoot, - { childList: true, subtree: true }, - async () => (await getRowsForCard(lastCard)).length === NUMBER_OF_TABS - ); - - info("Toggle the Show Less link with keyboard."); - lastCard.shadowRoot.querySelector("card-container").summaryEl.focus(); - // Tab to first item in the list - tab(); - // Tab to the footer - tab(); - enter(); - await BrowserTestUtils.waitForMutationCondition( - lastCard.shadowRoot, - { childList: true, subtree: true }, - async () => (await getRowsForCard(lastCard)).length < NUMBER_OF_TABS - ); - - await SpecialPowers.popPrefEnv(); - }); - await cleanup(); -}); - -add_task(async function search_open_tabs() { - // Open a new window and navigate to TEST_URL. Then, when we search for - // TEST_URL, it should show a search result in the new window's card. - const win = await BrowserTestUtils.openNewBrowserWindow(); - await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); - - await SpecialPowers.pushPrefEnv({ - set: [["browser.firefox-view.search.enabled", true]], - }); - await openFirefoxViewTab(window).then(async viewTab => { - const browser = viewTab.linkedBrowser; - await navigateToOpenTabs(browser); - const openTabs = getOpenTabsComponent(browser); - await openTabs.openTabsTarget.readyWindowsPromise; - await openTabs.updateComplete; - - const cards = getCards(browser); - is(cards.length, 2, "There are two windows."); - const winTabs = await getRowsForCard(cards[0]); - const newWinTabs = await getRowsForCard(cards[1]); - - info("Input a search query."); - EventUtils.synthesizeMouseAtCenter(openTabs.searchTextbox, {}, content); - EventUtils.sendString(TEST_URL, content); - await TestUtils.waitForCondition( - () => openTabs.viewCards[0].tabList.rowEls.length === 0, - "There are no matching search results in the original window." - ); - await TestUtils.waitForCondition( - () => openTabs.viewCards[1].tabList.rowEls.length === 1, - "There is one matching search result in the new window." - ); - - info("Clear the search query."); - EventUtils.synthesizeMouseAtCenter( - openTabs.searchTextbox.clearButton, - {}, - content - ); - await TestUtils.waitForCondition( - () => openTabs.viewCards[0].tabList.rowEls.length === winTabs.length, - "The original window's list is restored." - ); - await TestUtils.waitForCondition( - () => openTabs.viewCards[1].tabList.rowEls.length === newWinTabs.length, - "The new window's list is restored." - ); - openTabs.searchTextbox.blur(); - - info("Input a search query with keyboard."); - EventUtils.synthesizeKey("f", { accelKey: true }, content); - EventUtils.sendString(TEST_URL, content); - await TestUtils.waitForCondition( - () => openTabs.viewCards[0].tabList.rowEls.length === 0, - "There are no matching search results in the original window." - ); - await TestUtils.waitForCondition( - () => openTabs.viewCards[1].tabList.rowEls.length === 1, - "There is one matching search result in the new window." - ); - - info("Clear the search query with keyboard."); - is( - openTabs.shadowRoot.activeElement, - openTabs.searchTextbox, - "Search input is focused" - ); - EventUtils.synthesizeKey("KEY_Tab", {}, content); ok( - openTabs.searchTextbox.clearButton.matches(":focus-visible"), - "Clear Search button is focused" - ); - EventUtils.synthesizeKey("KEY_Enter", {}, content); - await TestUtils.waitForCondition( - () => openTabs.viewCards[0].tabList.rowEls.length === winTabs.length, - "The original window's list is restored." - ); - await TestUtils.waitForCondition( - () => openTabs.viewCards[1].tabList.rowEls.length === newWinTabs.length, - "The new window's list is restored." - ); - }); - - await SpecialPowers.popPrefEnv(); - await cleanup(); -}); - -add_task(async function search_open_tabs_recent_browsing() { - const NUMBER_OF_TABS = 6; - const win = await BrowserTestUtils.openNewBrowserWindow(); - for (let i = 0; i < NUMBER_OF_TABS; i++) { - await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); - } - await SpecialPowers.pushPrefEnv({ - set: [["browser.firefox-view.search.enabled", true]], - }); - await openFirefoxViewTab(window).then(async viewTab => { - const browser = viewTab.linkedBrowser; - await navigateToCategoryAndWait(browser.contentDocument, "recentbrowsing"); - const recentBrowsing = browser.contentDocument.querySelector( - "view-recentbrowsing" - ); - - info("Input a search query."); - EventUtils.synthesizeMouseAtCenter( - recentBrowsing.searchTextbox, - {}, - content - ); - EventUtils.sendString(TEST_URL, content); - const slot = recentBrowsing.querySelector("[slot='opentabs']"); - await TestUtils.waitForCondition( - () => slot.viewCards[0].tabList.rowEls.length === 5, - "Not all search results are shown yet." + openTabs.shadowRoot.querySelector("[card-count=three-or-more]"), + "The container shows three columns when three windows are open." ); - - info("Click the Show All link."); - const showAllLink = await TestUtils.waitForCondition(() => { - const elt = slot.viewCards[0].shadowRoot.querySelector( - "[data-l10n-id='firefoxview-show-all']" - ); - EventUtils.synthesizeMouseAtCenter(elt, {}, content); - if (slot.viewCards[0].tabList.rowEls.length === NUMBER_OF_TABS) { - return elt; - } - return false; - }, "All search results are shown."); - is(showAllLink.role, "link", "The show all control is a link."); - ok(BrowserTestUtils.isHidden(showAllLink), "The show all link is hidden."); }); - await SpecialPowers.popPrefEnv(); await cleanup(); }); diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_changes.js b/browser/components/firefoxview/tests/browser/browser_opentabs_changes.js index c293afa8cd..15aba26d74 100644 --- a/browser/components/firefoxview/tests/browser/browser_opentabs_changes.js +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_changes.js @@ -1,4 +1,4 @@ -const { NonPrivateTabs, getTabsTargetForWindow } = ChromeUtils.importESModule( +const { getTabsTargetForWindow } = ChromeUtils.importESModule( "resource:///modules/OpenTabs.sys.mjs" ); let privateTabsChanges; @@ -505,8 +505,9 @@ add_task(async function test_tabsFromPrivateWindows() { }); const private2TabsChanges = getTabsTargetForWindow(private2Win); private2TabsChanges.addEventListener("TabChange", private2Listener); - ok( - privateTabsChanges !== getTabsTargetForWindow(private2Win), + Assert.notStrictEqual( + privateTabsChanges, + getTabsTargetForWindow(private2Win), "getTabsTargetForWindow creates a distinct instance for a different private window" ); diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js index 57d0f8d031..955c2363d7 100644 --- a/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js @@ -1,6 +1,9 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ +// Test regularly times out - especially with verify +requestLongerTimeout(2); + const TEST_URL1 = "about:robots"; const TEST_URL2 = "https://example.org/"; const TEST_URL3 = "about:mozilla"; @@ -20,27 +23,6 @@ const fxaDevicesWithCommands = [ }, ]; -const { NonPrivateTabs } = ChromeUtils.importESModule( - "resource:///modules/OpenTabs.sys.mjs" -); - -async function getRowsForCard(card) { - await TestUtils.waitForCondition(() => card.tabList.rowEls.length); - return card.tabList.rowEls; -} - -async function add_new_tab(URL) { - let tabChangeRaised = BrowserTestUtils.waitForEvent( - NonPrivateTabs, - "TabChange" - ); - let tab = BrowserTestUtils.addTab(gBrowser, URL); - // wait so we can reliably compare the tab URL - await BrowserTestUtils.browserLoaded(tab.linkedBrowser); - await tabChangeRaised; - return tab; -} - function getVisibleTabURLs(win = window) { return win.gBrowser.visibleTabs.map(tab => tab.linkedBrowser.currentURI.spec); } @@ -78,7 +60,7 @@ async function waitUntilRowsMatch(openTabs, cardIndex, expectedURLs) { card.shadowRoot, { characterData: true, childList: true, subtree: true }, async () => { - let rows = await getRowsForCard(card); + let rows = await getTabRowsForCard(card); return ( rows.length == expectedURLs.length && JSON.stringify(getTabRowURLs(rows)) == expectedURLsAsString @@ -106,7 +88,7 @@ async function getContextMenuPanelListForCard(card) { async function openContextMenuForItem(tabItem, card) { // click on the item's button element (more menu) // and wait for the panel list to be shown - tabItem.buttonEl.click(); + tabItem.secondaryButtonEl.click(); // NOTE: menu must populate with devices data before it can be rendered // so the creation of the panel-list can be async let panelList = await getContextMenuPanelListForCard(card); @@ -122,7 +104,7 @@ async function moreMenuSetup() { await clickFirefoxViewButton(window); const document = window.FirefoxViewHandler.tab.linkedBrowser.contentDocument; - await navigateToCategoryAndWait(document, "opentabs"); + await navigateToViewAndWait(document, "opentabs"); let openTabs = document.querySelector("view-opentabs[name=opentabs]"); setSortOption(openTabs, "tabStripOrder"); @@ -134,7 +116,7 @@ async function moreMenuSetup() { let cards = getOpenTabsCards(openTabs); is(cards.length, 1, "There is one open window."); - let rows = await getRowsForCard(cards[0]); + let rows = await getTabRowsForCard(cards[0]); let firstTab = rows[0]; @@ -148,6 +130,44 @@ async function moreMenuSetup() { return [cards, rows]; } +add_task(async function test_close_open_tab() { + await withFirefoxView({}, async browser => { + const [cards, rows] = await moreMenuSetup(); + const firstTab = rows[0]; + const tertiaryButtonEl = firstTab.tertiaryButtonEl; + + ok(tertiaryButtonEl, "Dismiss button exists"); + + await clearAllParentTelemetryEvents(); + let closeTabEvent = [ + ["firefoxview_next", "close_open_tab", "tabs", undefined], + ]; + + let tabsUpdated = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + EventUtils.synthesizeMouseAtCenter(tertiaryButtonEl, {}, content); + await tabsUpdated; + Assert.deepEqual( + getVisibleTabURLs(), + [TEST_URL2, TEST_URL3], + "First tab successfully removed" + ); + + await telemetryEvent(closeTabEvent); + + const openTabs = cards[0].ownerDocument.querySelector( + "view-opentabs[name=opentabs]" + ); + await waitUntilRowsMatch(openTabs, 0, [TEST_URL2, TEST_URL3]); + + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs[0]); + } + }); +}); + add_task(async function test_more_menus() { await withFirefoxView({}, async browser => { let win = browser.ownerGlobal; @@ -156,11 +176,11 @@ add_task(async function test_more_menus() { gBrowser.selectedTab = gBrowser.visibleTabs[0]; Assert.equal( gBrowser.selectedTab.linkedBrowser.currentURI.spec, - "about:blank", - "Selected tab is about:blank" + "about:mozilla", + "Selected tab is about:mozilla" ); - info(`Loading ${TEST_URL1} into the selected about:blank tab`); + info(`Loading ${TEST_URL1} into the selected about:mozilla tab`); let tabLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); win.gURLBar.focus(); @@ -176,64 +196,18 @@ add_task(async function test_more_menus() { "Prepared 3 open tabs" ); + // Move Tab submenu item let firstTab = rows[0]; // Open the panel list (more menu) from the first list item let panelList = await openContextMenuForItem(firstTab, cards[0]); - // Close Tab menu item - info("Panel list shown. Clicking on panel-item"); - let panelItem = panelList.querySelector( - "panel-item[data-l10n-id=fxviewtabrow-close-tab]" - ); - let panelItemButton = panelItem.shadowRoot.querySelector( - "button[role=menuitem]" - ); - ok(panelItem, "Close Tab panel item exists"); - ok( - panelItemButton, - "Close Tab panel item button with role=menuitem exists" - ); - - await clearAllParentTelemetryEvents(); - let contextMenuEvent = [ - [ - "firefoxview_next", - "context_menu", - "tabs", - undefined, - { menu_action: "close-tab", data_type: "opentabs" }, - ], - ]; - - // close a tab via the menu - let tabChangeRaised = BrowserTestUtils.waitForEvent( - NonPrivateTabs, - "TabChange" - ); - menuHidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); - panelItemButton.click(); - info("Waiting for result of closing a tab via the menu"); - await tabChangeRaised; - await cards[0].getUpdateComplete(); - await menuHidden; - await telemetryEvent(contextMenuEvent); - - Assert.deepEqual( - getVisibleTabURLs(), - [TEST_URL2, TEST_URL3], - "Got the expected 2 open tabs" - ); - let openTabs = cards[0].ownerDocument.querySelector( "view-opentabs[name=opentabs]" ); - await waitUntilRowsMatch(openTabs, 0, [TEST_URL2, TEST_URL3]); + await waitUntilRowsMatch(openTabs, 0, [TEST_URL1, TEST_URL2, TEST_URL3]); - // Move Tab submenu item - firstTab = rows[0]; - is(firstTab.url, TEST_URL2, `First tab list item is ${TEST_URL2}`); + is(firstTab.url, TEST_URL1, `First tab list item is ${TEST_URL1}`); - panelList = await openContextMenuForItem(firstTab, cards[0]); let moveTabsPanelItem = panelList.querySelector( "panel-item[data-l10n-id=fxviewtabrow-move-tab]" ); @@ -243,15 +217,14 @@ add_task(async function test_more_menus() { ); ok(moveTabsSubmenuList, "Move tabs submenu panel list exists"); - // navigate down to the "Move tabs" submenu option, and + // navigate to the "Move tabs" submenu option, and // open it with the right arrow key - EventUtils.synthesizeKey("KEY_ArrowDown", {}); shown = BrowserTestUtils.waitForEvent(moveTabsSubmenuList, "shown"); EventUtils.synthesizeKey("KEY_ArrowRight", {}); await shown; await clearAllParentTelemetryEvents(); - contextMenuEvent = [ + let contextMenuEvent = [ [ "firefoxview_next", "context_menu", @@ -264,7 +237,7 @@ add_task(async function test_more_menus() { // click on the first option, which should be "Move to the end" since // this is the first tab menuHidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); - tabChangeRaised = BrowserTestUtils.waitForEvent( + let tabChangeRaised = BrowserTestUtils.waitForEvent( NonPrivateTabs, "TabChange" ); @@ -276,7 +249,7 @@ add_task(async function test_more_menus() { Assert.deepEqual( getVisibleTabURLs(), - [TEST_URL3, TEST_URL2], + [TEST_URL2, TEST_URL3, TEST_URL1], "The last tab became the first tab" ); @@ -284,15 +257,15 @@ add_task(async function test_more_menus() { // closing a tab since it very clearly reveals the issues // outlined in bug 1852622 when there are 3 or more tabs open // and one is moved via the more menus. - await waitUntilRowsMatch(openTabs, 0, [TEST_URL3, TEST_URL2]); + await waitUntilRowsMatch(openTabs, 0, [TEST_URL2, TEST_URL3, TEST_URL1]); // Copy Link menu item (copyLink function that's called is a member of Viewpage.mjs) panelList = await openContextMenuForItem(firstTab, cards[0]); firstTab = rows[0]; - panelItem = panelList.querySelector( + let panelItem = panelList.querySelector( "panel-item[data-l10n-id=fxviewtabrow-copy-link]" ); - panelItemButton = panelItem.shadowRoot.querySelector( + let panelItemButton = panelItem.shadowRoot.querySelector( "button[role=menuitem]" ); ok(panelItem, "Copy link panel item exists"); @@ -323,7 +296,7 @@ add_task(async function test_more_menus() { "text/plain", Ci.nsIClipboard.kGlobalClipboard ); - is(copiedText, TEST_URL3, "The correct url has been copied and pasted"); + is(copiedText, TEST_URL2, "The correct url has been copied and pasted"); while (gBrowser.tabs.length > 1) { BrowserTestUtils.removeTab(gBrowser.tabs[0]); @@ -349,11 +322,11 @@ add_task(async function test_send_device_submenu() { .callsFake(() => fxaDevicesWithCommands); await withFirefoxView({}, async browser => { - // TEST_URL2 is our only tab, left over from previous test + // TEST_URL1 is our only tab, left over from previous test Assert.deepEqual( getVisibleTabURLs(), - [TEST_URL2], - `We initially have a single ${TEST_URL2} tab` + [TEST_URL1], + `We initially have a single ${TEST_URL1} tab` ); let shown; @@ -376,9 +349,7 @@ add_task(async function test_send_device_submenu() { // navigate down to the "Send tabs" submenu option, and // open it with the right arrow key - EventUtils.synthesizeKey("KEY_ArrowDown", {}); - EventUtils.synthesizeKey("KEY_ArrowDown", {}); - EventUtils.synthesizeKey("KEY_ArrowDown", {}); + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 4 }); shown = BrowserTestUtils.waitForEvent(sendTabSubmenuList, "shown"); EventUtils.synthesizeKey("KEY_ArrowRight", {}); @@ -389,9 +360,9 @@ add_task(async function test_send_device_submenu() { .expects("sendTabToDevice") .once() .withExactArgs( - TEST_URL2, + TEST_URL1, [fxaDevicesWithCommands[0]], - "mochitest index /" + "Gort! Klaatu barada nikto!" ) .returns(true); diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_more.js b/browser/components/firefoxview/tests/browser/browser_opentabs_more.js new file mode 100644 index 0000000000..fd25348699 --- /dev/null +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_more.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let gInitialTab; +let gInitialTabURL; + +// This test opens many tabs and regularly times out - especially with verify +requestLongerTimeout(2); + +add_setup(function () { + gInitialTab = gBrowser.selectedTab; + gInitialTabURL = gBrowser.selectedBrowser.currentURI.spec; +}); + +async function cleanup() { + await SimpleTest.promiseFocus(window); + await promiseAllButPrimaryWindowClosed(); + await BrowserTestUtils.switchTab(gBrowser, gInitialTab); + await closeFirefoxViewTab(window); + + // clean up extra tabs + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs.at(-1)); + } + + is( + BrowserWindowTracker.orderedWindows.length, + 1, + "One window at the end of test cleanup" + ); + Assert.deepEqual( + gBrowser.tabs.map(tab => tab.linkedBrowser.currentURI.spec), + [gInitialTabURL], + "One about:blank tab open at the end up test cleanup" + ); +} + +add_task(async function toggle_show_more_link() { + const tabEntry = url => ({ + entries: [{ url, triggeringPrincipal_base64 }], + }); + const NUMBER_OF_WINDOWS = 4; + const NUMBER_OF_TABS = 42; + const browserState = { windows: [] }; + for (let windowIndex = 0; windowIndex < NUMBER_OF_WINDOWS; windowIndex++) { + const winData = { tabs: [] }; + let tabCount = windowIndex == NUMBER_OF_WINDOWS - 1 ? NUMBER_OF_TABS : 1; + for (let i = 0; i < tabCount; i++) { + winData.tabs.push(tabEntry(`data:,Window${windowIndex}-Tab${i}`)); + } + winData.selected = winData.tabs.length; + browserState.windows.push(winData); + } + // use Session restore to batch-open windows and tabs + info(`Restoring to browserState: ${JSON.stringify(browserState, null, 2)}`); + await SessionStoreTestUtils.promiseBrowserState(browserState); + info("Windows and tabs opened, waiting for readyWindowsPromise"); + await NonPrivateTabs.readyWindowsPromise; + info("readyWindowsPromise resolved"); + + // restoring this state requires an update to the initial tab globals + // so cleanup expects the right thing + gInitialTab = gBrowser.selectedTab; + gInitialTabURL = gBrowser.selectedBrowser.currentURI.spec; + + const windows = Array.from(Services.wm.getEnumerator("navigator:browser")); + is(windows.length, NUMBER_OF_WINDOWS, "There are four browser windows."); + + const tab = (win = window) => { + info("Tab"); + EventUtils.synthesizeKey("KEY_Tab", {}, win); + }; + + const enter = (win = window) => { + info("Enter"); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + }; + + let lastCard; + + await openFirefoxViewTab(window).then(async viewTab => { + const browser = viewTab.linkedBrowser; + await navigateToOpenTabs(browser); + const openTabs = getOpenTabsComponent(browser); + await openTabs.updateComplete; + + let cards; + info(`Waiting for ${NUMBER_OF_WINDOWS} of window cards`); + await BrowserTestUtils.waitForCondition(() => { + cards = getOpenTabsCards(openTabs); + return cards.length == NUMBER_OF_WINDOWS; + }); + is(cards.length, NUMBER_OF_WINDOWS, "There are four windows."); + lastCard = cards[NUMBER_OF_WINDOWS - 1]; + + Assert.less( + (await getTabRowsForCard(lastCard)).length, + NUMBER_OF_TABS, + "Not all tabs are shown yet." + ); + info("Toggle the Show More link."); + lastCard.shadowRoot.querySelector("div[slot=footer]").click(); + await BrowserTestUtils.waitForMutationCondition( + lastCard.shadowRoot, + { childList: true, subtree: true }, + async () => (await getTabRowsForCard(lastCard)).length === NUMBER_OF_TABS + ); + + info("Toggle the Show Less link."); + lastCard.shadowRoot.querySelector("div[slot=footer]").click(); + await BrowserTestUtils.waitForMutationCondition( + lastCard.shadowRoot, + { childList: true, subtree: true }, + async () => (await getTabRowsForCard(lastCard)).length < NUMBER_OF_TABS + ); + + // Setting this pref allows the test to run as expected with a keyboard on MacOS + await SpecialPowers.pushPrefEnv({ + set: [["accessibility.tabfocus", 7]], + }); + + info("Toggle the Show More link with keyboard."); + lastCard.shadowRoot.querySelector("card-container").summaryEl.focus(); + // Tab to first item in the list + tab(); + // Tab to the footer + tab(); + enter(); + await BrowserTestUtils.waitForMutationCondition( + lastCard.shadowRoot, + { childList: true, subtree: true }, + async () => (await getTabRowsForCard(lastCard)).length === NUMBER_OF_TABS + ); + + info("Toggle the Show Less link with keyboard."); + lastCard.shadowRoot.querySelector("card-container").summaryEl.focus(); + // Tab to first item in the list + tab(); + // Tab to the footer + tab(); + enter(); + await BrowserTestUtils.waitForMutationCondition( + lastCard.shadowRoot, + { childList: true, subtree: true }, + async () => (await getTabRowsForCard(lastCard)).length < NUMBER_OF_TABS + ); + + await SpecialPowers.popPrefEnv(); + }); + await cleanup(); +}); diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_pinned_tabs.js b/browser/components/firefoxview/tests/browser/browser_opentabs_pinned_tabs.js new file mode 100644 index 0000000000..d74812bca5 --- /dev/null +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_pinned_tabs.js @@ -0,0 +1,481 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let pageWithAlert = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/browser/browser/base/content/test/tabPrompts/openPromptOffTimeout.html"; +let pageWithSound = + "http://mochi.test:8888/browser/dom/base/test/file_audioLoop.html"; + +function cleanup() { + // Cleanup + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs[1]); + } +} + +const arrowDown = async tabList => { + info("Arrow down"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + await tabList.getUpdateComplete(); +}; +const arrowUp = async tabList => { + info("Arrow up"); + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + await tabList.getUpdateComplete(); +}; +const arrowRight = async tabList => { + info("Arrow right"); + EventUtils.synthesizeKey("KEY_ArrowRight", {}); + await tabList.getUpdateComplete(); +}; +const arrowLeft = async tabList => { + info("Arrow left"); + EventUtils.synthesizeKey("KEY_ArrowLeft", {}); + await tabList.getUpdateComplete(); +}; + +add_task(async function test_pin_unpin_open_tab() { + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + await navigateToViewAndWait(document, "opentabs"); + + let openTabs = document.querySelector("view-opentabs[name=opentabs]"); + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length + ); + await openTabs.openTabsTarget.readyWindowsPromise; + let card = openTabs.viewCards[0]; + let openTabEl = card.tabList.rowEls[0]; + let tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + // Pin tab + EventUtils.synthesizeMouseAtCenter( + openTabEl.secondaryButtonEl, + {}, + content + ); + await TestUtils.waitForCondition(() => card.tabContextMenu.panelList); + let panelList = card.tabContextMenu.panelList; + await BrowserTestUtils.waitForEvent(panelList, "shown"); + info("The context menu is shown when clicking the tab's 'more' button"); + + let pinTabPanelItem = panelList.querySelector( + "panel-item[data-l10n-id=fxviewtabrow-pin-tab]" + ); + + await clearAllParentTelemetryEvents(); + let contextMenuEvent = [ + [ + "firefoxview_next", + "context_menu", + "tabs", + undefined, + { menu_action: "pin-tab", data_type: "opentabs" }, + ], + ]; + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + // Unpin tab + EventUtils.synthesizeMouseAtCenter(pinTabPanelItem, {}, content); + info("Pin Tab context menu option clicked."); + + await tabChangeRaised; + await openTabs.updateComplete; + + let pinnedTab = card.tabList.rowEls[0]; + await TestUtils.waitForCondition(() => + pinnedTab.indicators.includes("pinned") + ); + + // Check aria roles + let listWrapper = card.tabList.shadowRoot.querySelector(".fxview-tab-list"); + ok( + Array.from(listWrapper.classList).includes("pinned"), + "The tab list has the 'pinned' class as expected." + ); + + Assert.strictEqual( + listWrapper.getAttribute("role"), + "tablist", + "The list wrapper has an aria-role of 'tablist'" + ); + Assert.strictEqual( + pinnedTab.pinnedTabButtonEl.getAttribute("role"), + "tab", + "The pinned tab's button element has a role of 'tab'" + ); + + // Open context menu + EventUtils.synthesizeMouseAtCenter( + pinnedTab, + { type: "contextmenu" }, + content + ); + await TestUtils.waitForCondition(() => card.tabContextMenu.panelList); + panelList = card.tabContextMenu.panelList; + await BrowserTestUtils.waitForEvent(panelList, "shown"); + info("The context menu is shown when right clicking the pinned tab"); + + let unpinTabPanelItem = panelList.querySelector( + "panel-item[data-l10n-id=fxviewtabrow-unpin-tab]" + ); + + await clearAllParentTelemetryEvents(); + contextMenuEvent = [ + [ + "firefoxview_next", + "context_menu", + "tabs", + undefined, + { menu_action: "unpin-tab", data_type: "opentabs" }, + ], + ]; + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + // Unpin tab + EventUtils.synthesizeMouseAtCenter(unpinTabPanelItem, {}, content); + info("Unpin Tab context menu option clicked."); + + await tabChangeRaised; + await openTabs.updateComplete; + await telemetryEvent(contextMenuEvent); + }); + cleanup(); +}); + +add_task(async function test_indicator_pinned_tabs_with_keyboard() { + await add_new_tab(URLs[0]); + await add_new_tab(URLs[1]); + await add_new_tab(pageWithSound); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + await navigateToViewAndWait(document, "opentabs"); + + let openTabs = document.querySelector("view-opentabs[name=opentabs]"); + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length + ); + await openTabs.openTabsTarget.readyWindowsPromise; + setSortOption(openTabs, "tabStripOrder"); + let card = openTabs.viewCards[0]; + + let openedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + pageWithAlert, + true + ); + let openedTabGotAttentionPromise = BrowserTestUtils.waitForAttribute( + "attention", + openedTab + ); + + await switchToFxViewTab(); + + await openedTabGotAttentionPromise; + + let tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + // Pin 2 of 5 tabs + browser.ownerGlobal.gBrowser.tabs.forEach((tab, i) => { + if (i > 2) { + browser.ownerGlobal.gBrowser.pinTab(tab); + } + }); + + await tabChangeRaised; + await openTabs.updateComplete; + + let soundPlayingPinnedTab = card.tabList.rowEls[0]; + let attentionPinnedTab = card.tabList.rowEls[1]; + let firstUnpinnedTab = card.tabList.rowEls[2]; + let secondUnpinnedTab = card.tabList.rowEls[3]; + + // Check soundplaying indicator + ok( + soundPlayingPinnedTab.indicators.includes("pinned") && + soundPlayingPinnedTab.indicators.includes("soundplaying") && + soundPlayingPinnedTab.mediaButtonEl, + "The first pinned tab has the 'sound playing' indicator." + ); + + soundPlayingPinnedTab.pinnedTabButtonEl.focus(); + ok( + isActiveElement(soundPlayingPinnedTab.pinnedTabButtonEl), + "Focus should be on the first pinned tab's button element." + ); + info("First pinned tab has focus"); + + // Test mute/unmute + EventUtils.synthesizeKey("m", { ctrlKey: true }); + await TestUtils.waitForCondition(() => + soundPlayingPinnedTab.indicators.includes("muted") + ); + EventUtils.synthesizeKey("m", { ctrlKey: true }); + await TestUtils.waitForCondition( + () => !soundPlayingPinnedTab.indicators.includes("muted") + ); + + await arrowRight(card.tabList); + ok( + isActiveElement(attentionPinnedTab.pinnedTabButtonEl), + "Focus should be on the second pinned tab's button element." + ); + + // Check notification dot indicator + ok( + attentionPinnedTab.indicators.includes("pinned") && + attentionPinnedTab.indicators.includes("attention") && + Array.from( + attentionPinnedTab.shadowRoot.querySelector( + ".fxview-tab-row-favicon-wrapper" + ).classList + ).includes("attention"), + "The second pinned tab has the 'attention' indicator." + ); + + await arrowDown(card.tabList); + ok( + isActiveElement(firstUnpinnedTab.mainEl), + "Focus should be on the first unpinned tab's main/link element." + ); + + await arrowRight(card.tabList); + ok( + isActiveElement(firstUnpinnedTab.secondaryButtonEl), + "Focus should be on the first unpinned tab's secondary/more button element." + ); + + await arrowUp(card.tabList); + ok( + isActiveElement(attentionPinnedTab.pinnedTabButtonEl), + "Focus should be on the second pinned tab's button element." + ); + + await arrowRight(card.tabList); + ok( + isActiveElement(firstUnpinnedTab.secondaryButtonEl), + "Focus should be on the first unpinned tab's secondary/more button element." + ); + + await arrowUp(card.tabList); + ok( + isActiveElement(attentionPinnedTab.pinnedTabButtonEl), + "Focus should be on the second pinned tab's button element." + ); + + await arrowLeft(card.tabList); + ok( + isActiveElement(soundPlayingPinnedTab.pinnedTabButtonEl), + "Focus should be on the first pinned tab's button element." + ); + + await arrowDown(card.tabList); + ok( + isActiveElement(firstUnpinnedTab.secondaryButtonEl), + "Focus should be on the first unpinned tab's secondary/more button element." + ); + + await arrowDown(card.tabList); + ok( + isActiveElement(secondUnpinnedTab.secondaryButtonEl), + "Focus should be on the second unpinned tab's secondary/more button element." + ); + + // Switch back to other tab to close prompt before cleanup + await BrowserTestUtils.switchTab(gBrowser, openedTab); + EventUtils.synthesizeKey("KEY_Enter", {}); + }); + cleanup(); +}); + +add_task(async function test_mute_unmute_pinned_tab() { + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + await navigateToViewAndWait(document, "opentabs"); + + let openTabs = document.querySelector("view-opentabs[name=opentabs]"); + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length + ); + await openTabs.openTabsTarget.readyWindowsPromise; + let card = openTabs.viewCards[0]; + let openTabEl = card.tabList.rowEls[0]; + let tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + // Mute tab + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a not focusable button within a pinned tab + // control. A keyboard-only user could mute/unmute this pinned tab via the + // context menu, while we do not want to create an additional, unnecessary + // tabstop for this control, therefore this rule check shall be ignored by + // a11y_checks suite. + AccessibilityUtils.setEnv({ focusableRule: false }); + EventUtils.synthesizeMouseAtCenter(openTabEl.mediaButtonEl, {}, content); + AccessibilityUtils.resetEnv(); + info("Mute Tab button clicked."); + + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + await tabChangeRaised; + await openTabs.updateComplete; + + let mutedTab = card.tabList.rowEls[0]; + await TestUtils.waitForCondition(() => + mutedTab.indicators.includes("muted") + ); + + // Unmute tab + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a not focusable button within a pinned tab + // control. A keyboard-only user could mute/unmute this pinned tab via the + // context menu, while we do not want to create an additional, unnecessary + // tabstop for this control, therefore this rule check shall be ignored by + // a11y_checks suite. + AccessibilityUtils.setEnv({ focusableRule: false }); + EventUtils.synthesizeMouseAtCenter(openTabEl.mediaButtonEl, {}, content); + AccessibilityUtils.resetEnv(); + info("Unmute Tab button clicked."); + + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + await tabChangeRaised; + await openTabs.updateComplete; + + let unmutedTab = card.tabList.rowEls[0]; + await TestUtils.waitForCondition( + () => !unmutedTab.indicators.includes("muted") + ); + }); + cleanup(); +}); + +add_task(async function test_mute_unmute_with_context_menu() { + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + await navigateToViewAndWait(document, "opentabs"); + + let openTabs = document.querySelector("view-opentabs[name=opentabs]"); + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length + ); + await openTabs.openTabsTarget.readyWindowsPromise; + let card = openTabs.viewCards[0]; + let openTabEl = card.tabList.rowEls[0]; + let tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + // Mute tab + EventUtils.synthesizeMouseAtCenter( + openTabEl.pinnedTabButtonEl, + { type: "contextmenu" }, + content + ); + await TestUtils.waitForCondition(() => card.tabContextMenu.panelList); + let panelList = card.tabContextMenu.panelList; + await BrowserTestUtils.waitForEvent(panelList, "shown"); + info("The context menu is shown when clicking the tab's 'more' button"); + + let pinTabPanelItem = panelList.querySelector( + "panel-item[data-l10n-id=fxviewtabrow-mute-tab]" + ); + + await clearAllParentTelemetryEvents(); + let contextMenuEvent = [ + [ + "firefoxview_next", + "context_menu", + "tabs", + undefined, + { menu_action: "mute-tab", data_type: "opentabs" }, + ], + ]; + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + // Mute tab + EventUtils.synthesizeMouseAtCenter(pinTabPanelItem, {}, content); + info("Mute Tab context menu option clicked."); + + await tabChangeRaised; + await openTabs.updateComplete; + + let mutedTab = card.tabList.rowEls[0]; + await TestUtils.waitForCondition(() => + mutedTab.indicators.includes("muted") + ); + + // Open context menu + EventUtils.synthesizeMouseAtCenter( + mutedTab, + { type: "contextmenu" }, + content + ); + await TestUtils.waitForCondition(() => card.tabContextMenu.panelList); + panelList = card.tabContextMenu.panelList; + await BrowserTestUtils.waitForEvent(panelList, "shown"); + info("The context menu is shown when right clicking the pinned tab"); + + let unmuteTabPanelItem = panelList.querySelector( + "panel-item[data-l10n-id=fxviewtabrow-unmute-tab]" + ); + + await clearAllParentTelemetryEvents(); + contextMenuEvent = [ + [ + "firefoxview_next", + "context_menu", + "tabs", + undefined, + { menu_action: "unmute-tab", data_type: "opentabs" }, + ], + ]; + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + // Unpin tab + EventUtils.synthesizeMouseAtCenter(unmuteTabPanelItem, {}, content); + info("Unmute Tab context menu option clicked."); + + await tabChangeRaised; + await openTabs.updateComplete; + await telemetryEvent(contextMenuEvent); + + let unmutedTab = card.tabList.rowEls[0]; + await TestUtils.waitForCondition( + () => !unmutedTab.indicators.includes("muted") + ); + }); + cleanup(); +}); diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js b/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js index e5beb4700a..ee3f9981e1 100644 --- a/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js @@ -13,9 +13,6 @@ const tabURL4 = "data:,Tab4"; let gInitialTab; let gInitialTabURL; -const { NonPrivateTabs } = ChromeUtils.importESModule( - "resource:///modules/OpenTabs.sys.mjs" -); add_setup(function () { gInitialTab = gBrowser.selectedTab; @@ -158,12 +155,12 @@ function getOpenTabsComponent(browser) { async function checkTabList(browser, expected) { const tabsView = getOpenTabsComponent(browser); - const openTabsCard = tabsView.shadowRoot.querySelector("view-opentabs-card"); - await tabsView.getUpdateComplete(); - const tabList = openTabsCard.shadowRoot.querySelector("fxview-tab-list"); - Assert.ok(tabList, "Found the tab list element"); - await TestUtils.waitForCondition(() => tabList.rowEls.length); - let actual = Array.from(tabList.rowEls).map(row => row.url); + const [openTabsCard] = getOpenTabsCards(tabsView); + await openTabsCard.updateComplete; + + const tabListRows = await getTabRowsForCard(openTabsCard); + Assert.ok(tabListRows, "Found the tab list element"); + let actual = Array.from(tabListRows).map(row => row.url); Assert.deepEqual( actual, expected, @@ -255,7 +252,7 @@ add_task(async function test_multiple_window_tabs() { NonPrivateTabs, "TabRecencyChange" ); - await SimpleTest.promiseFocus(win1); + await switchToWindow(win1); await tabChangeRaised; Assert.equal( tabUrl(win1.gBrowser.selectedTab), @@ -308,17 +305,20 @@ add_task(async function test_windows_activation() { await openFirefoxViewTab(win1).then(tab => (fxViewTab = tab)); const win2 = await BrowserTestUtils.openNewBrowserWindow(); + await switchToWindow(win2); await prepareOpenTabs([tabURL2], win2); const win3 = await BrowserTestUtils.openNewBrowserWindow(); + await switchToWindow(win3); await prepareOpenTabs([tabURL3], win3); - await tabChangeRaised; tabChangeRaised = BrowserTestUtils.waitForEvent( NonPrivateTabs, "TabRecencyChange" ); - await SimpleTest.promiseFocus(win1); + info("Switching back to win 1"); + await switchToWindow(win1); + info("Waiting for tabChangeRaised to resolve"); await tabChangeRaised; const browser = fxViewTab.linkedBrowser; @@ -329,7 +329,7 @@ add_task(async function test_windows_activation() { NonPrivateTabs, "TabRecencyChange" ); - await SimpleTest.promiseFocus(win2); + await switchToWindow(win2); await tabChangeRaised; await checkTabList(browser, [tabURL2, tabURL3, tabURL1]); await cleanup(win2, win3); @@ -341,6 +341,7 @@ add_task(async function test_minimize_restore_windows() { await prepareOpenTabs([tabURL1, tabURL2]); const win2 = await BrowserTestUtils.openNewBrowserWindow(); await prepareOpenTabs([tabURL3, tabURL4], win2); + await NonPrivateTabs.readyWindowsPromise; // to avoid confusing the results by activating different windows, // check fxview in the current window - which is win2 @@ -374,7 +375,7 @@ add_task(async function test_minimize_restore_windows() { ); await minimizeWindow(win2); info("Focusing win1, where tab2 is selected - making it most recent"); - await SimpleTest.promiseFocus(win1); + await switchToWindow(win1); await tabChangeRaised; Assert.equal( @@ -395,7 +396,7 @@ add_task(async function test_minimize_restore_windows() { "TabRecencyChange" ); await restoreWindow(win2); - await SimpleTest.promiseFocus(win2); + await switchToWindow(win2); await tabChangeRaised; info( diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_search.js b/browser/components/firefoxview/tests/browser/browser_opentabs_search.js new file mode 100644 index 0000000000..173bf1a623 --- /dev/null +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_search.js @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = "about:robots"; +let gInitialTab; +let gInitialTabURL; + +add_setup(function () { + gInitialTab = gBrowser.selectedTab; + gInitialTabURL = gBrowser.selectedBrowser.currentURI.spec; +}); + +async function cleanup() { + await SimpleTest.promiseFocus(window); + await promiseAllButPrimaryWindowClosed(); + await BrowserTestUtils.switchTab(gBrowser, gInitialTab); + await closeFirefoxViewTab(window); + + // clean up extra tabs + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs.at(-1)); + } +} + +add_task(async function search_open_tabs() { + // Open a new window and navigate to TEST_URL. Then, when we search for + // TEST_URL, it should show a search result in the new window's card. + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + await switchToWindow(win2); + await NonPrivateTabs.readyWindowsPromise; + await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, TEST_URL); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.firefox-view.search.enabled", true]], + }); + await openFirefoxViewTab(window).then(async viewTab => { + const browser = viewTab.linkedBrowser; + await navigateToOpenTabs(browser); + const openTabs = getOpenTabsComponent(browser); + await openTabs.updateComplete; + + const cards = getOpenTabsCards(openTabs); + is(cards.length, 2, "There are two windows."); + const winTabs = await getTabRowsForCard(cards[0]); + const newWinTabs = await getTabRowsForCard(cards[1]); + + info("Input a search query."); + EventUtils.synthesizeMouseAtCenter(openTabs.searchTextbox, {}, content); + EventUtils.sendString(TEST_URL, content); + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length === 0, + "There are no matching search results in the original window." + ); + await TestUtils.waitForCondition( + () => openTabs.viewCards[1].tabList.rowEls.length === 1, + "There is one matching search result in the new window." + ); + + info("Clear the search query."); + EventUtils.synthesizeMouseAtCenter( + openTabs.searchTextbox.clearButton, + {}, + content + ); + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length === winTabs.length, + "The original window's list is restored." + ); + await TestUtils.waitForCondition( + () => openTabs.viewCards[1].tabList.rowEls.length === newWinTabs.length, + "The new window's list is restored." + ); + openTabs.searchTextbox.blur(); + + info("Input a search query with keyboard."); + EventUtils.synthesizeKey("f", { accelKey: true }, content); + EventUtils.sendString(TEST_URL, content); + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length === 0, + "There are no matching search results in the original window." + ); + await TestUtils.waitForCondition( + () => openTabs.viewCards[1].tabList.rowEls.length === 1, + "There is one matching search result in the new window." + ); + + info("Clear the search query with keyboard."); + is( + openTabs.shadowRoot.activeElement, + openTabs.searchTextbox, + "Search input is focused" + ); + EventUtils.synthesizeKey("KEY_Tab", {}, content); + ok( + openTabs.searchTextbox.clearButton.matches(":focus-visible"), + "Clear Search button is focused" + ); + EventUtils.synthesizeKey("KEY_Enter", {}, content); + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length === winTabs.length, + "The original window's list is restored." + ); + await TestUtils.waitForCondition( + () => openTabs.viewCards[1].tabList.rowEls.length === newWinTabs.length, + "The new window's list is restored." + ); + }); + + await SpecialPowers.popPrefEnv(); + await cleanup(); +}); + +add_task(async function search_open_tabs_recent_browsing() { + const NUMBER_OF_TABS = 6; + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + await switchToWindow(win2); + await NonPrivateTabs.readyWindowsPromise; + + for (let i = 0; i < NUMBER_OF_TABS; i++) { + await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, TEST_URL); + } + await SpecialPowers.pushPrefEnv({ + set: [["browser.firefox-view.search.enabled", true]], + }); + await openFirefoxViewTab(window).then(async viewTab => { + const browser = viewTab.linkedBrowser; + await navigateToViewAndWait(browser.contentDocument, "recentbrowsing"); + const recentBrowsing = browser.contentDocument.querySelector( + "view-recentbrowsing" + ); + + info("Input a search query."); + EventUtils.synthesizeMouseAtCenter( + recentBrowsing.searchTextbox, + {}, + content + ); + EventUtils.sendString(TEST_URL, content); + const slot = recentBrowsing.querySelector("[slot='opentabs']"); + await TestUtils.waitForCondition( + () => slot.viewCards[0].tabList.rowEls.length === 5, + "Not all search results are shown yet." + ); + + info("Click the Show All link."); + const showAllLink = await TestUtils.waitForCondition(() => { + const elt = slot.viewCards[0].shadowRoot.querySelector( + "[data-l10n-id='firefoxview-show-all']" + ); + EventUtils.synthesizeMouseAtCenter(elt, {}, content); + if (slot.viewCards[0].tabList.rowEls.length === NUMBER_OF_TABS) { + return elt; + } + return false; + }, "All search results are shown."); + is(showAllLink.role, "link", "The show all control is a link."); + ok(BrowserTestUtils.isHidden(showAllLink), "The show all link is hidden."); + }); + await SpecialPowers.popPrefEnv(); + await cleanup(); +}); diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js b/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js index 1375052125..78fab976ed 100644 --- a/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js @@ -1,9 +1,7 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -const { NonPrivateTabs } = ChromeUtils.importESModule( - "resource:///modules/OpenTabs.sys.mjs" -); +requestLongerTimeout(2); let pageWithAlert = // eslint-disable-next-line @microsoft/sdl/no-insecure-url @@ -11,18 +9,12 @@ let pageWithAlert = let pageWithSound = "http://mochi.test:8888/browser/dom/base/test/file_audioLoop.html"; -function cleanup() { - // Cleanup - while (gBrowser.tabs.length > 1) { - BrowserTestUtils.removeTab(gBrowser.tabs[0]); - } -} - add_task(async function test_notification_dot_indicator() { + clearHistory(); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; let win = browser.ownerGlobal; - await navigateToCategoryAndWait(document, "opentabs"); + await navigateToViewAndWait(document, "opentabs"); // load page that opens prompt when page is hidden let openedTab = await BrowserTestUtils.openNewForegroundTab( gBrowser, @@ -47,9 +39,10 @@ add_task(async function test_notification_dot_indicator() { await tabChangeRaised; await openTabs.updateComplete; - await TestUtils.waitForCondition( - () => openTabs.viewCards[0].tabList.rowEls[1].attention, - "The opened tab doesn't have the attention property, so no notification dot is shown." + await TestUtils.waitForCondition(() => + Array.from(openTabs.viewCards[0].tabList.rowEls).some(rowEl => { + return rowEl.indicators.includes("attention"); + }) ); info("The newly opened tab has a notification dot."); @@ -58,11 +51,12 @@ add_task(async function test_notification_dot_indicator() { await BrowserTestUtils.switchTab(gBrowser, openedTab); EventUtils.synthesizeKey("KEY_Enter", {}, win); - cleanup(); + cleanupTabs(); }); }); add_task(async function test_container_indicator() { + clearHistory(); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; let win = browser.ownerGlobal; @@ -79,7 +73,7 @@ add_task(async function test_container_indicator() { URLs[0] ); - await navigateToCategoryAndWait(document, "opentabs"); + await navigateToViewAndWait(document, "opentabs"); let openTabs = document.querySelector("view-opentabs[name=opentabs]"); @@ -95,10 +89,14 @@ add_task(async function test_container_indicator() { ); info("openTabs component has finished updating."); - let containerTabElem = openTabs.viewCards[0].tabList.rowEls[1]; + let containerTabElem; await TestUtils.waitForCondition( - () => containerTabElem.containerObj, + () => + Array.from(openTabs.viewCards[0].tabList.rowEls).some(rowEl => { + containerTabElem = rowEl; + return rowEl.containerObj; + }), "The container tab element isn't marked in Fx View." ); @@ -111,14 +109,15 @@ add_task(async function test_container_indicator() { info("The newly opened tab is marked as a container tab."); - cleanup(); + cleanupTabs(); }); }); add_task(async function test_sound_playing_muted_indicator() { + clearHistory(); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "opentabs"); + await navigateToViewAndWait(document, "opentabs"); // Load a page in a container tab let soundTab = await BrowserTestUtils.openNewForegroundTab( @@ -146,9 +145,13 @@ add_task(async function test_sound_playing_muted_indicator() { "The tab list hasn't rendered." ); - let soundPlayingTabElem = openTabs.viewCards[0].tabList.rowEls[1]; - - await TestUtils.waitForCondition(() => soundPlayingTabElem.soundPlaying); + let soundPlayingTabElem; + await TestUtils.waitForCondition(() => + Array.from(openTabs.viewCards[0].tabList.rowEls).some(rowEl => { + soundPlayingTabElem = rowEl; + return rowEl.indicators.includes("soundplaying"); + }) + ); ok( soundPlayingTabElem.mediaButtonEl, @@ -174,7 +177,9 @@ add_task(async function test_sound_playing_muted_indicator() { await tabChangeRaised; await openTabs.updateComplete; - await TestUtils.waitForCondition(() => soundPlayingTabElem.muted); + await TestUtils.waitForCondition(() => + soundPlayingTabElem.indicators.includes("muted") + ); ok( soundPlayingTabElem.mediaButtonEl, @@ -185,7 +190,9 @@ add_task(async function test_sound_playing_muted_indicator() { soundTab.toggleMuteAudio(); await tabChangeRaised; await openTabs.updateComplete; - await TestUtils.waitForCondition(() => soundPlayingTabElem.soundPlaying); + await TestUtils.waitForCondition(() => + soundPlayingTabElem.indicators.includes("soundplaying") + ); ok( soundPlayingTabElem.mediaButtonEl, @@ -195,13 +202,80 @@ add_task(async function test_sound_playing_muted_indicator() { soundTab.toggleMuteAudio(); await tabChangeRaised; await openTabs.updateComplete; - await TestUtils.waitForCondition(() => soundPlayingTabElem.muted); + await TestUtils.waitForCondition(() => + soundPlayingTabElem.indicators.includes("muted") + ); ok( soundPlayingTabElem.mediaButtonEl, "The tab has the unmute button showing." ); - cleanup(); + cleanupTabs(); + }); +}); + +add_task(async function test_bookmark_indicator() { + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URLs[0]); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + await navigateToViewAndWait(document, "opentabs"); + const openTabs = document.querySelector("view-opentabs[name=opentabs]"); + setSortOption(openTabs, "recency"); + let rowEl = await TestUtils.waitForCondition( + () => openTabs.viewCards[0]?.tabList.rowEls[0] + ); + + info("Bookmark a tab while Firefox View is active."); + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: URLs[0], + }); + await TestUtils.waitForCondition( + () => rowEl.shadowRoot.querySelector(".bookmark"), + "Tab shows the bookmark star." + ); + await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + url: URLs[1], + }); + await TestUtils.waitForCondition( + () => !rowEl.shadowRoot.querySelector(".bookmark"), + "The bookmark star is removed." + ); + bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: URLs[0], + }); + await TestUtils.waitForCondition( + () => rowEl.shadowRoot.querySelector(".bookmark"), + "The bookmark star is restored." + ); + await PlacesUtils.bookmarks.remove(bookmark.guid); + await TestUtils.waitForCondition( + () => !rowEl.shadowRoot.querySelector(".bookmark"), + "The bookmark star is removed again." + ); + + info("Bookmark a tab while Firefox View is inactive."); + await BrowserTestUtils.switchTab(gBrowser, tab); + bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: URLs[0], + }); + await switchToFxViewTab(); + await TestUtils.waitForCondition( + () => rowEl.shadowRoot.querySelector(".bookmark"), + "Tab shows the bookmark star." + ); + await BrowserTestUtils.switchTab(gBrowser, tab); + await PlacesUtils.bookmarks.remove(bookmark.guid); + await switchToFxViewTab(); + await TestUtils.waitForCondition( + () => !rowEl.shadowRoot.querySelector(".bookmark"), + "The bookmark star is removed." + ); }); + await cleanupTabs(); + await PlacesUtils.bookmarks.eraseEverything(); }); diff --git a/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js index 313d86416e..fcfcf20562 100644 --- a/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js @@ -196,7 +196,7 @@ add_task(async function test_initial_closed_tab() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; is(document.location.href, getFirefoxViewURL()); - await navigateToCategoryAndWait(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); let { cleanup } = await prepareSingleClosedTab(); await switchToFxViewTab(window); let [listItems] = await waitForRecentlyClosedTabsList(document); @@ -220,7 +220,7 @@ add_task(async function test_list_ordering() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; await clearAllParentTelemetryEvents(); - navigateToCategory(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); let [cardMainSlotNode, listItems] = await waitForRecentlyClosedTabsList( document ); @@ -248,7 +248,7 @@ add_task(async function test_list_updates() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); let [listElem, listItems] = await waitForRecentlyClosedTabsList(document); Assert.deepEqual( @@ -321,7 +321,7 @@ add_task(async function test_restore_tab() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); let [listElem, listItems] = await waitForRecentlyClosedTabsList(document); Assert.deepEqual( @@ -365,7 +365,7 @@ add_task(async function test_dismiss_tab() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); let [listElem, listItems] = await waitForRecentlyClosedTabsList(document); await clearAllParentTelemetryEvents(); @@ -429,7 +429,7 @@ add_task(async function test_empty_states() { const { document } = browser.contentWindow; is(document.location.href, "about:firefoxview"); - navigateToCategory(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); let recentlyClosedComponent = document.querySelector( "view-recentlyclosed:not([slot=recentlyclosed])" ); @@ -479,7 +479,7 @@ add_task(async function test_observers_removed_when_view_is_hidden() { await withFirefoxView({}, async function (browser) { const { document } = browser.contentWindow; - navigateToCategory(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); const [listElem] = await waitForRecentlyClosedTabsList(document); is(listElem.rowEls.length, 1); @@ -510,7 +510,7 @@ add_task(async function test_search() { let { cleanup, expectedURLs } = await prepareClosedTabs(); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "recentlyclosed"); + await navigateToViewAndWait(document, "recentlyclosed"); const [listElem] = await waitForRecentlyClosedTabsList(document); const recentlyClosedComponent = document.querySelector( "view-recentlyclosed:not([slot=recentlyclosed])" @@ -569,7 +569,7 @@ add_task(async function test_search_recent_browsing() { const { document } = browser.contentWindow; info("Input a search query."); - await navigateToCategoryAndWait(document, "recentbrowsing"); + await navigateToViewAndWait(document, "recentbrowsing"); const recentBrowsing = document.querySelector("view-recentbrowsing"); EventUtils.synthesizeMouseAtCenter( recentBrowsing.searchTextbox, diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js index 15dba68551..86e4d9cdee 100644 --- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js @@ -56,7 +56,7 @@ add_task(async function test_network_offline() { sandbox.spy(TabsSetupFlowManager, "tryToClearError"); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); Services.obs.notifyObservers( @@ -112,7 +112,7 @@ add_task(async function test_sync_error() { const sandbox = await setupWithDesktopDevices(); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); Services.obs.notifyObservers(null, "weave:service:sync:error"); diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js index 8a3c63985b..11f135cd52 100644 --- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js @@ -29,7 +29,7 @@ add_task(async function test_unconfigured_initial_state() { }); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -85,7 +85,7 @@ add_task(async function test_signed_in() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -148,7 +148,7 @@ add_task(async function test_no_synced_tabs() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -188,7 +188,7 @@ add_task(async function test_no_error_for_two_desktop() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -232,7 +232,7 @@ add_task(async function test_empty_state() { await withFirefoxView({ openNewWindow: true }, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -277,7 +277,7 @@ add_task(async function test_tabs() { await withFirefoxView({ openNewWindow: true }, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -365,7 +365,7 @@ add_task(async function test_empty_desktop_same_name() { await withFirefoxView({ openNewWindow: true }, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -413,7 +413,7 @@ add_task(async function test_empty_desktop_same_name_three() { await withFirefoxView({ openNewWindow: true }, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -459,7 +459,7 @@ add_task(async function search_synced_tabs() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "syncedtabs"); + await navigateToViewAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -666,7 +666,7 @@ add_task(async function search_synced_tabs_recent_browsing() { }); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - await navigateToCategoryAndWait(document, "recentbrowsing"); + await navigateToViewAndWait(document, "recentbrowsing"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); const recentBrowsing = document.querySelector("view-recentbrowsing"); diff --git a/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js b/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js new file mode 100644 index 0000000000..d83c1056e0 --- /dev/null +++ b/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js @@ -0,0 +1,334 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test_open_tab_row_navigation() { + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + let win = browser.ownerGlobal; + + await navigateToViewAndWait(document, "opentabs"); + const openTabs = document.querySelector("view-opentabs[name=opentabs]"); + + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList?.rowEls.length === 1, + "The tab list hasn't rendered" + ); + + // Focus tab row + let tabRow = openTabs.viewCards[0].tabList.rowEls[0]; + let tabRowFocused = BrowserTestUtils.waitForEvent(tabRow, "focus", win); + tabRow.focus(); + await tabRowFocused; + info("The tab row main element has focus."); + + // Navigate right to context menu button + let secondaryButtonFocused = BrowserTestUtils.waitForEvent( + tabRow.secondaryButtonEl, + "focus", + win + ); + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + await secondaryButtonFocused; + info("The context menu button has focus."); + + // Navigate right to close button + let tertiaryButtonFocused = BrowserTestUtils.waitForEvent( + tabRow.tertiaryButtonEl, + "focus", + win + ); + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + await tertiaryButtonFocused; + info("The close button has focus"); + + // Navigate left to context menu button + secondaryButtonFocused = BrowserTestUtils.waitForEvent( + tabRow.secondaryButtonEl, + "focus", + win + ); + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win); + await secondaryButtonFocused; + info("The context menu button has focus."); + + // Navigate left to tab row main element + tabRowFocused = BrowserTestUtils.waitForEvent(tabRow.mainEl, "focus", win); + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win); + await tabRowFocused; + info("The tab row main element has focus."); + }); + + cleanupTabs(); +}); + +add_task(async function test_focus_moves_after_unmute() { + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + let win = browser.ownerGlobal; + await navigateToViewAndWait(document, "opentabs"); + let openTabs = document.querySelector("view-opentabs[name=opentabs]"); + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length, + "The tab list has rendered." + ); + await openTabs.openTabsTarget.readyWindowsPromise; + let card = openTabs.viewCards[0]; + let openTabEl = card.tabList.rowEls[0]; + let tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + + // Mute tab + openTabEl.muteOrUnmuteTab(); + + await tabChangeRaised; + await openTabs.updateComplete; + + let mutedTab = card.tabList.rowEls[0]; + await TestUtils.waitForCondition( + () => mutedTab.indicators.includes("muted"), + "The tab has been muted." + ); + + // Unmute using keyboard + card.tabList.currentActiveElementId = mutedTab.focusMediaButton(); + isActiveElement(mutedTab.mediaButtonEl); + info("The media button has focus."); + + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + + await tabChangeRaised; + await openTabs.updateComplete; + + let unmutedTab = card.tabList.rowEls[0]; + await TestUtils.waitForCondition( + () => !unmutedTab.indicators.includes("muted"), + "The tab is no longer muted." + ); + isActiveElement(unmutedTab.secondaryButtonEl); + info( + "Focus should be on the tab's secondary button element after unmuting." + ); + + // Mute tab again and check that only Enter keys will toggle it + unmutedTab.muteOrUnmuteTab(); + await TestUtils.waitForCondition( + () => mutedTab.indicators.includes("muted"), + "The tab has been muted." + ); + mutedTab = card.tabList.rowEls[0]; + + card.tabList.currentActiveElementId = mutedTab.focusLink(); + isActiveElement(mutedTab.mainEl); + info("The 'main' element has focus."); + + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + isActiveElement(mutedTab.mediaButtonEl); + info("The media button has focus."); + + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + isActiveElement(mutedTab.secondaryButtonEl); + info("The secondary/more button has focus."); + + ok( + mutedTab.indicators.includes("muted"), + "The muted tab is still muted after arrowing past it." + ); + + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win); + isActiveElement(mutedTab.mediaButtonEl); + info("The media button has focus."); + + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await tabChangeRaised; + await openTabs.updateComplete; + + unmutedTab = card.tabList.rowEls[0]; + await TestUtils.waitForCondition( + () => !unmutedTab.indicators.includes("muted"), + "The tab is no longer muted." + ); + isActiveElement(unmutedTab.secondaryButtonEl); + info( + "Focus should be on the tab's secondary button element after unmuting." + ); + }); + + cleanupTabs(); +}); + +add_task(async function test_open_tab_row_with_sound_navigation() { + const tabWithSound = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://mochi.test:8888/browser/dom/base/test/file_audioLoop.html", + true + ); + const tabsUpdated = await BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + let win = browser.ownerGlobal; + + await navigateToViewAndWait(document, "opentabs"); + const openTabs = document.querySelector("view-opentabs[name=opentabs]"); + await TestUtils.waitForCondition( + () => tabWithSound.hasAttribute("soundplaying"), + "Tab is playing sound" + ); + await tabsUpdated; + await openTabs.updateComplete; + + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList?.rowEls.length === 2, + "The tab list has rendered." + ); + + // Focus tab row with sound playing + let tabRow; + for (const rowEl of openTabs.viewCards[0].tabList.rowEls) { + if (rowEl.indicators.includes("soundplaying")) { + tabRow = rowEl; + break; + } + } + ok(tabRow, "Found a tab row with sound playing."); + let tabRowFocused = BrowserTestUtils.waitForEvent(tabRow, "focus", win); + tabRow.focus(); + await tabRowFocused; + info("The tab row main element has focus."); + + // Navigate right to media button + let mediaButtonFocused = BrowserTestUtils.waitForEvent( + tabRow.mediaButtonEl, + "focus", + win + ); + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + await mediaButtonFocused; + info("The media button has focus."); + + // Navigate right to context menu button + let secondaryButtonFocused = BrowserTestUtils.waitForEvent( + tabRow.secondaryButtonEl, + "focus", + win + ); + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + await secondaryButtonFocused; + info("The context menu button has focus."); + + // Navigate right to close button + let tertiaryButtonFocused = BrowserTestUtils.waitForEvent( + tabRow.tertiaryButtonEl, + "focus", + win + ); + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + await tertiaryButtonFocused; + info("The close button has focus"); + + // Navigate left to context menuo button + secondaryButtonFocused = BrowserTestUtils.waitForEvent( + tabRow.secondaryButtonEl, + "focus", + win + ); + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win); + await secondaryButtonFocused; + info("The context menu button has focus."); + + // Navigate left to media button + mediaButtonFocused = BrowserTestUtils.waitForEvent( + tabRow.mediaButtonEl, + "focus", + win + ); + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win); + await mediaButtonFocused; + info("The media button has focus."); + + // Navigate left to main element of tab row + tabRowFocused = BrowserTestUtils.waitForEvent(tabRow.mainEl, "focus", win); + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win); + await tabRowFocused; + info("The tab row main element has focus."); + }); + + cleanupTabs(); +}); + +add_task(async function test_open_tab_row_with_sound_mute_and_unmute() { + const tabWithSound = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://mochi.test:8888/browser/dom/base/test/file_audioLoop.html", + true + ); + const tabsUpdated = await BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + let win = browser.ownerGlobal; + + await navigateToViewAndWait(document, "opentabs"); + const openTabs = document.querySelector("view-opentabs[name=opentabs]"); + await TestUtils.waitForCondition( + () => tabWithSound.hasAttribute("soundplaying"), + "Tab is playing sound" + ); + await tabsUpdated; + await openTabs.updateComplete; + + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList?.rowEls.length === 2, + "The tab list has rendered." + ); + + // Focus tab row with sound playing + let tabRow; + for (const rowEl of openTabs.viewCards[0].tabList.rowEls) { + if (rowEl.indicators.includes("soundplaying")) { + tabRow = rowEl; + break; + } + } + ok(tabRow, "Found a tab row with sound playing."); + let tabRowFocused = BrowserTestUtils.waitForEvent(tabRow, "focus", win); + tabRow.focus(); + await tabRowFocused; + info("The tab row main element has focus."); + + // Navigate right to media button + let mediaButtonFocused = BrowserTestUtils.waitForEvent( + tabRow.mediaButtonEl, + "focus", + win + ); + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + await mediaButtonFocused; + info("The media button has focus."); + + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await TestUtils.waitForCondition( + () => tabRow.indicators.includes("muted"), + "Tab has been muted" + ); + + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await TestUtils.waitForCondition( + () => tabRow.indicators.includes("soundplaying"), + "Tab has been unmuted" + ); + }); + + cleanupTabs(); +}); diff --git a/browser/components/firefoxview/tests/browser/head.js b/browser/components/firefoxview/tests/browser/head.js index b0b41b759d..302f19071c 100644 --- a/browser/components/firefoxview/tests/browser/head.js +++ b/browser/components/firefoxview/tests/browser/head.js @@ -3,6 +3,7 @@ const { getFirefoxViewURL, + switchToWindow, withFirefoxView, assertFirefoxViewTab, assertFirefoxViewTabSelected, @@ -31,6 +32,11 @@ const { FeatureCalloutMessages } = ChromeUtils.importESModule( const { TelemetryTestUtils } = ChromeUtils.importESModule( "resource://testing-common/TelemetryTestUtils.sys.mjs" ); +const { NonPrivateTabs } = ChromeUtils.importESModule( + "resource:///modules/OpenTabs.sys.mjs" +); +// shut down the open tabs module after each test so we don't get debounced events bleeding into the next +registerCleanupFunction(() => NonPrivateTabs.stop()); const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL; const { SessionStoreTestUtils } = ChromeUtils.importESModule( @@ -548,31 +554,19 @@ registerCleanupFunction(() => { gSandbox?.restore(); }); -function navigateToCategory(document, category) { - const navigation = document.querySelector("fxview-category-navigation"); - let navButton = Array.from(navigation.categoryButtons).filter( - categoryButton => { - return categoryButton.name === category; - } - )[0]; - navButton.buttonEl.click(); -} - -async function navigateToCategoryAndWait(document, category) { - info(`navigateToCategoryAndWait, for ${category}`); - const navigation = document.querySelector("fxview-category-navigation"); +async function navigateToViewAndWait(document, view) { + info(`navigateToViewAndWait, for ${view}`); + const navigation = document.querySelector("moz-page-nav"); const win = document.ownerGlobal; SimpleTest.promiseFocus(win); - let navButton = Array.from(navigation.categoryButtons).find( - categoryButton => { - return categoryButton.name === category; - } - ); + let navButton = Array.from(navigation.pageNavButtons).find(pageNavButton => { + return pageNavButton.view === view; + }); const namedDeck = document.querySelector("named-deck"); await BrowserTestUtils.waitForCondition( () => navButton.getBoundingClientRect().height, - `Waiting for ${category} button to be clickable` + `Waiting for ${view} button to be clickable` ); EventUtils.synthesizeMouseAtCenter(navButton, {}, win); @@ -582,10 +576,10 @@ async function navigateToCategoryAndWait(document, category) { child => child.slot == "selected" ); return ( - namedDeck.selectedViewName == category && + namedDeck.selectedViewName == view && selectedView?.getBoundingClientRect().height ); - }, `Waiting for ${category} to be visible`); + }, `Waiting for ${view} to be visible`); } /** @@ -597,6 +591,7 @@ async function navigateToCategoryAndWait(document, category) { * The tab switched to. */ async function switchToFxViewTab(win = window) { + await switchToWindow(win); return BrowserTestUtils.switchTab(win.gBrowser, win.FirefoxViewHandler.tab); } @@ -657,10 +652,32 @@ function setSortOption(component, value) { EventUtils.synthesizeMouseAtCenter(el, {}, el.ownerGlobal); } +/** + * Select the Open Tabs view-page in the Firefox View tab. + */ +async function navigateToOpenTabs(browser) { + const document = browser.contentDocument; + if (document.querySelector("named-deck").selectedViewName != "opentabs") { + await navigateToViewAndWait(browser.contentDocument, "opentabs"); + } +} + function getOpenTabsCards(openTabs) { return openTabs.shadowRoot.querySelectorAll("view-opentabs-card"); } +function getOpenTabsComponent(browser) { + return browser.contentDocument.querySelector("named-deck > view-opentabs"); +} + +async function getTabRowsForCard(card) { + await TestUtils.waitForCondition( + () => card.tabList.rowEls.length, + "Wait for the card's tab list to have rows" + ); + return card.tabList.rowEls; +} + async function click_recently_closed_tab_item(itemElem, itemProperty = "") { // Make sure the firefoxview tab still has focus is( @@ -675,7 +692,7 @@ async function click_recently_closed_tab_item(itemElem, itemProperty = "") { let clickTarget; switch (itemProperty) { case "dismiss": - clickTarget = itemElem.buttonEl; + clickTarget = itemElem.secondaryButtonEl; break; default: clickTarget = itemElem.mainEl; @@ -706,3 +723,25 @@ async function waitForRecentlyClosedTabsList(doc) { }); return [cardMainSlotNode, cardMainSlotNode.rowEls]; } + +async function add_new_tab(URL) { + let tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + let tab = BrowserTestUtils.addTab(gBrowser, URL); + // wait so we can reliably compare the tab URL + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await tabChangeRaised; + return tab; +} + +function isActiveElement(expectedLinkEl) { + return expectedLinkEl.getRootNode().activeElement == expectedLinkEl; +} + +function cleanupTabs() { + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs[0]); + } +} diff --git a/browser/components/firefoxview/tests/chrome/chrome.toml b/browser/components/firefoxview/tests/chrome/chrome.toml index b1677430b2..3edeefd4a9 100644 --- a/browser/components/firefoxview/tests/chrome/chrome.toml +++ b/browser/components/firefoxview/tests/chrome/chrome.toml @@ -2,6 +2,4 @@ ["test_card_container.html"] -["test_fxview_category_navigation.html"] - ["test_fxview_tab_list.html"] diff --git a/browser/components/firefoxview/tests/chrome/test_fxview_category_navigation.html b/browser/components/firefoxview/tests/chrome/test_fxview_category_navigation.html deleted file mode 100644 index 0ea0a94baf..0000000000 --- a/browser/components/firefoxview/tests/chrome/test_fxview_category_navigation.html +++ /dev/null @@ -1,322 +0,0 @@ -<!DOCTYPE HTML> -<html> -<head> - <meta charset="utf-8"> - <title>FxviewCategoryNavigation Tests</title> - <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> - <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> - <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> - <script type="module" src="chrome://browser/content/firefoxview/fxview-category-navigation.mjs"></script> -</head> -<style> -body { - display: flex; -} -#navigation { - width: var(--in-content-sidebar-width); -} -fxview-category-button[name="category-one"]::part(icon) { - background-image: url("chrome://mozapps/skin/extensions/category-discover.svg"); -} -fxview-category-button[name="category-two"]::part(icon) { - background-image: url("chrome://mozapps/skin/extensions/category-discover.svg"); -} -fxview-category-button[name="category-three"]::part(icon) { - background-image: url("chrome://mozapps/skin/extensions/category-discover.svg"); -} -fxview-category-button[name="category-four"]::part(icon) { - background-image: url("chrome://mozapps/skin/extensions/category-discover.svg"); -} -fxview-category-button[name="category-five"]::part(icon) { - background-image: url("chrome://mozapps/skin/extensions/category-discover.svg"); -} -</style> -<body> - <p id="display"></p> - <div id="content"> - <div id="navigation"> - <fxview-category-navigation> - <h2 slot="category-nav-header">Header</h2> - <fxview-category-button class="category" slot="category-button" name="category-one"> - <span class="category-name">Category 1</span> - </fxview-category-button> - <fxview-category-button class="category" slot="category-button" name="category-two"> - <span class="category-name">Category 2</span> - </fxview-category-button> - <fxview-category-button class="category" slot="category-button" name="category-three"> - <span class="category-name">Category 3</span> - </fxview-category-button> - <fxview-category-button class="category" slot="category-button" name="category-four"> - <span class="category-name">Category 4</span> - </fxview-category-button> - <fxview-category-button class="category" slot="category-button" name="category-five"> - <span class="category-name">Category 5</span> - </fxview-category-button> - </fxview-category-navigation> - </div> - </div> -<pre id="test"></pre> -<script> - Services.scriptloader.loadSubScript( - "chrome://browser/content/utilityOverlay.js", - this - ); - const { BrowserTestUtils } = ChromeUtils.importESModule( - "resource://testing-common/BrowserTestUtils.sys.mjs" - ); - -const fxviewCategoryNav = document.querySelector("fxview-category-navigation"); - -function isActiveElement(expectedActiveEl) { - return expectedActiveEl.getRootNode().activeElement == expectedActiveEl; - } - - /** - * Tests that the first category is selected by default - */ - add_task(async function test_first_item_selected_by_default() { - is( - fxviewCategoryNav.categoryButtons.length, - 5, - "Five category buttons are in the navigation" - ); - - ok( - fxviewCategoryNav.categoryButtons[0].name === fxviewCategoryNav.currentCategory, - "The first category button is selected by default" - ) - }); - - /** - * Tests that categories are selected when clicked - */ - add_task(async function test_select_category() { - let gBrowser = BrowserWindowTracker.getTopWindow().top.gBrowser; - let secondCategory = fxviewCategoryNav.categoryButtons[1]; - let categoryChanged = BrowserTestUtils.waitForEvent( - gBrowser, - "change-category" - ); - - secondCategory.buttonEl.click(); - await categoryChanged; - - ok( - secondCategory.name === fxviewCategoryNav.currentCategory, - "The second category button is selected" - ) - - let thirdCategory = fxviewCategoryNav.categoryButtons[2]; - categoryChanged = BrowserTestUtils.waitForEvent( - gBrowser, - "change-category" - ); - - thirdCategory.buttonEl.click(); - await categoryChanged; - - ok( - thirdCategory.name === fxviewCategoryNav.currentCategory, - "The third category button is selected" - ) - - let firstCategory = fxviewCategoryNav.categoryButtons[0]; - categoryChanged = BrowserTestUtils.waitForEvent( - gBrowser, - "change-category" - ); - - firstCategory.buttonEl.click(); - await categoryChanged; - - ok( - firstCategory.name === fxviewCategoryNav.currentCategory, - "The first category button is selected" - ) - }); - - /** - * Tests that categories are keyboard-navigable - */ - add_task(async function test_keyboard_navigation() { - const arrowDown = async () => { - info("Arrow down"); - synthesizeKey("KEY_ArrowDown", {}); - await fxviewCategoryNav.getUpdateComplete(); - }; - const arrowUp = async () => { - info("Arrow up"); - synthesizeKey("KEY_ArrowUp", {}); - await fxviewCategoryNav.getUpdateComplete(); - }; - const arrowLeft = async () => { - info("Arrow left"); - synthesizeKey("KEY_ArrowLeft", {}); - await fxviewCategoryNav.getUpdateComplete(); - }; - const arrowRight = async () => { - info("Arrow right"); - synthesizeKey("KEY_ArrowRight", {}); - await fxviewCategoryNav.getUpdateComplete(); - }; - - // Setting this pref allows the test to run as expected with a keyboard on MacOS - await SpecialPowers.pushPrefEnv({ - set: [["accessibility.tabfocus", 7]], - }); - - let firstCategory = fxviewCategoryNav.categoryButtons[0]; - let secondCategory = fxviewCategoryNav.categoryButtons[1]; - let thirdCategory = fxviewCategoryNav.categoryButtons[2]; - let fourthCategory = fxviewCategoryNav.categoryButtons[3]; - let fifthCategory = fxviewCategoryNav.categoryButtons[4]; - - is( - firstCategory.name, - fxviewCategoryNav.currentCategory, - "The first category button is selected" - ) - firstCategory.focus(); - await arrowDown(); - ok( - isActiveElement(secondCategory), - "The second category button is the active element after first arrow down" - ); - is( - secondCategory.name, - fxviewCategoryNav.currentCategory, - "The second category button is selected" - ) - await arrowDown(); - is( - thirdCategory.name, - fxviewCategoryNav.currentCategory, - "The third category button is selected" - ) - await arrowDown(); - is( - fourthCategory.name, - fxviewCategoryNav.currentCategory, - "The fourth category button is selected" - ) - await arrowDown(); - is( - fifthCategory.name, - fxviewCategoryNav.currentCategory, - "The fifth category button is selected" - ) - await arrowDown(); - is( - fifthCategory.name, - fxviewCategoryNav.currentCategory, - "The fifth category button is still selected" - ) - await arrowUp(); - is( - fourthCategory.name, - fxviewCategoryNav.currentCategory, - "The fourth category button is selected" - ) - await arrowUp(); - is( - thirdCategory.name, - fxviewCategoryNav.currentCategory, - "The third category button is selected" - ) - await arrowUp(); - is( - secondCategory.name, - fxviewCategoryNav.currentCategory, - "The second category button is selected" - ) - await arrowUp(); - is( - firstCategory.name, - fxviewCategoryNav.currentCategory, - "The first category button is selected" - ) - await arrowUp(); - is( - firstCategory.name, - fxviewCategoryNav.currentCategory, - "The first category button is still selected" - ) - - // Test navigation with arrow left/right keys - is( - firstCategory.name, - fxviewCategoryNav.currentCategory, - "The first category button is selected" - ) - firstCategory.focus(); - await arrowRight(); - ok( - isActiveElement(secondCategory), - "The second category button is the active element after first arrow right" - ); - is( - secondCategory.name, - fxviewCategoryNav.currentCategory, - "The second category button is selected" - ) - await arrowRight(); - is( - thirdCategory.name, - fxviewCategoryNav.currentCategory, - "The third category button is selected" - ) - await arrowRight(); - is( - fourthCategory.name, - fxviewCategoryNav.currentCategory, - "The fourth category button is selected" - ) - await arrowRight(); - is( - fifthCategory.name, - fxviewCategoryNav.currentCategory, - "The fifth category button is selected" - ) - await arrowRight(); - is( - fifthCategory.name, - fxviewCategoryNav.currentCategory, - "The fifth category button is still selected" - ) - await arrowLeft(); - is( - fourthCategory.name, - fxviewCategoryNav.currentCategory, - "The fourth category button is selected" - ) - await arrowLeft(); - is( - thirdCategory.name, - fxviewCategoryNav.currentCategory, - "The third category button is selected" - ) - await arrowLeft(); - is( - secondCategory.name, - fxviewCategoryNav.currentCategory, - "The second category button is selected" - ) - await arrowLeft(); - is( - firstCategory.name, - fxviewCategoryNav.currentCategory, - "The first category button is selected" - ) - await arrowLeft(); - is( - firstCategory.name, - fxviewCategoryNav.currentCategory, - "The first category button is still selected" - ) - - await SpecialPowers.popPrefEnv(); - }); -</script> -</body> -</html> 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 22f04acab2..e48f776592 100644 --- a/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html +++ b/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html @@ -308,12 +308,12 @@ ); await arrowRight(); ok( - isActiveElement(tabItems[0].buttonEl), + isActiveElement(tabItems[0].secondaryButtonEl), "Focus should be on the first row's context menu button element of the list" ); await arrowDown(); ok( - isActiveElement(tabItems[1].buttonEl), + isActiveElement(tabItems[1].secondaryButtonEl), "Focus should be on the second row's context menu button element of the list" ); await arrowLeft(); diff --git a/browser/components/firefoxview/viewpage.mjs b/browser/components/firefoxview/viewpage.mjs index fee02b49d6..83b04faf5d 100644 --- a/browser/components/firefoxview/viewpage.mjs +++ b/browser/components/firefoxview/viewpage.mjs @@ -131,7 +131,7 @@ export class ViewPage extends ViewPageContent { super(); this.selectedTab = false; this.recentBrowsing = Boolean(this.recentBrowsingElement); - this.onVisibilityChange = this.onVisibilityChange.bind(this); + this.onTabSelect = this.onTabSelect.bind(this); this.onResize = this.onResize.bind(this); } @@ -148,14 +148,17 @@ export class ViewPage extends ViewPageContent { this.windowResizeTask?.arm(); } - onVisibilityChange(event) { - if (this.isVisible) { + onTabSelect({ target }) { + const win = target.ownerGlobal; + + let selfBrowser = window.docShell?.chromeEventHandler; + const { gBrowser } = this.getWindow(); + let isForegroundTab = gBrowser.selectedBrowser == selfBrowser; + + if (win.FirefoxViewHandler.tab?.selected && isForegroundTab) { this.paused = false; this.viewVisibleCallback(); - } else if ( - this.ownerViewPage.selectedTab && - this.ownerDocument.visibilityState == "hidden" - ) { + } else { this.paused = true; this.viewHiddenCallback(); } @@ -163,19 +166,12 @@ export class ViewPage extends ViewPageContent { connectedCallback() { super.connectedCallback(); - this.ownerDocument.addEventListener( - "visibilitychange", - this.onVisibilityChange - ); } disconnectedCallback() { super.disconnectedCallback(); - this.ownerDocument.removeEventListener( - "visibilitychange", - this.onVisibilityChange - ); this.getWindow().removeEventListener("resize", this.onResize); + this.getWindow().removeEventListener("TabSelect", this.onTabSelect); } updateAllVirtualLists() { @@ -246,6 +242,7 @@ export class ViewPage extends ViewPageContent { this.paused = false; this.viewVisibleCallback(); this.getWindow().addEventListener("resize", this.onResize); + this.getWindow().addEventListener("TabSelect", this.onTabSelect); } } @@ -257,5 +254,6 @@ export class ViewPage extends ViewPageContent { this.windowResizeTask?.finalize(); } this.getWindow().removeEventListener("resize", this.onResize); + this.getWindow().removeEventListener("TabSelect", this.onTabSelect); } } |