/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- * vim:set ts=2 sw=2 sts=2 et: */ "use strict"; const { Weave } = ChromeUtils.importESModule( "resource://services-sync/main.sys.mjs" ); const { SyncedTabs } = ChromeUtils.importESModule( "resource://services-sync/SyncedTabs.sys.mjs" ); Log.repository.getLogger("Sync.RemoteTabs").addAppender(new Log.DumpAppender()); // A mock "Tabs" engine which the SyncedTabs module will use instead of the real // engine. We pass a constructor that Sync creates. function MockTabsEngine() { this.clients = {}; // We'll set this dynamically // Mock fxAccounts + recentDeviceList as if we hit the FxA server this.fxAccounts = { device: { recentDeviceList: [ { id: 1, name: "updated desktop name", availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "baz", }, }, { id: 2, name: "updated mobile name", availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "boo", }, }, ], }, }; } MockTabsEngine.prototype = { name: "tabs", enabled: true, getAllClients() { return Object.values(this.clients); }, getOpenURLs() { return new Set(); }, }; let tabsEngine; // A clients engine that doesn't need to be a constructor. let MockClientsEngine = { clientSettings: null, // Set in `configureClients`. isMobile(guid) { if (!guid.endsWith("desktop") && !guid.endsWith("mobile")) { throw new Error( "this module expected guids to end with 'desktop' or 'mobile'" ); } return guid.endsWith("mobile"); }, remoteClientExists(id) { return this.clientSettings[id] !== false; }, getClientName(id) { if (this.clientSettings[id]) { return this.clientSettings[id]; } let client = tabsEngine.clients[id]; let fxaDevice = tabsEngine.fxAccounts.device.recentDeviceList.find( device => device.id === client.fxaDeviceId ); return fxaDevice ? fxaDevice.name : client.clientName; }, getClientFxaDeviceId(id) { if (this.clientSettings[id]) { return this.clientSettings[id]; } return tabsEngine.clients[id].fxaDeviceId; }, getClientType() { return "desktop"; }, }; function configureClients(clients, clientSettings = {}) { // each client record is expected to have an id. for (let [guid, client] of Object.entries(clients)) { client.id = guid; } tabsEngine.clients = clients; // Apply clients collection overrides. MockClientsEngine.clientSettings = clientSettings; // Send an observer that pretends the engine just finished a sync. Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); } add_task(async function setup() { await Weave.Service.promiseInitialized; // Configure Sync with our mock tabs engine and force it to become initialized. await Weave.Service.engineManager.unregister("tabs"); await Weave.Service.engineManager.register(MockTabsEngine); Weave.Service.clientsEngine = MockClientsEngine; tabsEngine = Weave.Service.engineManager.get("tabs"); // Tell the Sync XPCOM service it is initialized. let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( Ci.nsISupports ).wrappedJSObject; weaveXPCService.ready = true; }); // The tests. add_task(async function test_noClients() { // no clients, can't be tabs. await configureClients({}); let tabs = await SyncedTabs.getTabClients(); equal(Object.keys(tabs).length, 0); }); add_task(async function test_clientWithTabs() { await configureClients({ guid_desktop: { clientName: "My Desktop", tabs: [ { urlHistory: ["http://foo.com/"], icon: "http://foo.com/favicon", lastUsed: 1655745700, // Mon, 20 Jun 2022 17:21:40 GMT }, ], }, guid_mobile: { clientName: "My Phone", tabs: [], }, }); let clients = await SyncedTabs.getTabClients(); equal(clients.length, 2); clients.sort((a, b) => { return a.name.localeCompare(b.name); }); equal(clients[0].tabs.length, 1); equal(clients[0].tabs[0].url, "http://foo.com/"); equal(clients[0].tabs[0].icon, "http://foo.com/favicon"); equal(clients[0].tabs[0].lastUsed, 1655745700); // second client has no tabs. equal(clients[1].tabs.length, 0); }); add_task(async function test_staleClientWithTabs() { await configureClients( { guid_desktop: { clientName: "My Desktop", tabs: [ { urlHistory: ["http://foo.com/"], icon: "http://foo.com/favicon", lastUsed: 1655745750, }, ], }, guid_mobile: { clientName: "My Phone", tabs: [], }, guid_stale_mobile: { clientName: "My Deleted Phone", tabs: [], }, guid_stale_desktop: { clientName: "My Deleted Laptop", tabs: [ { urlHistory: ["https://bar.com/"], icon: "https://bar.com/favicon", lastUsed: 1655745700, }, ], }, guid_stale_name_desktop: { clientName: "My Generic Device", tabs: [ { urlHistory: ["https://example.edu/"], icon: "https://example.edu/favicon", lastUsed: 1655745800, }, ], }, }, { guid_stale_mobile: false, guid_stale_desktop: false, // We should always use the device name from the clients collection, instead // of the possibly stale tabs collection. guid_stale_name_desktop: "My Laptop", } ); let clients = await SyncedTabs.getTabClients(); clients.sort((a, b) => { return a.name.localeCompare(b.name); }); equal(clients.length, 3); equal(clients[0].name, "My Desktop"); equal(clients[0].tabs.length, 1); equal(clients[0].tabs[0].url, "http://foo.com/"); equal(clients[0].tabs[0].lastUsed, 1655745750); equal(clients[1].name, "My Laptop"); equal(clients[1].tabs.length, 1); equal(clients[1].tabs[0].url, "https://example.edu/"); equal(clients[1].tabs[0].lastUsed, 1655745800); equal(clients[2].name, "My Phone"); equal(clients[2].tabs.length, 0); }); add_task(async function test_clientWithTabsIconsDisabled() { Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false); await configureClients({ guid_desktop: { clientName: "My Desktop", tabs: [ { urlHistory: ["http://foo.com/"], icon: "http://foo.com/favicon", }, ], }, }); let clients = await SyncedTabs.getTabClients(); equal(clients.length, 1); clients.sort((a, b) => { return a.name.localeCompare(b.name); }); equal(clients[0].tabs.length, 1); equal(clients[0].tabs[0].url, "http://foo.com/"); // Expect the default favicon due to the pref being false. equal(clients[0].tabs[0].icon, "page-icon:http://foo.com/"); Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons"); }); add_task(async function test_filter() { // Nothing matches. await configureClients({ guid_desktop: { clientName: "My Desktop", tabs: [ { urlHistory: ["http://foo.com/"], title: "A test page.", }, { urlHistory: ["http://bar.com/"], title: "Another page.", }, ], }, }); let clients = await SyncedTabs.getTabClients("foo"); equal(clients.length, 1); equal(clients[0].tabs.length, 1); equal(clients[0].tabs[0].url, "http://foo.com/"); // check it matches the title. clients = await SyncedTabs.getTabClients("test"); equal(clients.length, 1); equal(clients[0].tabs.length, 1); equal(clients[0].tabs[0].url, "http://foo.com/"); }); add_task(async function test_duplicatesTabsAcrossClients() { await configureClients({ guid_desktop: { clientName: "My Desktop", tabs: [ { urlHistory: ["http://foo.com/"], title: "A test page.", }, ], }, guid_mobile: { clientName: "My Phone", tabs: [ { urlHistory: ["http://foo.com/"], title: "A test page.", }, ], }, }); let clients = await SyncedTabs.getTabClients(); equal(clients.length, 2); equal(clients[0].tabs.length, 1); equal(clients[1].tabs.length, 1); equal(clients[0].tabs[0].url, "http://foo.com/"); equal(clients[1].tabs[0].url, "http://foo.com/"); }); add_task(async function test_clientsTabUpdatedName() { // See the "fxAccounts" object in the MockEngine above for the device list await configureClients({ guid_desktop: { clientName: "My Desktop", tabs: [ { urlHistory: ["http://foo.com/"], icon: "http://foo.com/favicon", }, ], fxaDeviceId: 1, }, guid_mobile: { clientName: "My Phone", tabs: [ { urlHistory: ["http://bar.com/"], icon: "http://bar.com/favicon", }, ], fxaDeviceId: 2, }, }); let clients = await SyncedTabs.getTabClients(); equal(clients.length, 2); equal(clients[0].name, "updated desktop name"); equal(clients[1].name, "updated mobile name"); });