/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ registerCleanupFunction(async function () { await clearAllParentTelemetryEvents(); cleanup_tab_pickup(); }); function setupWithFxaDevices() { const sandbox = (gSandbox = setupSyncFxAMocks({ state: UIState.STATUS_SIGNED_IN, fxaDevices: [ { id: 1, name: "My desktop", isCurrentDevice: true, type: "desktop", }, { id: 2, name: "Other device", isCurrentDevice: false, type: "mobile", }, ], })); return sandbox; } const mockDesktopTab1 = { client: "6c12bonqXZh8", device: "My desktop", deviceType: "desktop", type: "tab", title: "Example2", url: "https://example.com", icon: "https://example/favicon.png", lastUsed: Math.floor((Date.now() - 1000 * 60) / 1000), // This is one minute from now, which is below the threshold for 'Just now' }; const mockDesktopTab2 = { client: "6c12bonqXZh8", device: "My desktop", deviceType: "desktop", type: "tab", title: "Sandboxes - Sinon.JS", url: "https://sinonjs.org/releases/latest/sandbox/", icon: "https://sinonjs.org/assets/images/favicon.png", lastUsed: 1655391592, // Thu Jun 16 2022 14:59:52 GMT+0000 }; const mockMobileTab1 = { client: "9d0y686hBXel", device: "My phone", deviceType: "mobile", type: "tab", title: "Element", url: "https://chat.mozilla.org/#room:mozilla.org", icon: "https://chat.mozilla.org/vector-icons/favicon.ico", lastUsed: 1664571288, }; const NO_TABS_EVENTS = [ ["firefoxview", "entered", "firefoxview", undefined], ["firefoxview", "synced_tabs", "tabs", undefined, { count: "0" }], ]; const SINGLE_TAB_EVENTS = [ ["firefoxview", "entered", "firefoxview", undefined], ["firefoxview", "synced_tabs", "tabs", undefined, { count: "1" }], ]; const DEVICE_ADDED_NO_TABS_EVENTS = [ ["firefoxview", "synced_tabs", "tabs", undefined, undefined], ["firefoxview", "synced_tabs_empty", "since_device_added", undefined], ]; const DEVICE_ADDED_TABS_EVENTS = [ ["firefoxview", "synced_tabs", "tabs", undefined, undefined], ]; async function whenResolved(functionSpy, functionLabel) { info(`Waiting for ${functionLabel} to be called`); await TestUtils.waitForCondition( () => functionSpy.called, `Waiting for ${functionLabel} to be called` ); is( functionSpy.getCall(0).returnValue.constructor.name, "Promise", `${functionLabel} returned a promise` ); info(`Waiting for the promise returned by ${functionLabel} to be resolved`); await functionSpy.getCall(0).returnValue; info(`${functionLabel} promise resolved`); } async function test_device_added({ initialRecentTabsResult, expectedInitialTelementryEvents, expectedDeviceAddedTelementryEvents, }) { const recentTabsResult = initialRecentTabsResult; await clearAllParentTelemetryEvents(); const sandbox = setupWithFxaDevices(); const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs"); syncedTabsMock.callsFake(() => { info( `Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${recentTabsResult.length} tabs\n` ); return Promise.resolve(recentTabsResult); }); ok( !isFirefoxViewTabSelected(), "Before we call withFirefoxView, about:firefoxview tab is not selected" ); ok( !TabsSetupFlowManager.hasVisibleViews, "Initially hasVisibleViews is false" ); await withFirefoxView({}, async browser => { info("inside withFirefoxView taskFn, waiting for setupListState"); const { document } = browser.contentWindow; const stopWaitingSpy = sandbox.spy( TabsSetupFlowManager, "stopWaitingForTabs" ); const signedInChangeSpy = sandbox.spy( TabsSetupFlowManager, "onSignedInChange" ); await setupListState(browser); info("setupListState finished"); // ensure any tab syncs triggered by Fxa sign-in are complete before proceeding await whenResolved(signedInChangeSpy, "onSignedInChange"); if (!recentTabsResult.length) { info("No synced tabs so we wait for the result of the sync we trigger"); await whenResolved(stopWaitingSpy, "stopWaitingForTabs"); info("stopWaitingForTabs finished"); } const isTablistVisible = !!initialRecentTabsResult.length; testVisibility(browser, { expectedVisible: { "ol.synced-tabs-list": isTablistVisible, "#synced-tabs-placeholder": !isTablistVisible, }, }); const syncedTabsItems = document.querySelectorAll( "ol.synced-tabs-list > li:not(.synced-tab-li-placeholder)" ); info( "list items: " + Array.from(syncedTabsItems) .map(li => `li.${li.className}`) .join(", ") ); is( syncedTabsItems.length, initialRecentTabsResult.length, `synced-tabs-list should have initial count of ${initialRecentTabsResult.length} non-placeholder list items` ); // confirm telemetry is in expected state? info( "Checking telemetry against expectedInitialTelementryEvents: " + JSON.stringify(expectedInitialTelementryEvents, null, 2) ); TelemetryTestUtils.assertEvents( expectedInitialTelementryEvents, { category: "firefoxview" }, { clear: true, process: "parent" } ); // add a new mock device info("Adding a new mock fxa dedvice"); gMockFxaDevices.push({ id: 1, name: "My primary phone", isCurrentDevice: false, type: "mobile", }); const startWaitingSpy = sandbox.spy( TabsSetupFlowManager, "startWaitingForNewDeviceTabs" ); // Notify of the newly added device info("Notifying devicelist_updated with the new mobile device"); Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated"); // Some time passes here waiting for sync to get data from that device // we expect new-device handling to kick in. If there are 0 tabs we'll signal we're waiting, // create a timestamp and only clear it when there are > 0 tabs. // If there are already > 0 tabs, we'll basically do nothing, showing any new tabs when they arrive await whenResolved(startWaitingSpy, "startWaitingForNewDeviceTabs"); info( "Initial tabs count: " + recentTabsResult.length + ", assert on _noTabsVisibleFromAddedDeviceTimestamp: " + TabsSetupFlowManager._noTabsVisibleFromAddedDeviceTimestamp ); if (recentTabsResult.length) { ok( !TabsSetupFlowManager._noTabsVisibleFromAddedDeviceTimestamp, "Should not be waiting if there were > 0 tabs initially" ); } else { ok( TabsSetupFlowManager._noTabsVisibleFromAddedDeviceTimestamp, "Should be waiting if there were 0 tabs initially" ); } // Add tab data from this new device and notify of the changed data recentTabsResult.push(mockMobileTab1); stopWaitingSpy.resetHistory(); info("Notifying tabs.changed with the new mobile device's tabs"); Services.obs.notifyObservers(null, "services.sync.tabs.changed"); // handling the tab.change and clearing the timestamp is necessarily async // as counting synced tabs via getRecentTabs() is async. // There may not be any outcome depending on the tab state, so we just wait // for stopWaitingForTabs to get called and its promise to resolve info("Waiting for the stopWaitingSpy to be called"); await whenResolved(stopWaitingSpy, "stopWaitingForTabs"); await TestUtils.waitForTick(); // allow time for the telemetry event to get recorded info( "We've added a synced tab and updated the tab list, got snapshotEvents:" + JSON.stringify( Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ), null, 2 ) ); // confirm no telemetry was recorded for tabs from the newly-added device // as the tab list was never empty info( "Checking telemetry against expectedDeviceAddedTelementryEvents: " + JSON.stringify(expectedDeviceAddedTelementryEvents, null, 2) ); TelemetryTestUtils.assertEvents( expectedDeviceAddedTelementryEvents, { category: "firefoxview" }, { clear: true, process: "parent" } ); }); sandbox.restore(); cleanup_tab_pickup(); } add_task(async function test_device_added_with_existing_tabs() { /* Confirm that no telemetry is recorded when a new device is added while the synced tabs list has tabs */ await test_device_added({ initialRecentTabsResult: [mockDesktopTab1], expectedInitialTelementryEvents: SINGLE_TAB_EVENTS, expectedDeviceAddedTelementryEvents: DEVICE_ADDED_TABS_EVENTS, }); }); add_task(async function test_device_added_with_empty_list() { /* Confirm that telemetry is recorded when a device is added and the synced tabs list is empty until its tabs get synced */ await test_device_added({ initialRecentTabsResult: [], expectedInitialTelementryEvents: NO_TABS_EVENTS, expectedDeviceAddedTelementryEvents: DEVICE_ADDED_NO_TABS_EVENTS, }); });