diff --git a/browser/components/firefoxview/firefoxview-next.html b/browser/components/firefoxview/firefoxview-next.html index e20a90de86ed..c8ddf0458ee3 100644 --- a/browser/components/firefoxview/firefoxview-next.html +++ b/browser/components/firefoxview/firefoxview-next.html @@ -92,7 +92,7 @@
- +
@@ -103,10 +103,13 @@
- - - - + + + +
diff --git a/browser/components/firefoxview/firefoxview-next.mjs b/browser/components/firefoxview/firefoxview-next.mjs index f2bc9a108641..7cdb325e5c13 100644 --- a/browser/components/firefoxview/firefoxview-next.mjs +++ b/browser/components/firefoxview/firefoxview-next.mjs @@ -3,25 +3,37 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ let pageList = []; +let categoryPagesDeck = null; +let categoryNavigation = null; + const { topChromeWindow } = window.browsingContext; function onHashChange() { - changePage(document.location.hash.substring(1)); + let page = document.location?.hash.substring(1); + if (!page || !pageList.includes(page)) { + page = "recentbrowsing"; + } + changePage(page); } function changePage(page) { - let navigation = document.querySelector("fxview-category-navigation"); - if (page && !pageList.includes(page)) { - page = "recentbrowsing"; - document.location.hash = "recentbrowsing"; - } - document.querySelector("named-deck").selectedViewName = page || pageList[0]; - navigation.currentCategory = page || pageList[0]; - if (navigation.categoryButtons.includes(document.activeElement)) { - let currentCategoryButton = navigation.categoryButtons.find( + categoryPagesDeck.selectedViewName = page; + categoryNavigation.currentCategory = page; + if (categoryNavigation.categoryButtons.includes(document.activeElement)) { + let currentCategoryButton = categoryNavigation.categoryButtons.find( categoryButton => categoryButton.name === page ); - (currentCategoryButton || navigation.categoryButtons[0]).focus(); + (currentCategoryButton || categoryNavigation.categoryButtons[0]).focus(); + } +} + +function onPagesDeckViewChange() { + for (const child of categoryPagesDeck.children) { + if (child.getAttribute("name") == categoryPagesDeck.selectedViewName) { + child.enter(); + } else { + child.exit(); + } } } @@ -47,8 +59,11 @@ function recordNavigationTelemetry(source, eventTarget) { window.addEventListener("DOMContentLoaded", async () => { recordEnteredTelemetry(); - let navigation = document.querySelector("fxview-category-navigation"); - for (const item of navigation.categoryButtons) { + + categoryNavigation = document.querySelector("fxview-category-navigation"); + categoryPagesDeck = document.querySelector("named-deck"); + + for (const item of categoryNavigation.categoryButtons) { pageList.push(item.getAttribute("name")); } window.addEventListener("hashchange", onHashChange); @@ -60,31 +75,26 @@ window.addEventListener("DOMContentLoaded", async () => { window.addEventListener("card-container-view-all", function (event) { recordNavigationTelemetry("view-all", event.originalTarget); }); - if (document.location.hash) { - changePage(document.location.hash.substring(1)); - } + + categoryPagesDeck.addEventListener("view-changed", onPagesDeckViewChange); + + // set the initial state + onHashChange(); + onPagesDeckViewChange(); + if (Cu.isInAutomation) { Services.obs.notifyObservers(null, "firefoxview-entered"); } }); -document - .querySelector("named-deck") - .addEventListener("view-changed", async event => { - for (const child of event.target.children) { - if (child.getAttribute("name") == event.target.selectedViewName) { - child.enter(); - } else { - child.exit(); - } - } - }); - document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { recordEnteredTelemetry(); if (Cu.isInAutomation) { - Services.obs.notifyObservers(null, "firefoxview-entered"); + // allow all the component visibilitychange handlers to execute before notifying + requestAnimationFrame(() => { + Services.obs.notifyObservers(null, "firefoxview-entered"); + }); } } }); @@ -127,7 +137,7 @@ function onCommand(e) { null, { menu_action: item.id, - page: location.hash.substring(1) || "recentbrowsing", + page: location.hash?.substring(1) || "recentbrowsing", } ); } diff --git a/browser/components/firefoxview/history.mjs b/browser/components/firefoxview/history.mjs index 5589b7c2f958..e8d9fcea9ebe 100644 --- a/browser/components/firefoxview/history.mjs +++ b/browser/components/firefoxview/history.mjs @@ -45,6 +45,7 @@ const SEARCH_DEBOUNCE_TIMEOUT_MS = 1000; class HistoryInView extends ViewPage { constructor() { super(); + this._started = false; this.allHistoryItems = new Map(); this.historyMapByDate = []; this.historyMapBySite = []; @@ -58,10 +59,25 @@ class HistoryInView extends ViewPage { this.fullyUpdated = false; } + start() { + if (this._started) { + return; + } + this._started = true; + + this.#updateAllHistoryItems(); + this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data)); + + this.searchTask = new lazy.DeferredTask( + () => this.#updateSearchResults(), + SEARCH_DEBOUNCE_RATE_MS, + SEARCH_DEBOUNCE_TIMEOUT_MS + ); + } + async connectedCallback() { super.connectedCallback(); await this.updateHistoryData(); - this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data)); XPCOMUtils.defineLazyPreferenceGetter( this, "importHistoryDismissedPref", @@ -88,23 +104,26 @@ class HistoryInView extends ViewPage { // Convert milliseconds to days this.profileAge = profileAge / 1000 / 60 / 60 / 24; } - this.searchTask = new lazy.DeferredTask( - () => this.#updateSearchResults(), - SEARCH_DEBOUNCE_RATE_MS, - SEARCH_DEBOUNCE_TIMEOUT_MS - ); + } + + stop() { + if (!this._started) { + return; + } + this._started = false; + this.placesQuery.close(); + if (!this.searchTask.isFinalized) { + this.searchTask.finalize(); + } } disconnectedCallback() { super.disconnectedCallback(); - this.placesQuery.close(); + this.stop(); this.migrationWizardDialog?.removeEventListener( "MigrationWizard:Close", this.migrationWizardDialog ); - if (!this.searchTask.isFinalized) { - this.searchTask.finalize(); - } } async #updateAllHistoryItems(allHistoryItems) { @@ -133,13 +152,12 @@ class HistoryInView extends ViewPage { } } - viewTabVisibleCallback() { - this.#updateAllHistoryItems(); - this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data)); + viewVisibleCallback() { + this.start(); } - viewTabHiddenCallback() { - this.placesQuery.close(); + viewHiddenCallback() { + this.stop(); } static queries = { diff --git a/browser/components/firefoxview/opentabs.mjs b/browser/components/firefoxview/opentabs.mjs index c0047d3b0a40..45eff670d2c8 100644 --- a/browser/components/firefoxview/opentabs.mjs +++ b/browser/components/firefoxview/opentabs.mjs @@ -8,7 +8,7 @@ import { map, when, } from "chrome://global/content/vendor/lit.all.mjs"; -import { ViewPage } from "./viewpage.mjs"; +import { ViewPage, ViewPageContent } from "./viewpage.mjs"; const lazy = {}; @@ -39,10 +39,15 @@ class OpenTabsInView extends ViewPage { static properties = { windows: { type: Map }, }; + static queries = { + viewCards: { all: "view-opentabs-card" }, + }; + static TAB_ATTRS_TO_WATCH = Object.freeze(["image", "label"]); constructor() { super(); + this._started = false; this.everyWindowCallbackId = `firefoxview-${Services.uuid.generateUUID()}`; this.windows = new Map(); this.currentWindow = this.getWindow(); @@ -53,13 +58,22 @@ class OpenTabsInView extends ViewPage { this.devices = []; } - connectedCallback() { - super.connectedCallback(); + start() { + if (this._started) { + return; + } + this._started = true; + + Services.obs.addObserver(this.boundObserve, lazy.UIState.ON_UPDATE); + Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); + Services.obs.addObserver(this.boundObserve, TOPIC_CURRENT_BROWSER_CHANGED); + lazy.EveryWindow.registerCallback( this.everyWindowCallbackId, win => { if (win.gBrowser && this._shouldShowOpenTabs(win) && !win.closed) { const { tabContainer } = win.gBrowser; + tabContainer.addEventListener("TabSelect", this); tabContainer.addEventListener("TabAttrModified", this); tabContainer.addEventListener("TabClose", this); tabContainer.addEventListener("TabMove", this); @@ -75,6 +89,7 @@ class OpenTabsInView extends ViewPage { win => { if (win.gBrowser && this._shouldShowOpenTabs(win)) { const { tabContainer } = win.gBrowser; + tabContainer.removeEventListener("TabSelect", this); tabContainer.removeEventListener("TabAttrModified", this); tabContainer.removeEventListener("TabClose", this); tabContainer.removeEventListener("TabMove", this); @@ -86,41 +101,52 @@ class OpenTabsInView extends ViewPage { } } ); - this._updateOpenTabsList(); - this.addObserversIfNeeded(); + // EveryWindow will invoke the callback for existing windows - including this one + // So this._updateOpenTabsList will get called for the already-open window if (this.currentWindow.gSync) { this.devices = this.currentWindow.gSync.getSendTabTargets(); } + + for (let card of this.viewCards) { + card.paused = false; + card.viewVisibleCallback?.(); + } } disconnectedCallback() { + super.disconnectedCallback(); + this.stop(); + } + + stop() { + if (!this._started) { + return; + } + this._started = false; + this.paused = true; + lazy.EveryWindow.unregisterCallback(this.everyWindowCallbackId); - this.removeObserversIfNeeded(); - } - addObserversIfNeeded() { - if (!this.observerAdded) { - Services.obs.addObserver(this.boundObserve, lazy.UIState.ON_UPDATE); - Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); - Services.obs.addObserver( - this.boundObserve, - TOPIC_CURRENT_BROWSER_CHANGED - ); - this.observerAdded = true; + Services.obs.removeObserver(this.boundObserve, lazy.UIState.ON_UPDATE); + Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); + Services.obs.removeObserver( + this.boundObserve, + TOPIC_CURRENT_BROWSER_CHANGED + ); + + for (let card of this.viewCards) { + card.paused = true; + card.viewHiddenCallback?.(); } } - removeObserversIfNeeded() { - if (this.observerAdded) { - Services.obs.removeObserver(this.boundObserve, lazy.UIState.ON_UPDATE); - Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); - Services.obs.removeObserver( - this.boundObserve, - TOPIC_CURRENT_BROWSER_CHANGED - ); - this.observerAdded = false; - } + viewVisibleCallback() { + this.start(); + } + + viewHiddenCallback() { + this.stop(); } async observe(subject, topic, data) { @@ -144,9 +170,6 @@ class OpenTabsInView extends ViewPage { } render() { - if (!this.selectedTab && !this.recentBrowsing) { - return null; - } if (this.recentBrowsing) { return this.getRecentBrowsingTemplate(); } @@ -200,6 +223,7 @@ class OpenTabsInView extends ViewPage { `; } @@ -259,6 +285,13 @@ class OpenTabsInView extends ViewPage { const win = target.ownerGlobal; const tabs = this.windows.get(win); switch (type) { + case "TabSelect": { + // if we're switching away from our tab, we can halt any updates immediately + if (detail.previousTab == this.getBrowserTab()) { + this.stop(); + } + return; + } case "TabAttrModified": if ( !detail.changed.some(attr => @@ -328,7 +361,7 @@ customElements.define("view-opentabs", OpenTabsInView); * @property {string} title * The window title. */ -class OpenTabsInViewCard extends ViewPage { +class OpenTabsInViewCard extends ViewPageContent { static properties = { showMore: { type: Boolean }, tabs: { type: Array }, @@ -512,6 +545,18 @@ class OpenTabsInViewCard extends ViewPage { ); } + viewVisibleCallback() { + if (this.tabList) { + this.tabList.visible = true; + } + } + + viewHiddenCallback() { + if (this.tabList) { + this.tabList.visible = false; + } + } + render() { return html` this.observe(...args); this.fullyUpdated = false; this.maxTabsLength = this.recentBrowsing ? 5 : 25; @@ -62,55 +63,54 @@ class RecentlyClosedTabsInView extends ViewPage { } } - connectedCallback() { - super.connectedCallback(); + start() { + if (this._started) { + return; + } + this._started = true; + this.paused = false; this.updateRecentlyClosedTabs(); - this.addObserversIfNeeded(); + + Services.obs.addObserver( + this.boundObserve, + SS_NOTIFY_CLOSED_OBJECTS_CHANGED + ); + Services.obs.addObserver( + this.boundObserve, + SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH + ); + } + + stop() { + if (!this._started) { + return; + } + this._started = false; + + Services.obs.removeObserver( + this.boundObserve, + SS_NOTIFY_CLOSED_OBJECTS_CHANGED + ); + Services.obs.removeObserver( + this.boundObserve, + SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH + ); } disconnectedCallback() { super.disconnectedCallback(); - this.removeObserversIfNeeded(); + this.stop(); } - addObserversIfNeeded() { - if (!this.observerAdded) { - Services.obs.addObserver( - this.boundObserve, - SS_NOTIFY_CLOSED_OBJECTS_CHANGED - ); - Services.obs.addObserver( - this.boundObserve, - SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH - ); - this.observerAdded = true; - } + // We remove all the observers when the instance is not visible to the user + viewHiddenCallback() { + this.stop(); } - removeObserversIfNeeded() { - if (this.observerAdded) { - Services.obs.removeObserver( - this.boundObserve, - SS_NOTIFY_CLOSED_OBJECTS_CHANGED - ); - Services.obs.removeObserver( - this.boundObserve, - SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH - ); - this.observerAdded = false; - } - } - - // we observe when a tab closes but since this notification fires more frequently and on - // all windows, we remove the observer when another tab is selected - viewTabHiddenCallback() { - this.removeObserversIfNeeded(); - } - - // we check for changes to the session store once the user return to this tab. - viewTabVisibleCallback() { - this.addObserversIfNeeded(); - this.updateRecentlyClosedTabs(); + // We add observers and check for changes to the session store once the user return to this tab. + // or the instance becomes visible to the user + viewVisibleCallback() { + this.start(); } getTabStateValue(tab, key) { @@ -282,9 +282,6 @@ class RecentlyClosedTabsInView extends ViewPage { } render() { - if (!this.selectedTab && !this.recentBrowsing) { - return null; - } return html` this.observe(...args); this._currentSetupStateIndex = -1; this.errorState = null; @@ -56,7 +57,13 @@ class SyncedTabsInView extends ViewPage { connectedCallback() { super.connectedCallback(); this.addEventListener("click", this); - this.ownerDocument.addEventListener("visibilitychange", this); + } + + start() { + if (this._started) { + return; + } + this._started = true; Services.obs.addObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED); Services.obs.addObserver(this.boundObserve, SYNCED_TABS_CHANGED); @@ -64,15 +71,21 @@ class SyncedTabsInView extends ViewPage { this.onVisibilityChange(); } - cleanup() { + stop() { + if (!this._started) { + return; + } + this._started = false; TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded"); - this.ownerDocument?.removeEventListener("visibilitychange", this); + this.onVisibilityChange(); + Services.obs.removeObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED); Services.obs.removeObserver(this.boundObserve, SYNCED_TABS_CHANGED); } disconnectedCallback() { - this.cleanup(); + super.disconnectedCallback(); + this.stop(); } handleEvent(event) { @@ -114,15 +127,19 @@ class SyncedTabsInView extends ViewPage { if (event.type == "change") { TabsSetupFlowManager.syncOpenTabs(event.target); } - - // Returning to fxview seems like a likely time for a device check - if (event.type == "visibilitychange") { - this.onVisibilityChange(); - } } + + viewVisibleCallback() { + this.start(); + } + + viewHiddenCallback() { + this.stop(); + } + onVisibilityChange() { - const isVisible = document.visibilityState == "visible"; const isOpen = this.open; + const isVisible = this.isVisible; if (isVisible && isOpen) { this.update(); TabsSetupFlowManager.updateViewVisibility(this._id, "visible"); diff --git a/browser/components/firefoxview/tests/browser/FirefoxViewTestUtils.sys.mjs b/browser/components/firefoxview/tests/browser/FirefoxViewTestUtils.sys.mjs index 029c5133683d..0e21d97fddba 100644 --- a/browser/components/firefoxview/tests/browser/FirefoxViewTestUtils.sys.mjs +++ b/browser/components/firefoxview/tests/browser/FirefoxViewTestUtils.sys.mjs @@ -51,35 +51,45 @@ async function openFirefoxViewTab(win) { ); } await testScope.SimpleTest.promiseFocus(win); + let fxviewTab = win.FirefoxViewHandler.tab; + let alreadyLoaded = + fxviewTab?.linkedBrowser.currentURI.spec.includes(getFirefoxViewURL()) && + fxviewTab?.linkedBrowser?.contentDocument?.readyState == "complete"; + let enteredPromise = alreadyLoaded + ? Promise.resolve() + : TestUtils.topicObserved("firefoxview-entered"); - const fxViewTab = win.FirefoxViewHandler.tab; - const alreadyLoaded = - fxViewTab?.linkedBrowser?.currentURI.spec.split("#")[0] == - getFirefoxViewURL(); - const enteredPromise = - alreadyLoaded && - fxViewTab.linkedBrowser.contentDocument.visibilityState == "visible" - ? Promise.resolve() - : TestUtils.topicObserved("firefoxview-entered"); - await BrowserTestUtils.synthesizeMouseAtCenter( - "#firefox-view-button", - { type: "mousedown" }, - win.browsingContext - ); + if (!fxviewTab?.selected) { + await BrowserTestUtils.synthesizeMouseAtCenter( + "#firefox-view-button", + { type: "mousedown" }, + win.browsingContext + ); + await TestUtils.waitForTick(); + } + + fxviewTab = win.FirefoxViewHandler.tab; assertFirefoxViewTab(win); Assert.ok( win.FirefoxViewHandler.tab.selected, "Firefox View tab is selected" ); - if (!alreadyLoaded) { - testScope.info("Not already loaded, waiting for browserLoaded"); - await BrowserTestUtils.browserLoaded( - win.FirefoxViewHandler.tab.linkedBrowser - ); - } - await enteredPromise; - return win.FirefoxViewHandler.tab; + testScope.info( + "openFirefoxViewTab, waiting for complete readyState, visible and firefoxview-entered" + ); + await Promise.all([ + TestUtils.waitForCondition(() => { + const document = fxviewTab.linkedBrowser.contentDocument; + return ( + document.readyState == "complete" && + document.visibilityState == "visible" + ); + }), + enteredPromise, + ]); + testScope.info("openFirefoxViewTab, ready resolved"); + return fxviewTab; } function closeFirefoxViewTab(win) { diff --git a/browser/components/firefoxview/tests/browser/firefoxview-next/browser.toml b/browser/components/firefoxview/tests/browser/firefoxview-next/browser.toml index 387ddb3ab071..c0b283e65f50 100644 --- a/browser/components/firefoxview/tests/browser/firefoxview-next/browser.toml +++ b/browser/components/firefoxview/tests/browser/firefoxview-next/browser.toml @@ -3,6 +3,8 @@ support-files = [ "../head.js"] ["browser_firefoxview_next.js"] +["browser_firefoxview_paused.js"] + ["browser_firefoxview_next_general_telemetry.js"] fail-if = ["a11y_checks"] # Bug 1854625 clicked button and fxview-tab-row-secondary-button may not be focusable diff --git a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_firefoxview_paused.js b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_firefoxview_paused.js new file mode 100644 index 000000000000..8a55ade7e7f4 --- /dev/null +++ b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_firefoxview_paused.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../head.js */ + +const tabURL1 = "data:,Tab1"; +const tabURL2 = "data:,Tab2"; +const tabURL3 = "data:,Tab3"; + +const TestTabs = {}; + +function getTopLevelViewElements(document) { + return { + recentBrowsingView: document.querySelector( + "named-deck > view-recentbrowsing" + ), + recentlyClosedView: document.querySelector( + "named-deck > view-recentlyclosed" + ), + openTabsView: document.querySelector("named-deck > view-opentabs"), + }; +} + +async function getElements(document) { + let { recentBrowsingView, recentlyClosedView, openTabsView } = + getTopLevelViewElements(document); + let recentBrowsingOpenTabsView = + recentBrowsingView.querySelector("view-opentabs"); + let recentBrowsingOpenTabsList = + recentBrowsingOpenTabsView?.viewCards[0]?.tabList; + let recentBrowsingRecentlyClosedTabsView = recentBrowsingView.querySelector( + "view-recentlyclosed" + ); + let recentBrowsingRecentlyClosedTabsList = + recentBrowsingRecentlyClosedTabsView?.tabList; + + let recentlyClosedList = recentlyClosedView.tabList; + let openTabsList = + openTabsView.shadowRoot.querySelector("view-opentabs-card")?.tabList; + + return { + // recentbrowsing + recentBrowsingView, + recentBrowsingOpenTabsView, + recentBrowsingOpenTabsList, + recentBrowsingRecentlyClosedTabsView, + recentBrowsingRecentlyClosedTabsList, + + // recentlyclosed + recentlyClosedView, + recentlyClosedList, + + // opentabs + openTabsView, + openTabsList, + }; +} + +async function nextFrame(global = window) { + await new Promise(resolve => { + global.requestAnimationFrame(() => { + global.requestAnimationFrame(resolve); + }); + }); +} + +async function setupOpenAndClosedTabs() { + TestTabs.tab1 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + tabURL1 + ); + TestTabs.tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + tabURL2 + ); + TestTabs.tab3 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + tabURL3 + ); + // close a tab so we have recently-closed tabs content + await SessionStoreTestUtils.closeTab(TestTabs.tab3); +} + +async function checkFxRenderCalls(browser, elements, selectedView) { + const sandbox = sinon.createSandbox(); + const topLevelViews = getTopLevelViewElements(browser.contentDocument); + + // sanity-check the selectedView we were given + ok( + Object.values(topLevelViews).find(view => view == selectedView), + `The selected view is in the topLevelViews` + ); + + const elementSpies = new Map(); + const viewSpies = new Map(); + + for (let [elemName, elem] of Object.entries(topLevelViews)) { + let spy; + if (elem.render.isSinonProxy) { + spy = elem.render; + } else { + info(`Creating spy for render on element: ${elemName}`); + spy = sandbox.spy(elem, "render"); + } + viewSpies.set(elem, spy); + } + for (let [elemName, elem] of Object.entries(elements)) { + let spy; + if (elem.render.isSinonProxy) { + spy = elem.render; + } else { + info(`Creating spy for render on element: ${elemName}`); + spy = sandbox.spy(elem, "render"); + } + elementSpies.set(elem, spy); + } + + info("test switches to tab2"); + await BrowserTestUtils.switchTab(gBrowser, TestTabs.tab2); + + // check all the top-level views are paused + ok( + topLevelViews.recentBrowsingView.paused, + "The recent-browsing view is paused" + ); + ok( + topLevelViews.recentlyClosedView.paused, + "The recently-closed tabs view is paused" + ); + ok(topLevelViews.openTabsView.paused, "The open tabs view is paused"); + + function assertSpiesCalled(spiesMap, expectCalled) { + let message = expectCalled ? "to be called" : "to not be called"; + for (let [elem, renderSpy] of spiesMap.entries()) { + is( + expectCalled, + renderSpy.called, + `Expected the render method spy on element ${elem.localName} ${message}` + ); + } + } + + await nextFrame(); + info("test removes tab1"); + await BrowserTestUtils.removeTab(TestTabs.tab1); + await nextFrame(); + + assertSpiesCalled(viewSpies, false); + assertSpiesCalled(elementSpies, false); + + for (let renderSpy of [...viewSpies.values(), ...elementSpies.values()]) { + renderSpy.resetHistory(); + } + + info("test will re-open fxview"); + await openFirefoxViewTab(window); + await nextFrame(); + + assertSpiesCalled(elementSpies, true); + ok( + selectedView.render.called, + `Render was called on the selected top-level view: ${selectedView.localName}` + ); + + // check all the other views did not render + viewSpies.delete(selectedView); + assertSpiesCalled(viewSpies, false); + + sandbox.restore(); +} + +add_task(async function test_recentbrowsing() { + await setupOpenAndClosedTabs(); + + await withFirefoxView({}, async browser => { + const document = browser.contentDocument; + is(document.querySelector("named-deck").selectedViewName, "recentbrowsing"); + + const { + recentBrowsingView, + recentBrowsingOpenTabsView, + recentBrowsingOpenTabsList, + recentBrowsingRecentlyClosedTabsView, + recentBrowsingRecentlyClosedTabsList, + } = await getElements(document); + + ok(recentBrowsingView, "Found the recent-browsing view"); + ok(recentBrowsingOpenTabsView, "Found the recent-browsing open tabs view"); + ok(recentBrowsingOpenTabsList, "Found the recent-browsing open tabs list"); + ok( + recentBrowsingRecentlyClosedTabsView, + "Found the recent-browsing recently-closed tabs view" + ); + ok( + recentBrowsingRecentlyClosedTabsList, + "Found the recent-browsing recently-closed tabs list" + ); + + await checkFxRenderCalls( + browser, + { + recentBrowsingView, + recentBrowsingOpenTabsView, + recentBrowsingOpenTabsList, + recentBrowsingRecentlyClosedTabsView, + recentBrowsingRecentlyClosedTabsList, + }, + recentBrowsingView + ); + }); + await BrowserTestUtils.removeTab(TestTabs.tab2); +}); + +add_task(async function test_opentabs() { + await setupOpenAndClosedTabs(); + + await withFirefoxView({}, async browser => { + const document = browser.contentDocument; + const { openTabsView } = getTopLevelViewElements(document); + + await navigateToCategoryAndWait(document, "opentabs"); + + const { openTabsList } = await getElements(document); + ok(openTabsView, "Found the open tabs view"); + ok(openTabsList, "Found the first open tabs list"); + ok(!openTabsView.paused, "The open tabs view is un-paused"); + is(openTabsView.slot, "selected", "The open tabs view is selected"); + + await checkFxRenderCalls( + browser, + { + openTabsView, + openTabsList, + }, + openTabsView + ); + }); + await BrowserTestUtils.removeTab(TestTabs.tab2); +}); + +add_task(async function test_recentlyclosed() { + await setupOpenAndClosedTabs(); + + await withFirefoxView({}, async browser => { + const document = browser.contentDocument; + const { recentlyClosedView } = getTopLevelViewElements(document); + await navigateToCategoryAndWait(document, "recentlyclosed"); + + const { recentlyClosedList } = await getElements(document); + ok(recentlyClosedView, "Found the recently-closed view"); + ok(recentlyClosedList, "Found the recently-closed list"); + ok(!recentlyClosedView.paused, "The recently-closed view is un-paused"); + + await checkFxRenderCalls( + browser, + { + recentlyClosedView, + recentlyClosedList, + }, + recentlyClosedView + ); + }); + await BrowserTestUtils.removeTab(TestTabs.tab2); +}); diff --git a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_history_firefoxview_next.js b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_history_firefoxview_next.js index 255e08967299..74d78fd8b899 100644 --- a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_history_firefoxview_next.js +++ b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_history_firefoxview_next.js @@ -58,14 +58,6 @@ async function historyComponentReady(historyComponent) { is(expected, actual, `Total number of cards should be ${expected}`); } -async function openFirefoxView(win) { - await BrowserTestUtils.synthesizeMouseAtCenter( - "#firefox-view-button", - { type: "mousedown" }, - win.browsingContext - ); -} - async function historyTelemetry() { await TestUtils.waitForCondition( () => { @@ -177,7 +169,7 @@ add_task(async function test_list_ordering() { const { document } = browser.contentWindow; is(document.location.href, "about:firefoxview-next"); - navigateToCategory(document, "history"); + await navigateToCategoryAndWait(document, "history"); let historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; @@ -201,9 +193,14 @@ add_task(async function test_list_ordering() { return historyComponent.lists[0].rowEls.length; }); let firstHistoryLink = historyComponent.lists[0].rowEls[0].mainEl; + let promiseHidden = BrowserTestUtils.waitForEvent( + document, + "visibilitychange" + ); await EventUtils.synthesizeMouseAtCenter(firstHistoryLink, {}, content); await historyTelemetry(); - await switchToFxViewTab(browser.ownerGlobal); + await promiseHidden; + await openFirefoxViewTab(browser.ownerGlobal); // Test number of cards when sorted by site/domain await clearAllParentTelemetryEvents(); @@ -266,7 +263,7 @@ add_task(async function test_empty_states() { const { document } = browser.contentWindow; is(document.location.href, "about:firefoxview-next"); - navigateToCategory(document, "history"); + await navigateToCategoryAndWait(document, "history"); let historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; @@ -354,7 +351,7 @@ add_task(async function test_observers_removed_when_view_is_hidden() { ); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "history"); + await navigateToCategoryAndWait(document, "history"); const historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; let visitList = await TestUtils.waitForCondition(() => @@ -363,7 +360,12 @@ add_task(async function test_observers_removed_when_view_is_hidden() { info("The list should show a visit from the new tab."); await TestUtils.waitForCondition(() => visitList.rowEls.length === 1); + let promiseHidden = BrowserTestUtils.waitForEvent( + document, + "visibilitychange" + ); await BrowserTestUtils.switchTab(gBrowser, tab); + await promiseHidden; const { date } = await PlacesUtils.history .fetch(NEW_TAB_URL, { includeVisits: true, @@ -377,7 +379,7 @@ add_task(async function test_observers_removed_when_view_is_hidden() { ); info("The list should update when Firefox View is visible."); - await switchToFxViewTab(browser.ownerGlobal); + await openFirefoxViewTab(browser.ownerGlobal); visitList = await TestUtils.waitForCondition(() => historyComponent.cards?.[0]?.querySelector("fxview-tab-list") ); @@ -399,7 +401,7 @@ add_task(async function test_show_all_history_telemetry() { const { document } = browser.contentWindow; is(document.location.href, "about:firefoxview-next"); - navigateToCategory(document, "history"); + await navigateToCategoryAndWait(document, "history"); let historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; @@ -424,7 +426,7 @@ add_task(async function test_show_all_history_telemetry() { add_task(async function test_search_history() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "history"); + await navigateToCategoryAndWait(document, "history"); const historyComponent = document.querySelector("view-history"); historyComponent.profileAge = 8; await historyComponentReady(historyComponent); diff --git a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_opentabs_cards.js b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_opentabs_cards.js index 289a608f6bc4..7b737a80740d 100644 --- a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_opentabs_cards.js +++ b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_opentabs_cards.js @@ -20,9 +20,8 @@ add_setup(function () { async function navigateToOpenTabs(browser) { const document = browser.contentDocument; if (document.querySelector("named-deck").selectedViewName != "opentabs") { - navigateToCategory(document, "opentabs"); + await navigateToCategoryAndWait(browser.contentDocument, "opentabs"); } - await TestUtils.waitForTick(); } function getOpenTabsComponent(browser) { @@ -79,8 +78,14 @@ add_task(async function open_tab_same_window() { gBrowser.visibleTabs[0].linkedBrowser.currentURI.spec, "The first item represents the first visible tab" ); + let promiseHidden = BrowserTestUtils.waitForEvent( + browser.contentDocument, + "visibilitychange" + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + await promiseHidden; }); - await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + const [originalTab, newTab] = gBrowser.visibleTabs; await openFirefoxViewTab(window).then(async viewTab => { @@ -92,7 +97,12 @@ add_task(async function open_tab_same_window() { is(tabItems.length, 2, "There are two items."); is(tabItems[1].url, TEST_URL, "The newly opened tab appears last."); + let promiseHidden = BrowserTestUtils.waitForEvent( + browser.contentDocument, + "visibilitychange" + ); tabItems[0].mainEl.click(); + await promiseHidden; }); await BrowserTestUtils.waitForCondition( @@ -104,7 +114,14 @@ add_task(async function open_tab_same_window() { const browser = viewTab.linkedBrowser; const cards = getCards(browser); let tabItems = await getRowsForCard(cards[0]); + + let promiseHidden = BrowserTestUtils.waitForEvent( + browser.contentDocument, + "visibilitychange" + ); + tabItems[1].mainEl.click(); + await promiseHidden; }); await BrowserTestUtils.waitForCondition( @@ -128,11 +145,8 @@ add_task(async function open_tab_same_window() { return tabItems[0].url === TEST_URL; } ); - }); + await BrowserTestUtils.removeTab(newTab); - await BrowserTestUtils.removeTab(newTab); - await openFirefoxViewTab(window).then(async viewTab => { - const browser = viewTab.linkedBrowser; const [card] = getCards(browser); await TestUtils.waitForCondition( async () => (await getRowsForCard(card)).length === 1, @@ -300,11 +314,11 @@ add_task(async function toggle_show_more_link() { is(cards.length, NUMBER_OF_WINDOWS, "There are four windows."); lastCard = cards[NUMBER_OF_WINDOWS - 1]; lastWindow = windows[NUMBER_OF_WINDOWS - 2]; - }); - for (let i = 0; i < NUMBER_OF_TABS - 1; i++) { - await BrowserTestUtils.openNewForegroundTab(lastWindow.gBrowser); - } + for (let i = 0; i < NUMBER_OF_TABS - 1; i++) { + await BrowserTestUtils.openNewForegroundTab(lastWindow.gBrowser); + } + }); await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; diff --git a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_opentabs_recency_next.js b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_opentabs_recency_next.js index e3a70f2a6e89..7be1a7bfc234 100644 --- a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_opentabs_recency_next.js +++ b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_opentabs_recency_next.js @@ -159,10 +159,16 @@ add_task(async function test_single_window_tabs() { await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; await checkTabList(browser, [tabURL2, tabURL1]); + + // switch to the first tab + let promiseHidden = BrowserTestUtils.waitForEvent( + browser.contentDocument, + "visibilitychange" + ); + await BrowserTestUtils.switchTab(gBrowser, gBrowser.visibleTabs[0]); + await promiseHidden; }); - // switch to the first tab - await BrowserTestUtils.switchTab(gBrowser, gBrowser.visibleTabs[0]); // and check the results in the open tabs section of Recent Browsing await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; @@ -184,20 +190,29 @@ add_task(async function test_multiple_window_tabs() { await openFirefoxViewTab(win2).then(async viewTab => { const browser = viewTab.linkedBrowser; await checkTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]); - }); - Assert.equal( - tabUrl(win2.gBrowser.selectedTab), - fxViewURL, - `The selected tab in window 2 is ${fxViewURL}` - ); - info("Switching to first tab (tab3) in win2"); - await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[0]); - Assert.equal( - tabUrl(win2.gBrowser.selectedTab), - tabURL3, - `The selected tab in window 2 is ${tabURL3}` - ); + Assert.equal( + tabUrl(win2.gBrowser.selectedTab), + fxViewURL, + `The selected tab in window 2 is ${fxViewURL}` + ); + + info("Switching to first tab (tab3) in win2"); + let promiseHidden = BrowserTestUtils.waitForEvent( + browser.contentDocument, + "visibilitychange" + ); + await BrowserTestUtils.switchTab( + win2.gBrowser, + win2.gBrowser.visibleTabs[0] + ); + Assert.equal( + tabUrl(win2.gBrowser.selectedTab), + tabURL3, + `The selected tab in window 2 is ${tabURL3}` + ); + await promiseHidden; + }); info("Opening fxview in win2 to confirm tab3 is most recent"); await openFirefoxViewTab(win2).then(async viewTab => { @@ -221,10 +236,18 @@ add_task(async function test_multiple_window_tabs() { "In fxview, check result of activating window 1, where tab 2 is selected" ); await checkTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]); - }); - info("Switching to first visible tab (tab1) in win1"); - await BrowserTestUtils.switchTab(win1.gBrowser, win1.gBrowser.visibleTabs[0]); + let promiseHidden = BrowserTestUtils.waitForEvent( + browser.contentDocument, + "visibilitychange" + ); + info("Switching to first visible tab (tab1) in win1"); + await BrowserTestUtils.switchTab( + win1.gBrowser, + win1.gBrowser.visibleTabs[0] + ); + await promiseHidden; + }); // check result in the fxview in the 1st window info("Opening fxview in win1 to confirm tab1 is most recent"); @@ -272,10 +295,18 @@ add_task(async function test_minimize_restore_windows() { await openFirefoxViewTab(win2).then(async viewTab => { const browser = viewTab.linkedBrowser; await checkTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]); + + let promiseHidden = BrowserTestUtils.waitForEvent( + browser.contentDocument, + "visibilitychange" + ); + info("Switching to the first tab (tab3) in 2nd window"); + await BrowserTestUtils.switchTab( + win2.gBrowser, + win2.gBrowser.visibleTabs[0] + ); + await promiseHidden; }); - // - info("Switching to the first tab (tab3) in 2nd window"); - await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[0]); // then minimize the window, focusing the 1st window info("Minimizing win2, leaving tab 3 selected"); diff --git a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_syncedtabs_errors_firefoxview_next.js b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_syncedtabs_errors_firefoxview_next.js index 59e60d727760..7561e6e03741 100644 --- a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_syncedtabs_errors_firefoxview_next.js +++ b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_syncedtabs_errors_firefoxview_next.js @@ -59,7 +59,7 @@ add_task(async function test_network_offline() { sandbox.spy(TabsSetupFlowManager, "tryToClearError"); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); Services.obs.notifyObservers( @@ -115,7 +115,7 @@ add_task(async function test_sync_error() { const sandbox = await setupWithDesktopDevices(); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(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/firefoxview-next/browser_syncedtabs_firefoxview_next.js b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_syncedtabs_firefoxview_next.js index 114d991d6aab..3e8b73fcd0bc 100644 --- a/browser/components/firefoxview/tests/browser/firefoxview-next/browser_syncedtabs_firefoxview_next.js +++ b/browser/components/firefoxview/tests/browser/firefoxview-next/browser_syncedtabs_firefoxview_next.js @@ -27,7 +27,7 @@ add_task(async function test_unconfigured_initial_state() { }); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -46,7 +46,7 @@ add_task(async function test_unconfigured_initial_state() { EventUtils.synthesizeMouseAtCenter( emptyState.querySelector(`button[data-action="sign-in"]`), {}, - content + browser.contentWindow ); await TestUtils.waitForCondition( () => @@ -83,7 +83,7 @@ add_task(async function test_signed_in() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -102,7 +102,7 @@ add_task(async function test_signed_in() { EventUtils.synthesizeMouseAtCenter( emptyState.querySelector(`button[data-action="add-device"]`), {}, - content + browser.contentWindow ); await TestUtils.waitForCondition( () => @@ -146,7 +146,7 @@ add_task(async function test_no_synced_tabs() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -186,7 +186,7 @@ add_task(async function test_no_error_for_two_desktop() { await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -230,7 +230,7 @@ add_task(async function test_empty_state() { await withFirefoxView({ openNewWindow: true }, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -275,7 +275,7 @@ add_task(async function test_tabs() { await withFirefoxView({ openNewWindow: true }, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -363,7 +363,7 @@ add_task(async function test_empty_desktop_same_name() { await withFirefoxView({ openNewWindow: true }, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( @@ -411,7 +411,7 @@ add_task(async function test_empty_desktop_same_name_three() { await withFirefoxView({ openNewWindow: true }, async browser => { const { document } = browser.contentWindow; - navigateToCategory(document, "syncedtabs"); + await navigateToCategoryAndWait(document, "syncedtabs"); Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( diff --git a/browser/components/firefoxview/tests/browser/head.js b/browser/components/firefoxview/tests/browser/head.js index c03fd3818bc4..aa163a7b3ef5 100644 --- a/browser/components/firefoxview/tests/browser/head.js +++ b/browser/components/firefoxview/tests/browser/head.js @@ -597,6 +597,36 @@ function navigateToCategory(document, category) { navButton.buttonEl.click(); } +async function navigateToCategoryAndWait(document, category) { + info(`navigateToCategoryAndWait, for ${category}`); + const navigation = document.querySelector("fxview-category-navigation"); + const win = document.ownerGlobal; + SimpleTest.promiseFocus(win); + let navButton = Array.from(navigation.categoryButtons).find( + categoryButton => { + return categoryButton.name === category; + } + ); + const namedDeck = document.querySelector("named-deck"); + + await BrowserTestUtils.waitForCondition( + () => navButton.getBoundingClientRect().height, + `Waiting for ${category} button to be clickable` + ); + + EventUtils.synthesizeMouseAtCenter(navButton, {}, win); + + await BrowserTestUtils.waitForCondition(() => { + let selectedView = Array.from(namedDeck.children).find( + child => child.slot == "selected" + ); + return ( + namedDeck.selectedViewName == category && + selectedView?.getBoundingClientRect().height + ); + }, `Waiting for ${category} to be visible`); +} + /** * Switch to the Firefox View tab. * diff --git a/browser/components/firefoxview/viewpage.mjs b/browser/components/firefoxview/viewpage.mjs index 0277d5d3c268..22bed4369367 100644 --- a/browser/components/firefoxview/viewpage.mjs +++ b/browser/components/firefoxview/viewpage.mjs @@ -15,64 +15,58 @@ import "chrome://browser/content/firefoxview/fxview-tab-list.mjs"; import { placeLinkOnClipboard } from "./helpers.mjs"; -export class ViewPage extends MozLitElement { +/** + * A base class for content container views displayed on firefox-view. + * + * @property {boolean} recentBrowsing + * Is part of the recentbrowsing page view + * @property {boolean} paused + * No content will be updated and rendered while paused + */ +export class ViewPageContent extends MozLitElement { static get properties() { return { - selectedTab: { type: Boolean }, recentBrowsing: { type: Boolean }, + paused: { type: Boolean }, }; } - constructor() { super(); - this.selectedTab = false; - this.recentBrowsing = Boolean(this.closest("VIEW-RECENTBROWSING")); + // don't update or render until explicitly un-paused + this.paused = true; } - connectedCallback() { - super.connectedCallback(); - this.ownerDocument.addEventListener("visibilitychange", this); + get ownerViewPage() { + return this.closest("[type='page']") || this; } - disconnectedCallback() { - super.disconnectedCallback(); - this.ownerDocument.removeEventListener("visibilitychange", this); - } - - handleEvent(event) { - switch (event.type) { - case "visibilitychange": - if (this.ownerDocument.visibilityState === "visible") { - this.viewTabVisibleCallback(); - } else { - this.viewTabHiddenCallback(); - } - break; + get isVisible() { + if (!this.isConnected || this.ownerDocument.visibilityState != "visible") { + return false; } + return this.ownerViewPage.selectedTab; } /** - * Override this function to run a callback whenever Firefox View is visible. + * Override this function to run a callback whenever this content is visible. */ - viewTabVisibleCallback() {} + viewVisibleCallback() {} /** - * Override this function to run a callback whenever Firefox View is hidden. + * Override this function to run a callback whenever this content is hidden. */ - viewTabHiddenCallback() {} - - enter() { - this.selectedTab = true; - } - - exit() { - this.selectedTab = false; - } + viewHiddenCallback() {} getWindow() { return window.browsingContext.embedderWindowGlobal.browsingContext.window; } + getBrowserTab() { + return this.getWindow().gBrowser.getTabForBrowser( + window.browsingContext.embedderElement + ); + } + copyLink(e) { placeLinkOnClipboard(this.triggerNode.title, this.triggerNode.url); this.recordContextMenuTelemetry("copy-link", e); @@ -104,4 +98,73 @@ export class ViewPage extends MozLitElement { } ); } + + shouldUpdate(changedProperties) { + return !this.paused && super.shouldUpdate(changedProperties); + } +} + +/** + * A "page" in firefox view, which may be hidden or shown by the named-deck container or + * via the owner document's visibility + * + * @property {boolean} selectedTab + * Is this page the selected view in the named-deck container + */ +export class ViewPage extends ViewPageContent { + static get properties() { + return { + selectedTab: { type: Boolean }, + }; + } + + constructor() { + super(); + this.selectedTab = false; + this.recentBrowsing = Boolean(this.closest("VIEW-RECENTBROWSING")); + this.onVisibilityChange = this.onVisibilityChange.bind(this); + } + + onVisibilityChange(event) { + if (this.isVisible) { + this.paused = false; + this.viewVisibleCallback(); + } else if ( + this.ownerViewPage.selectedTab && + this.ownerDocument.visibilityState == "hidden" + ) { + this.paused = true; + this.viewHiddenCallback(); + } + } + + connectedCallback() { + super.connectedCallback(); + this.ownerDocument.addEventListener( + "visibilitychange", + this.onVisibilityChange + ); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.ownerDocument.removeEventListener( + "visibilitychange", + this.onVisibilityChange + ); + } + + enter() { + this.selectedTab = true; + if (this.isVisible) { + this.paused = false; + this.viewVisibleCallback(); + } + } + + exit() { + this.selectedTab = false; + this.paused = true; + this.viewHiddenCallback(); + } }