Bug 1863783 - Ensure fxview page content only listens/observes and renders updates when visible. r=fxview-reviewers,sclements,jsudiaman

* Add a type=page to the top-level ViewPage instances
* Rename viewTabVisibleCallback and viewTabHiddenCallback to view*Callback and call each when selectedness or visiblity changes
* Ensure active view/pages are always properly initialized during page load and category switching
* Add a test to verify no mutations happen when tabs change while firefox view is inactive
* Fix tests to better account for loading and readiness sequence when activating firefox view

Differential Revision: https://phabricator.services.mozilla.com/D193744
This commit is contained in:
Sam Foster 2023-11-23 07:57:33 +00:00
Родитель 80de3c062f
Коммит 2cd97ee94c
17 изменённых файлов: 767 добавлений и 251 удалений

Просмотреть файл

@ -92,7 +92,7 @@
<main id="pages" role="application" data-l10n-id="firefoxview-page-label">
<div class="main-container">
<named-deck>
<view-recentbrowsing name="recentbrowsing">
<view-recentbrowsing name="recentbrowsing" type="page">
<div>
<view-opentabs slot="opentabs"></view-opentabs>
</div>
@ -103,10 +103,13 @@
<view-syncedtabs slot="syncedtabs"></view-syncedtabs>
</div>
</view-recentbrowsing>
<view-history name="history"></view-history>
<view-opentabs name="opentabs"></view-opentabs>
<view-recentlyclosed name="recentlyclosed"></view-recentlyclosed>
<view-syncedtabs name="syncedtabs"></view-syncedtabs>
<view-history name="history" type="page"></view-history>
<view-opentabs name="opentabs" type="page"></view-opentabs>
<view-recentlyclosed
name="recentlyclosed"
type="page"
></view-recentlyclosed>
<view-syncedtabs name="syncedtabs" type="page"></view-syncedtabs>
</named-deck>
</div>
</main>

Просмотреть файл

@ -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",
}
);
}

Просмотреть файл

@ -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 = {

Просмотреть файл

@ -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 {
<view-opentabs-card
class=${cardClasses}
.tabs=${currentWindowTabs}
.paused=${this.paused}
data-inner-id="${this.currentWindow.windowGlobalChild
.innerWindowId}"
data-l10n-id="firefoxview-opentabs-current-window-header"
@ -216,6 +240,7 @@ class OpenTabsInView extends ViewPage {
<view-opentabs-card
class=${cardClasses}
.tabs=${tabs}
.paused=${this.paused}
data-inner-id="${win.windowGlobalChild.innerWindowId}"
data-l10n-id="firefoxview-opentabs-window-header"
data-l10n-args="${JSON.stringify({ winID })}"
@ -251,6 +276,7 @@ class OpenTabsInView extends ViewPage {
return html`<view-opentabs-card
.tabs=${tabs}
.recentBrowsing=${true}
.paused=${this.paused}
.devices=${this.devices}
></view-opentabs-card>`;
}
@ -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`
<link

Просмотреть файл

@ -11,11 +11,21 @@ class RecentBrowsingInView extends ViewPage {
this.pageType = "recentbrowsing";
}
connectedCallback() {
super.connectedCallback();
viewVisibleCallback() {
for (let child of this.children) {
let childView = child.firstElementChild;
childView.paused = false;
childView.viewVisibleCallback();
}
}
disconnectedCallback() {}
viewHiddenCallback() {
for (let child of this.children) {
let childView = child.firstElementChild;
childView.paused = true;
childView.viewHiddenCallback();
}
}
render() {
return html`

Просмотреть файл

@ -33,6 +33,7 @@ function getWindow() {
class RecentlyClosedTabsInView extends ViewPage {
constructor() {
super();
this._started = false;
this.boundObserve = (...args) => 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`
<link
rel="stylesheet"

Просмотреть файл

@ -25,6 +25,7 @@ const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open";
class SyncedTabsInView extends ViewPage {
constructor() {
super();
this._started = false;
this.boundObserve = (...args) => 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");

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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

Просмотреть файл

@ -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);
});

Просмотреть файл

@ -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);

Просмотреть файл

@ -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;

Просмотреть файл

@ -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");

Просмотреть файл

@ -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");

Просмотреть файл

@ -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(

Просмотреть файл

@ -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.
*

Просмотреть файл

@ -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();
}
}