Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
"use strict";
requestLongerTimeout(2);
const { FxAccounts } = ChromeUtils.importESModule(
"resource://gre/modules/FxAccounts.sys.mjs"
);
let { SyncedTabs } = ChromeUtils.importESModule(
);
let { UIState } = ChromeUtils.importESModule(
);
ChromeUtils.defineESModuleGetters(this, {
UITour: "resource:///modules/UITour.sys.mjs",
});
const DECKINDEX_TABS = 0;
const DECKINDEX_FETCHING = 1;
const DECKINDEX_TABSDISABLED = 2;
const DECKINDEX_NOCLIENTS = 3;
var initialLocation = gBrowser.currentURI.spec;
var newTab = null;
// A helper to notify there are new tabs. Returns a promise that is resolved
// once the UI has been updated.
function updateTabsPanel() {
let promiseTabsUpdated = promiseObserverNotified(
"synced-tabs-menu:test:tabs-updated"
);
Services.obs.notifyObservers(null, SyncedTabs.TOPIC_TABS_CHANGED);
return promiseTabsUpdated;
}
// This is the mock we use for SyncedTabs.sys.mjs - tests may override various
// functions.
let mockedInternal = {
get isConfiguredToSyncTabs() {
return true;
},
getTabClients() {
return Promise.resolve([]);
},
syncTabs() {
return Promise.resolve();
},
hasSyncedThisSession: false,
};
add_setup(async function () {
const getSignedInUser = FxAccounts.config.getSignedInUser;
FxAccounts.config.getSignedInUser = async () =>
Promise.resolve({ uid: "uid", email: "foo@bar.com" });
Services.prefs.setCharPref(
"identity.fxaccounts.remote.root",
);
let oldInternal = SyncedTabs._internal;
SyncedTabs._internal = mockedInternal;
let origNotifyStateUpdated = UIState._internal.notifyStateUpdated;
// Sync start-up will interfere with our tests, don't let UIState send UI updates.
UIState._internal.notifyStateUpdated = () => {};
// Force gSync initialization
gSync.init();
registerCleanupFunction(() => {
FxAccounts.config.getSignedInUser = getSignedInUser;
Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
UIState._internal.notifyStateUpdated = origNotifyStateUpdated;
SyncedTabs._internal = oldInternal;
});
});
// The test expects the about:preferences#sync page to open in the current tab
async function openPrefsFromMenuPanel(expectedPanelId, entryPoint) {
info("Check Sync button functionality");
CustomizableUI.addWidgetToArea(
"sync-button",
CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
);
await waitForOverflowButtonShown();
// check the button's functionality
await document.getElementById("nav-bar").overflowable.show();
if (entryPoint == "uitour") {
UITour.tourBrowsersByWindow.set(window, new Set());
UITour.tourBrowsersByWindow.get(window).add(gBrowser.selectedBrowser);
}
let syncButton = document.getElementById("sync-button");
ok(syncButton, "The Sync button was added to the Panel Menu");
let tabsUpdatedPromise = promiseObserverNotified(
"synced-tabs-menu:test:tabs-updated"
);
syncButton.click();
let syncPanel = document.getElementById("PanelUI-remotetabs");
let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
await Promise.all([tabsUpdatedPromise, viewShownPromise]);
ok(syncPanel.getAttribute("visible"), "Sync Panel is in view");
// Sync is not configured - verify that state is reflected.
let subpanel = document.getElementById(expectedPanelId);
ok(!subpanel.hidden, "sync setup element is visible");
// Find and click the "setup" button.
let setupButton = subpanel.querySelector(".PanelUI-remotetabs-button");
setupButton.click();
await new Promise(resolve => {
let handler = async e => {
if (
e.originalTarget != gBrowser.selectedBrowser.contentDocument ||
e.target.location.href == "about:blank"
) {
info("Skipping spurious 'load' event for " + e.target.location.href);
return;
}
gBrowser.selectedBrowser.removeEventListener("load", handler, true);
resolve();
};
gBrowser.selectedBrowser.addEventListener("load", handler, true);
});
newTab = gBrowser.selectedTab;
is(
gBrowser.currentURI.spec,
"about:preferences?entrypoint=" + entryPoint + "#sync",
"Firefox Sync preference page opened with `menupanel` entrypoint"
);
ok(!isOverflowOpen(), "The panel closed");
if (isOverflowOpen()) {
await hideOverflow();
}
}
function hideOverflow() {
let panelHidePromise = promiseOverflowHidden(window);
PanelUI.overflowPanel.hidePopup();
return panelHidePromise;
}
async function asyncCleanup() {
// reset the panel UI to the default state
await resetCustomization();
ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
// restore the tabs
BrowserTestUtils.addTab(gBrowser, initialLocation);
gBrowser.removeTab(newTab);
UITour.tourBrowsersByWindow.delete(window);
}
// When Sync is not setup.
add_task(async function () {
gSync.updateAllUI({ status: UIState.STATUS_NOT_CONFIGURED });
await openPrefsFromMenuPanel("PanelUI-remotetabs-setupsync", "synced-tabs");
});
add_task(asyncCleanup);
// When an account is connected by Sync is not enabled.
add_task(async function () {
gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, syncEnabled: false });
await openPrefsFromMenuPanel(
"PanelUI-remotetabs-syncdisabled",
"synced-tabs"
);
});
add_task(asyncCleanup);
// When Sync is configured in an unverified state.
add_task(async function () {
gSync.updateAllUI({
status: UIState.STATUS_NOT_VERIFIED,
email: "foo@bar.com",
});
await openPrefsFromMenuPanel("PanelUI-remotetabs-unverified", "synced-tabs");
});
add_task(asyncCleanup);
// When Sync is configured in a "needs reauthentication" state.
add_task(async function () {
gSync.updateAllUI({
status: UIState.STATUS_LOGIN_FAILED,
email: "foo@bar.com",
});
await openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs");
});
// Test the Connect Another Device button
add_task(async function () {
gSync.updateAllUI({
status: UIState.STATUS_SIGNED_IN,
syncEnabled: true,
email: "foo@bar.com",
lastSync: new Date(),
});
let button = document.getElementById(
"PanelUI-remotetabs-connect-device-button"
);
ok(button, "found the button");
await document.getElementById("nav-bar").overflowable.show();
// Actually show the fxa view:
let shown = BrowserTestUtils.waitForEvent(
document.getElementById("PanelUI-remotetabs"),
"ViewShown"
);
PanelUI.showSubView(
"PanelUI-remotetabs",
document.getElementById("sync-button")
);
await shown;
let promiseTabOpened = BrowserTestUtils.waitForNewTab(
gBrowser,
url =>
url.includes("entrypoint=synced-tabs")
);
button.click();
// the panel should have been closed.
ok(!isOverflowOpen(), "click closed the panel");
await promiseTabOpened;
gBrowser.removeTab(gBrowser.selectedTab);
});
// Test the "Sync Now" button
add_task(async function () {
gSync.updateAllUI({
status: UIState.STATUS_SIGNED_IN,
syncEnabled: true,
email: "foo@bar.com",
lastSync: new Date(),
});
await document.getElementById("nav-bar").overflowable.show();
let tabsUpdatedPromise = promiseObserverNotified(
"synced-tabs-menu:test:tabs-updated"
);
let syncPanel = document.getElementById("PanelUI-remotetabs");
let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
let syncButton = document.getElementById("sync-button");
syncButton.click();
await Promise.all([tabsUpdatedPromise, viewShownPromise]);
ok(syncPanel.getAttribute("visible"), "Sync Panel is in view");
let subpanel = document.getElementById("PanelUI-remotetabs-main");
ok(!subpanel.hidden, "main pane is visible");
let deck = document.getElementById("PanelUI-remotetabs-deck");
// The widget is still fetching tabs, as we've neutered everything that
// provides them
is(deck.selectedIndex, DECKINDEX_FETCHING, "first deck entry is visible");
// Tell the widget there are tabs available, but with zero clients.
mockedInternal.getTabClients = () => {
return Promise.resolve([]);
};
mockedInternal.hasSyncedThisSession = true;
await updateTabsPanel();
// The UI should be showing the "no clients" pane.
is(
deck.selectedIndex,
DECKINDEX_NOCLIENTS,
"no-clients deck entry is visible"
);
// Tell the widget there are tabs available - we have 3 clients, one with no
// tabs.
mockedInternal.getTabClients = () => {
return Promise.resolve([
{
id: "guid_mobile",
type: "client",
name: "My Phone",
lastModified: 1492201200,
tabs: [],
},
{
id: "guid_desktop",
type: "client",
name: "My Desktop",
lastModified: 1492201200,
tabs: [
{
lastUsed: 10, // the most recent
},
{
lastUsed: 1, // the least recent.
},
{
lastUsed: 5,
},
],
},
{
id: "guid_second_desktop",
name: "My Other Desktop",
lastModified: 1492201200,
tabs: [
{
lastUsed: 6,
},
],
},
]);
};
await updateTabsPanel();
// The UI should be showing tabs!
is(deck.selectedIndex, DECKINDEX_TABS, "no-clients deck entry is visible");
let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
let node = tabList.firstElementChild;
// First entry should be the client with the most-recent tab.
is(node.nodeName, "vbox");
let currentClient = node;
node = node.firstElementChild;
is(node.getAttribute("itemtype"), "client", "node is a client entry");
is(node.textContent, "My Desktop", "correct client");
// Next node is an hbox, that contains the tab and potentially
// a button for closing the tab remotely
node = node.nextElementSibling;
is(node.nodeName, "hbox");
// Next entry is the most-recent tab
let childNode = node.firstElementChild;
is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
// Next entry is the next-most-recent tab
node = node.nextElementSibling;
is(node.nodeName, "hbox");
childNode = node.firstElementChild;
is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
// Next entry is the least-recent tab from the first client.
node = node.nextElementSibling;
is(node.nodeName, "hbox");
childNode = node.firstElementChild;
is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
node = node.nextElementSibling;
is(node, null, "no more siblings");
// Next is a toolbarseparator between the clients.
node = currentClient.nextElementSibling;
is(node.nodeName, "toolbarseparator");
// Next is the container for client 2.
node = node.nextElementSibling;
is(node.nodeName, "vbox");
currentClient = node;
// Next is the client with 1 tab.
node = node.firstElementChild;
is(node.getAttribute("itemtype"), "client", "node is a client entry");
is(node.textContent, "My Other Desktop", "correct client");
// Its single tab
node = node.nextElementSibling;
is(node.nodeName, "hbox");
childNode = node.firstElementChild;
is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
node = node.nextElementSibling;
is(node, null, "no more siblings");
// Next is a toolbarseparator between the clients.
node = currentClient.nextElementSibling;
is(node.nodeName, "toolbarseparator");
// Next is the container for client 3.
node = node.nextElementSibling;
is(node.nodeName, "vbox");
currentClient = node;
// Next is the client with no tab.
node = node.firstElementChild;
is(node.getAttribute("itemtype"), "client", "node is a client entry");
is(node.textContent, "My Phone", "correct client");
// There is a single node saying there's no tabs for the client.
node = node.nextElementSibling;
is(node.nodeName, "label", "node is a label");
is(node.getAttribute("itemtype"), null, "node is neither a tab nor a client");
node = node.nextElementSibling;
is(node, null, "no more siblings");
is(currentClient.nextElementSibling, null, "no more clients");
// Check accessibility. There should be containers for each client, with an
// aria attribute that identifies the client name.
let clientContainers = [
...tabList.querySelectorAll("[aria-labelledby]").values(),
];
let labelIds = clientContainers.map(container =>
container.getAttribute("aria-labelledby")
);
let labels = labelIds.map(id => document.getElementById(id).textContent);
Assert.deepEqual(labels.sort(), [
"My Desktop",
"My Other Desktop",
"My Phone",
]);
let didSync = false;
let oldDoSync = gSync.doSync;
gSync.doSync = function () {
didSync = true;
gSync.doSync = oldDoSync;
};
let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
is(syncNowButton.disabled, false);
syncNowButton.click();
ok(didSync, "clicking the button called the correct function");
await hideOverflow();
});
// Test the pagination capabilities (Show More/All tabs)
add_task(async function () {
mockedInternal.getTabClients = () => {
return Promise.resolve([
{
id: "guid_desktop",
type: "client",
name: "My Desktop",
lastModified: 1492201200,
tabs: (function () {
let allTabsDesktop = [];
// We choose 77 tabs, because TABS_PER_PAGE is 25, which means
// on the second to last page we should have 22 items shown
// (because we have to show at least NEXT_PAGE_MIN_TABS=5 tabs on the last page)
for (let i = 1; i <= 77; i++) {
allTabsDesktop.push({ title: "Tab #" + i, url: SAMPLE_TAB_URL });
}
return allTabsDesktop;
})(),
},
]);
};
gSync.updateAllUI({
status: UIState.STATUS_SIGNED_IN,
syncEnabled: true,
lastSync: new Date(),
email: "foo@bar.com",
});
await document.getElementById("nav-bar").overflowable.show();
let tabsUpdatedPromise = promiseObserverNotified(
"synced-tabs-menu:test:tabs-updated"
);
let syncPanel = document.getElementById("PanelUI-remotetabs");
let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
let syncButton = document.getElementById("sync-button");
syncButton.click();
await Promise.all([tabsUpdatedPromise, viewShownPromise]);
// Check pre-conditions
ok(syncPanel.getAttribute("visible"), "Sync Panel is in view");
let subpanel = document.getElementById("PanelUI-remotetabs-main");
ok(!subpanel.hidden, "main pane is visible");
let deck = document.getElementById("PanelUI-remotetabs-deck");
is(deck.selectedIndex, DECKINDEX_TABS, "we should be showing tabs");
function checkTabsPage(tabsShownCount, showMoreLabel) {
let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
let node = tabList.firstElementChild.firstElementChild;
is(node.getAttribute("itemtype"), "client", "node is a client entry");
is(node.textContent, "My Desktop", "correct client");
for (let i = 0; i < tabsShownCount; i++) {
node = node.nextElementSibling;
is(node.nodeName, "hbox");
let childNode = node.firstElementChild;
is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
is(
childNode.getAttribute("label"),
"Tab #" + (i + 1),
"the tab is the correct one"
);
is(
childNode.getAttribute("targetURI"),
SAMPLE_TAB_URL,
"url is the correct one"
);
}
let showMoreButton;
if (showMoreLabel) {
node = showMoreButton = node.nextElementSibling;
is(
node.getAttribute("itemtype"),
"showmorebutton",
"node is a show more button"
);
is(node.getAttribute("label"), showMoreLabel);
}
node = node.nextElementSibling;
is(node, null, "no more entries");
return showMoreButton;
}
async function checkCanOpenURL() {
let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
let node =
tabList.firstElementChild.firstElementChild.nextElementSibling
.firstElementChild;
let promiseTabOpened = BrowserTestUtils.waitForLocationChange(
gBrowser,
SAMPLE_TAB_URL
);
node.click();
await promiseTabOpened;
}
let showMoreButton;
function clickShowMoreButton() {
let promise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
showMoreButton.click();
return promise;
}
showMoreButton = checkTabsPage(25, "Show more tabs");
await clickShowMoreButton();
checkTabsPage(77, null);
/* calling this will close the overflow menu */
await checkCanOpenURL();
});