tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_synced_tabs_menu.js (18420B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 requestLongerTimeout(2);
      8 
      9 const { FxAccounts } = ChromeUtils.importESModule(
     10  "resource://gre/modules/FxAccounts.sys.mjs"
     11 );
     12 let { SyncedTabs } = ChromeUtils.importESModule(
     13  "resource://services-sync/SyncedTabs.sys.mjs"
     14 );
     15 let { UIState } = ChromeUtils.importESModule(
     16  "resource://services-sync/UIState.sys.mjs"
     17 );
     18 
     19 ChromeUtils.defineESModuleGetters(this, {
     20  UITour: "moz-src:///browser/components/uitour/UITour.sys.mjs",
     21 });
     22 
     23 const DECKINDEX_TABS = 0;
     24 const DECKINDEX_FETCHING = 1;
     25 const DECKINDEX_TABSDISABLED = 2;
     26 const DECKINDEX_NOCLIENTS = 3;
     27 
     28 const SAMPLE_TAB_URL = "https://example.com/";
     29 
     30 var initialLocation = gBrowser.currentURI.spec;
     31 var newTab = null;
     32 
     33 // A helper to notify there are new tabs. Returns a promise that is resolved
     34 // once the UI has been updated.
     35 function updateTabsPanel() {
     36  let promiseTabsUpdated = promiseObserverNotified(
     37    "synced-tabs-menu:test:tabs-updated"
     38  );
     39  Services.obs.notifyObservers(null, SyncedTabs.TOPIC_TABS_CHANGED);
     40  return promiseTabsUpdated;
     41 }
     42 
     43 // This is the mock we use for SyncedTabs.sys.mjs - tests may override various
     44 // functions.
     45 let mockedInternal = {
     46  get isConfiguredToSyncTabs() {
     47    return true;
     48  },
     49  getTabClients() {
     50    return Promise.resolve([]);
     51  },
     52  syncTabs() {
     53    return Promise.resolve();
     54  },
     55  hasSyncedThisSession: false,
     56 };
     57 
     58 add_setup(async function () {
     59  const getSignedInUser = FxAccounts.config.getSignedInUser;
     60 
     61  FxAccounts.config.getSignedInUser = async () =>
     62    Promise.resolve({ uid: "uid", email: "foo@bar.com" });
     63 
     64  Services.prefs.setCharPref(
     65    "identity.fxaccounts.remote.root",
     66    "https://example.com/"
     67  );
     68 
     69  // Mock the global fxAccounts object used by gSync
     70  const origWindowFxAccounts = window.fxAccounts;
     71  window.fxAccounts = {
     72    getSignedInUser: async () => ({ uid: "uid", email: "foo@bar.com" }),
     73    hasLocalSession: async () => true,
     74    keys: {
     75      canGetKeyForScope: async () => true,
     76    },
     77    device: {
     78      recentDeviceList: null,
     79    },
     80  };
     81 
     82  let oldInternal = SyncedTabs._internal;
     83  SyncedTabs._internal = mockedInternal;
     84 
     85  let origNotifyStateUpdated = UIState._internal.notifyStateUpdated;
     86  // Sync start-up will interfere with our tests, don't let UIState send UI updates.
     87  UIState._internal.notifyStateUpdated = () => {};
     88 
     89  // Force gSync initialization
     90  gSync.init();
     91 
     92  registerCleanupFunction(() => {
     93    FxAccounts.config.getSignedInUser = getSignedInUser;
     94    Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
     95    UIState._internal.notifyStateUpdated = origNotifyStateUpdated;
     96    window.fxAccounts = origWindowFxAccounts;
     97    SyncedTabs._internal = oldInternal;
     98  });
     99 });
    100 
    101 // The test expects the about:preferences#sync page to open in the current tab
    102 async function openPrefsFromMenuPanel(expectedPanelId, entryPoint) {
    103  info("Check Sync button functionality");
    104  CustomizableUI.addWidgetToArea(
    105    "sync-button",
    106    CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
    107  );
    108 
    109  await waitForOverflowButtonShown();
    110 
    111  // check the button's functionality
    112  await document.getElementById("nav-bar").overflowable.show();
    113 
    114  if (entryPoint == "uitour") {
    115    UITour.tourBrowsersByWindow.set(window, new Set());
    116    UITour.tourBrowsersByWindow.get(window).add(gBrowser.selectedBrowser);
    117  }
    118 
    119  let syncButton = document.getElementById("sync-button");
    120  ok(syncButton, "The Sync button was added to the Panel Menu");
    121 
    122  let tabsUpdatedPromise = promiseObserverNotified(
    123    "synced-tabs-menu:test:tabs-updated"
    124  );
    125  syncButton.click();
    126  let syncPanel = document.getElementById("PanelUI-remotetabs");
    127  let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
    128  await Promise.all([tabsUpdatedPromise, viewShownPromise]);
    129  ok(syncPanel.getAttribute("visible"), "Sync Panel is in view");
    130 
    131  // Sync is not configured - verify that state is reflected.
    132  let subpanel = document.getElementById(expectedPanelId);
    133  ok(!subpanel.hidden, "sync setup element is visible");
    134 
    135  // Find and click the "setup" button.
    136  let setupButton = subpanel.querySelector(".PanelUI-remotetabs-button");
    137  setupButton.click();
    138 
    139  await new Promise(resolve => {
    140    let handler = async e => {
    141      if (
    142        e.originalTarget != gBrowser.selectedBrowser.contentDocument ||
    143        e.target.location.href == "about:blank"
    144      ) {
    145        info("Skipping spurious 'load' event for " + e.target.location.href);
    146        return;
    147      }
    148      gBrowser.selectedBrowser.removeEventListener("load", handler, true);
    149      resolve();
    150    };
    151    gBrowser.selectedBrowser.addEventListener("load", handler, true);
    152  });
    153  newTab = gBrowser.selectedTab;
    154 
    155  is(
    156    gBrowser.currentURI.spec,
    157    "about:preferences?entrypoint=" + entryPoint + "#sync",
    158    "Firefox Sync preference page opened with `menupanel` entrypoint"
    159  );
    160  ok(!isOverflowOpen(), "The panel closed");
    161 
    162  if (isOverflowOpen()) {
    163    await hideOverflow();
    164  }
    165 }
    166 
    167 function hideOverflow() {
    168  let panelHidePromise = promiseOverflowHidden(window);
    169  PanelUI.overflowPanel.hidePopup();
    170  return panelHidePromise;
    171 }
    172 
    173 async function asyncCleanup() {
    174  // reset the panel UI to the default state
    175  await resetCustomization();
    176  ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
    177 
    178  // restore the tabs
    179  BrowserTestUtils.addTab(gBrowser, initialLocation);
    180  gBrowser.removeTab(newTab);
    181  UITour.tourBrowsersByWindow.delete(window);
    182 }
    183 
    184 // When Sync is not setup.
    185 add_task(async function () {
    186  gSync.updateAllUI({ status: UIState.STATUS_NOT_CONFIGURED });
    187  await openPrefsFromMenuPanel("PanelUI-remotetabs-setupsync", "synced-tabs");
    188 });
    189 add_task(asyncCleanup);
    190 
    191 // When an account is connected by Sync is not enabled.
    192 add_task(async function () {
    193  gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, syncEnabled: false });
    194  await openPrefsFromMenuPanel(
    195    "PanelUI-remotetabs-syncdisabled",
    196    "synced-tabs"
    197  );
    198 });
    199 add_task(asyncCleanup);
    200 
    201 // When Sync is configured in an unverified state.
    202 add_task(async function () {
    203  gSync.updateAllUI({
    204    status: UIState.STATUS_NOT_VERIFIED,
    205    email: "foo@bar.com",
    206  });
    207  await openPrefsFromMenuPanel("PanelUI-remotetabs-unverified", "synced-tabs");
    208 });
    209 add_task(asyncCleanup);
    210 
    211 // When Sync is configured in a "needs reauthentication" state.
    212 add_task(async function () {
    213  gSync.updateAllUI({
    214    status: UIState.STATUS_LOGIN_FAILED,
    215    email: "foo@bar.com",
    216  });
    217  await openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs");
    218 });
    219 
    220 // Test the Connect Another Device button
    221 add_task(async function () {
    222  gSync.updateAllUI({
    223    status: UIState.STATUS_SIGNED_IN,
    224    syncEnabled: true,
    225    email: "foo@bar.com",
    226    lastSync: new Date(),
    227  });
    228 
    229  let button = document.getElementById(
    230    "PanelUI-remotetabs-connect-device-button"
    231  );
    232  ok(button, "found the button");
    233 
    234  await document.getElementById("nav-bar").overflowable.show();
    235  // Actually show the fxa view:
    236  let shown = BrowserTestUtils.waitForEvent(
    237    document.getElementById("PanelUI-remotetabs"),
    238    "ViewShown"
    239  );
    240  PanelUI.showSubView(
    241    "PanelUI-remotetabs",
    242    document.getElementById("sync-button")
    243  );
    244  await shown;
    245 
    246  let promiseTabOpened = BrowserTestUtils.waitForNewTab(
    247    gBrowser,
    248    url =>
    249      url.startsWith("https://example.com/connect_another_device") &&
    250      url.includes("entrypoint=synced-tabs")
    251  );
    252  button.click();
    253  // the panel should have been closed.
    254  ok(!isOverflowOpen(), "click closed the panel");
    255  await promiseTabOpened;
    256 
    257  gBrowser.removeTab(gBrowser.selectedTab);
    258 });
    259 
    260 // Test the "Sync Now" button
    261 add_task(async function () {
    262  await SpecialPowers.pushPrefEnv({
    263    set: [["browser.tabs.remoteSVGIconDecoding", true]],
    264  });
    265 
    266  gSync.updateAllUI({
    267    status: UIState.STATUS_SIGNED_IN,
    268    syncEnabled: true,
    269    email: "foo@bar.com",
    270    lastSync: new Date(),
    271  });
    272 
    273  await document.getElementById("nav-bar").overflowable.show();
    274  let tabsUpdatedPromise = promiseObserverNotified(
    275    "synced-tabs-menu:test:tabs-updated"
    276  );
    277  let syncPanel = document.getElementById("PanelUI-remotetabs");
    278  let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
    279  let syncButton = document.getElementById("sync-button");
    280  syncButton.click();
    281  await Promise.all([tabsUpdatedPromise, viewShownPromise]);
    282  ok(syncPanel.getAttribute("visible"), "Sync Panel is in view");
    283 
    284  let subpanel = document.getElementById("PanelUI-remotetabs-main");
    285  ok(!subpanel.hidden, "main pane is visible");
    286  let deck = document.getElementById("PanelUI-remotetabs-deck");
    287 
    288  // The widget is still fetching tabs, as we've neutered everything that
    289  // provides them
    290  is(deck.selectedIndex, DECKINDEX_FETCHING, "first deck entry is visible");
    291 
    292  // Tell the widget there are tabs available, but with zero clients.
    293  mockedInternal.getTabClients = () => {
    294    return Promise.resolve([]);
    295  };
    296  mockedInternal.hasSyncedThisSession = true;
    297  await updateTabsPanel();
    298  // The UI should be showing the "no clients" pane.
    299  is(
    300    deck.selectedIndex,
    301    DECKINDEX_NOCLIENTS,
    302    "no-clients deck entry is visible"
    303  );
    304 
    305  // Tell the widget there are tabs available - we have 3 clients, one with no
    306  // tabs.
    307  mockedInternal.getTabClients = () => {
    308    return Promise.resolve([
    309      {
    310        id: "guid_mobile",
    311        type: "client",
    312        name: "My Phone",
    313        lastModified: 1492201200,
    314        tabs: [],
    315      },
    316      {
    317        id: "guid_desktop",
    318        type: "client",
    319        name: "My Desktop",
    320        lastModified: 1492201200,
    321        tabs: [
    322          {
    323            title: "http://example.com/10",
    324            lastUsed: 10, // the most recent
    325          },
    326          {
    327            title: "http://example.com/1",
    328            lastUsed: 1, // the least recent.
    329          },
    330          {
    331            title: "http://example.com/5",
    332            lastUsed: 5,
    333          },
    334        ],
    335      },
    336      {
    337        id: "guid_second_desktop",
    338        name: "My Other Desktop",
    339        lastModified: 1492201200,
    340        tabs: [
    341          {
    342            title: "http://example.com/6",
    343            icon: "http://example.com/favicon.ico",
    344            lastUsed: 6,
    345          },
    346        ],
    347      },
    348    ]);
    349  };
    350  await updateTabsPanel();
    351 
    352  // The UI should be showing tabs!
    353  is(deck.selectedIndex, DECKINDEX_TABS, "no-clients deck entry is visible");
    354  let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
    355  let node = tabList.firstElementChild;
    356  // First entry should be the client with the most-recent tab.
    357  is(node.nodeName, "vbox");
    358  let currentClient = node;
    359  node = node.firstElementChild;
    360  is(node.getAttribute("itemtype"), "client", "node is a client entry");
    361  is(node.textContent, "My Desktop", "correct client");
    362  // Next node is an hbox, that contains the tab and potentially
    363  // a button for closing the tab remotely
    364  node = node.nextElementSibling;
    365  is(node.nodeName, "hbox");
    366  // Next entry is the most-recent tab
    367  let childNode = node.firstElementChild;
    368  is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
    369  is(childNode.getAttribute("label"), "http://example.com/10");
    370 
    371  // Next entry is the next-most-recent tab
    372  node = node.nextElementSibling;
    373  is(node.nodeName, "hbox");
    374  childNode = node.firstElementChild;
    375  is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
    376  is(childNode.getAttribute("label"), "http://example.com/5");
    377 
    378  // Next entry is the least-recent tab from the first client.
    379  node = node.nextElementSibling;
    380  is(node.nodeName, "hbox");
    381  childNode = node.firstElementChild;
    382  is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
    383  is(childNode.getAttribute("label"), "http://example.com/1");
    384  node = node.nextElementSibling;
    385  is(node, null, "no more siblings");
    386 
    387  // Next is a toolbarseparator between the clients.
    388  node = currentClient.nextElementSibling;
    389  is(node.nodeName, "toolbarseparator");
    390 
    391  // Next is the container for client 2.
    392  node = node.nextElementSibling;
    393  is(node.nodeName, "vbox");
    394  currentClient = node;
    395 
    396  // Next is the client with 1 tab.
    397  node = node.firstElementChild;
    398  is(node.getAttribute("itemtype"), "client", "node is a client entry");
    399  is(node.textContent, "My Other Desktop", "correct client");
    400  // Its single tab
    401  node = node.nextElementSibling;
    402  is(node.nodeName, "hbox");
    403  childNode = node.firstElementChild;
    404  is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
    405  is(childNode.getAttribute("label"), "http://example.com/6");
    406  // Check the favicon image.
    407  let image = new URL(childNode.getAttribute("image"));
    408  is(image.protocol, "moz-remote-image:", "image protocol is correct");
    409  is(
    410    image.searchParams.get("url"),
    411    "http://example.com/favicon.ico",
    412    "image url is correct"
    413  );
    414  node = node.nextElementSibling;
    415  is(node, null, "no more siblings");
    416 
    417  // Next is a toolbarseparator between the clients.
    418  node = currentClient.nextElementSibling;
    419  is(node.nodeName, "toolbarseparator");
    420 
    421  // Next is the container for client 3.
    422  node = node.nextElementSibling;
    423  is(node.nodeName, "vbox");
    424  currentClient = node;
    425 
    426  // Next is the client with no tab.
    427  node = node.firstElementChild;
    428  is(node.getAttribute("itemtype"), "client", "node is a client entry");
    429  is(node.textContent, "My Phone", "correct client");
    430  // There is a single node saying there's no tabs for the client.
    431  node = node.nextElementSibling;
    432  is(node.nodeName, "label", "node is a label");
    433  is(node.getAttribute("itemtype"), null, "node is neither a tab nor a client");
    434 
    435  node = node.nextElementSibling;
    436  is(node, null, "no more siblings");
    437  is(currentClient.nextElementSibling, null, "no more clients");
    438 
    439  // Check accessibility. There should be containers for each client, with an
    440  // aria attribute that identifies the client name.
    441  let clientContainers = [
    442    ...tabList.querySelectorAll("[aria-labelledby]").values(),
    443  ];
    444  let labelIds = clientContainers.map(container =>
    445    container.getAttribute("aria-labelledby")
    446  );
    447  let labels = labelIds.map(id => document.getElementById(id).textContent);
    448  Assert.deepEqual(labels.sort(), [
    449    "My Desktop",
    450    "My Other Desktop",
    451    "My Phone",
    452  ]);
    453 
    454  let didSync = false;
    455  let oldDoSync = gSync.doSync;
    456  gSync.doSync = function () {
    457    didSync = true;
    458    gSync.doSync = oldDoSync;
    459  };
    460 
    461  let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
    462  is(syncNowButton.disabled, false);
    463  syncNowButton.click();
    464  ok(didSync, "clicking the button called the correct function");
    465 
    466  await hideOverflow();
    467 
    468  await SpecialPowers.popPrefEnv();
    469 });
    470 
    471 // Test the pagination capabilities (Show More/All tabs)
    472 add_task(async function () {
    473  mockedInternal.getTabClients = () => {
    474    return Promise.resolve([
    475      {
    476        id: "guid_desktop",
    477        type: "client",
    478        name: "My Desktop",
    479        lastModified: 1492201200,
    480        tabs: (function () {
    481          let allTabsDesktop = [];
    482          // We choose 77 tabs, because TABS_PER_PAGE is 25, which means
    483          // on the second to last page we should have 22 items shown
    484          // (because we have to show at least NEXT_PAGE_MIN_TABS=5 tabs on the last page)
    485          for (let i = 1; i <= 77; i++) {
    486            allTabsDesktop.push({ title: "Tab #" + i, url: SAMPLE_TAB_URL });
    487          }
    488          return allTabsDesktop;
    489        })(),
    490      },
    491    ]);
    492  };
    493 
    494  gSync.updateAllUI({
    495    status: UIState.STATUS_SIGNED_IN,
    496    syncEnabled: true,
    497    lastSync: new Date(),
    498    email: "foo@bar.com",
    499  });
    500 
    501  await document.getElementById("nav-bar").overflowable.show();
    502  let tabsUpdatedPromise = promiseObserverNotified(
    503    "synced-tabs-menu:test:tabs-updated"
    504  );
    505  let syncPanel = document.getElementById("PanelUI-remotetabs");
    506  let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
    507  let syncButton = document.getElementById("sync-button");
    508  syncButton.click();
    509  await Promise.all([tabsUpdatedPromise, viewShownPromise]);
    510 
    511  // Check pre-conditions
    512  ok(syncPanel.getAttribute("visible"), "Sync Panel is in view");
    513  let subpanel = document.getElementById("PanelUI-remotetabs-main");
    514  ok(!subpanel.hidden, "main pane is visible");
    515  let deck = document.getElementById("PanelUI-remotetabs-deck");
    516  is(deck.selectedIndex, DECKINDEX_TABS, "we should be showing tabs");
    517 
    518  function checkTabsPage(tabsShownCount, showMoreLabel) {
    519    let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
    520    let node = tabList.firstElementChild.firstElementChild;
    521    is(node.getAttribute("itemtype"), "client", "node is a client entry");
    522    is(node.textContent, "My Desktop", "correct client");
    523    for (let i = 0; i < tabsShownCount; i++) {
    524      node = node.nextElementSibling;
    525      is(node.nodeName, "hbox");
    526      let childNode = node.firstElementChild;
    527      is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
    528      is(
    529        childNode.getAttribute("label"),
    530        "Tab #" + (i + 1),
    531        "the tab is the correct one"
    532      );
    533      is(
    534        childNode.getAttribute("targetURI"),
    535        SAMPLE_TAB_URL,
    536        "url is the correct one"
    537      );
    538    }
    539    let showMoreButton;
    540    if (showMoreLabel) {
    541      node = showMoreButton = node.nextElementSibling;
    542      is(
    543        node.getAttribute("itemtype"),
    544        "showmorebutton",
    545        "node is a show more button"
    546      );
    547      is(node.getAttribute("label"), showMoreLabel);
    548    }
    549    node = node.nextElementSibling;
    550    is(node, null, "no more entries");
    551 
    552    return showMoreButton;
    553  }
    554 
    555  async function checkCanOpenURL() {
    556    let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
    557    let node =
    558      tabList.firstElementChild.firstElementChild.nextElementSibling
    559        .firstElementChild;
    560    let promiseTabOpened = BrowserTestUtils.waitForLocationChange(
    561      gBrowser,
    562      SAMPLE_TAB_URL
    563    );
    564    node.click();
    565    await promiseTabOpened;
    566  }
    567 
    568  let showMoreButton;
    569  function clickShowMoreButton() {
    570    let promise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
    571    showMoreButton.click();
    572    return promise;
    573  }
    574 
    575  showMoreButton = checkTabsPage(25, "Show more tabs");
    576  await clickShowMoreButton();
    577 
    578  checkTabsPage(77, null);
    579  /* calling this will close the overflow menu */
    580  await checkCanOpenURL();
    581 });