/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ const FXA_CONTINUE_EVENT = [ ["firefoxview", "entered", "firefoxview", undefined], ["firefoxview", "fxa_continue", "sync", undefined], ]; const FXA_MOBILE_EVENT = [ ["firefoxview", "entered", "firefoxview", undefined], ["firefoxview", "fxa_mobile", "sync", undefined, { has_devices: "false" }], ]; var gMockFxaDevices = null; var gUIStateStatus; function promiseSyncReady() { let service = Cc["@mozilla.org/weave/service;1"].getService( Ci.nsISupports ).wrappedJSObject; return service.whenLoaded(); } var gSandbox; async function setupWithDesktopDevices() { const sandbox = setupMocks({ state: UIState.STATUS_SIGNED_IN, fxaDevices: [ { id: 1, name: "This Device", isCurrentDevice: true, type: "desktop", }, { id: 2, name: "Other Device", type: "desktop", }, ], }); await SpecialPowers.pushPrefEnv({ set: [["services.sync.engine.tabs", true]], }); return sandbox; } add_setup(async function () { registerCleanupFunction(() => { // reset internal state so it doesn't affect the next tests TabsSetupFlowManager.resetInternalState(); }); // gSync.init() is called in a requestIdleCallback. Force its initialization. gSync.init(); registerCleanupFunction(async function () { Services.prefs.clearUserPref("services.sync.engine.tabs"); await tearDown(gSandbox); }); // set tab sync false so we don't skip setup states await SpecialPowers.pushPrefEnv({ set: [["services.sync.engine.tabs", false]], }); }); add_task(async function test_unconfigured_initial_state() { await clearAllParentTelemetryEvents(); // test with the pref set to show FEATURE TOUR CALLOUT await SpecialPowers.pushPrefEnv({ set: [ [ "browser.firefox-view.feature-tour", JSON.stringify({ screen: `FEATURE_CALLOUT_1`, complete: false, }), ], ], }); const sandbox = setupMocks({ state: UIState.STATUS_NOT_CONFIGURED, syncEnabled: false, }); await withFirefoxView({ openNewWindow: true }, async browser => { Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForVisibleSetupStep(browser, { expectedVisible: "#tabpickup-steps-view1", }); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); await BrowserTestUtils.synthesizeMouseAtCenter( 'button[data-action="view1-primary-action"]', {}, browser ); await TestUtils.waitForCondition( () => { let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ).parent; return events && events.length >= 2; }, "Waiting for entered and fxa_continue firefoxview telemetry events.", 200, 100 ); TelemetryTestUtils.assertEvents( FXA_CONTINUE_EVENT, { category: "firefoxview" }, { clear: true, process: "parent" } ); }); await tearDown(sandbox); }); add_task(async function test_signed_in() { await clearAllParentTelemetryEvents(); const sandbox = setupMocks({ state: UIState.STATUS_SIGNED_IN, fxaDevices: [ { id: 1, name: "This Device", isCurrentDevice: true, type: "desktop", }, ], }); await withFirefoxView({ openNewWindow: true }, async browser => { Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForVisibleSetupStep(browser, { expectedVisible: "#tabpickup-steps-view2", }); is( fxAccounts.device.recentDeviceList?.length, 1, "Just 1 device connected" ); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); await BrowserTestUtils.synthesizeMouseAtCenter( 'button[data-action="view2-primary-action"]', {}, browser ); await TestUtils.waitForCondition( () => { let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ).parent; return events && events.length >= 2; }, "Waiting for entered and fxa_mobile firefoxview telemetry events.", 200, 100 ); TelemetryTestUtils.assertEvents( FXA_MOBILE_EVENT, { category: "firefoxview" }, { clear: true, process: "parent" } ); }); await tearDown(sandbox); }); add_task(async function test_support_links() { await clearAllParentTelemetryEvents(); setupMocks({ state: UIState.STATUS_SIGNED_IN, fxaDevices: [ { id: 1, name: "This Device", isCurrentDevice: true, type: "desktop", }, ], }); await withFirefoxView({}, async browser => { Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForVisibleSetupStep(browser, { expectedVisible: "#tabpickup-steps-view2", }); const { document } = browser.contentWindow; const container = document.getElementById("tab-pickup-container"); const supportLinks = Array.from( container.querySelectorAll("a[href]") ).filter(a => !!a.href); is(supportLinks.length, 2, "Support links have non-empty hrefs"); }); }); add_task(async function test_2nd_desktop_connected() { const sandbox = setupMocks({ state: UIState.STATUS_SIGNED_IN, fxaDevices: [ { id: 1, name: "This Device", isCurrentDevice: true, type: "desktop", }, { id: 2, name: "Other Device", type: "desktop", }, ], }); await withFirefoxView({}, async browser => { // ensure tab sync is false so we don't skip onto next step ok( !Services.prefs.getBoolPref("services.sync.engine.tabs", false), "services.sync.engine.tabs is initially false" ); Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForVisibleSetupStep(browser, { expectedVisible: "#tabpickup-steps-view3", }); is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected"); ok( fxAccounts.device.recentDeviceList?.every( device => device.type !== "mobile" && device.type !== "tablet" ), "No connected device is type:mobile or type:tablet" ); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); }); await tearDown(sandbox); }); add_task(async function test_mobile_connected() { const sandbox = setupMocks({ state: UIState.STATUS_SIGNED_IN, fxaDevices: [ { id: 1, name: "This Device", isCurrentDevice: true, type: "desktop", }, { id: 2, name: "Other Device", type: "mobile", }, ], }); await withFirefoxView({}, async browser => { // ensure tab sync is false so we don't skip onto next step ok( !Services.prefs.getBoolPref("services.sync.engine.tabs", false), "services.sync.engine.tabs is initially false" ); Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForVisibleSetupStep(browser, { expectedVisible: "#tabpickup-steps-view3", }); is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected"); ok( fxAccounts.device.recentDeviceList?.some( device => device.type == "mobile" ), "A connected device is type:mobile" ); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); }); await tearDown(sandbox); }); add_task(async function test_tablet_connected() { const sandbox = setupMocks({ state: UIState.STATUS_SIGNED_IN, fxaDevices: [ { id: 1, name: "This Device", isCurrentDevice: true, type: "desktop", }, { id: 2, name: "Other Device", type: "tablet", }, ], }); await withFirefoxView({}, async browser => { // ensure tab sync is false so we don't skip onto next step ok( !Services.prefs.getBoolPref("services.sync.engine.tabs", false), "services.sync.engine.tabs is initially false" ); Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForVisibleSetupStep(browser, { expectedVisible: "#tabpickup-steps-view3", }); is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected"); ok( fxAccounts.device.recentDeviceList?.some( device => device.type == "tablet" ), "A connected device is type:tablet" ); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); }); await tearDown(sandbox); }); add_task(async function test_tab_sync_enabled() { const sandbox = setupMocks({ state: UIState.STATUS_SIGNED_IN, fxaDevices: [ { id: 1, name: "This Device", isCurrentDevice: true, type: "desktop", }, { id: 2, name: "Other Device", type: "mobile", }, ], }); await withFirefoxView({}, async browser => { Services.obs.notifyObservers(null, UIState.ON_UPDATE); // test initial state, with the pref not enabled await waitForVisibleSetupStep(browser, { expectedVisible: "#tabpickup-steps-view3", }); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); // test with the pref toggled on await SpecialPowers.pushPrefEnv({ set: [["services.sync.engine.tabs", true]], }); await waitForElementVisible(browser, "#tabpickup-steps", false); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); // reset and test clicking the action button await SpecialPowers.popPrefEnv(); await waitForVisibleSetupStep(browser, { expectedVisible: "#tabpickup-steps-view3", }); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); const actionButton = browser.contentWindow.document.querySelector( "#tabpickup-steps-view3 button.primary" ); actionButton.click(); await waitForElementVisible(browser, "#tabpickup-steps", false); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); await waitForElementVisible(browser, ".featureCallout .FEATURE_CALLOUT_1"); ok(true, "Tab pickup product tour screen renders when sync is enabled"); ok( Services.prefs.getBoolPref("services.sync.engine.tabs", false), "tab sync pref should be enabled after button click" ); }); await tearDown(sandbox); }); add_task(async function test_mobile_promo() { const sandbox = await setupWithDesktopDevices(); await withFirefoxView({}, async browser => { // ensure last tab fetch was just now so we don't get the loading state await touchLastTabFetch(); Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForElementVisible(browser, ".synced-tabs-container"); is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected"); info("checking mobile promo, should be visible now"); checkMobilePromo(browser, { mobilePromo: true, mobileConfirmation: false, }); gMockFxaDevices.push({ id: 3, name: "Mobile Device", type: "mobile", }); Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated"); // Wait for the async refreshDeviceList(), // which should result in the promo being hidden await waitForElementVisible( browser, "#tab-pickup-container > .promo-box", false ); is(fxAccounts.device.recentDeviceList?.length, 3, "3 devices connected"); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: true, }); info("checking mobile promo disappears on log out"); gMockFxaDevices.pop(); Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated"); await waitForElementVisible( browser, "#tab-pickup-container > .promo-box", true ); checkMobilePromo(browser, { mobilePromo: true, mobileConfirmation: false, }); // Set the UIState to what we expect when the user signs out gUIStateStatus = UIState.STATUS_NOT_CONFIGURED; gUIStateSyncEnabled = undefined; info( "notifying that we've signed out of fxa, UIState.get().status:" + UIState.get().status ); Services.obs.notifyObservers(null, UIState.ON_UPDATE); info("waiting for setup card 1 to appear again"); await waitForVisibleSetupStep(browser, { expectedVisible: "#tabpickup-steps-view1", }); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); }); await tearDown(sandbox); }); add_task(async function test_mobile_promo_pref() { const sandbox = await setupWithDesktopDevices(); await SpecialPowers.pushPrefEnv({ set: [[MOBILE_PROMO_DISMISSED_PREF, true]], }); await withFirefoxView({}, async browser => { // ensure tab sync is false so we don't skip onto next step info("starting test, will notify of UIState update"); // ensure last tab fetch was just now so we don't get the loading state await touchLastTabFetch(); Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForElementVisible(browser, ".synced-tabs-container"); is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected"); info("checking mobile promo, should be still hidden because of the pref"); checkMobilePromo(browser, { mobilePromo: false, mobileConfirmation: false, }); // reset the dismissed pref, which should case the promo to get shown await SpecialPowers.pushPrefEnv({ set: [[MOBILE_PROMO_DISMISSED_PREF, false]], }); await waitForElementVisible( browser, "#tab-pickup-container > .promo-box", true ); const promoElem = browser.contentWindow.document.querySelector( "#tab-pickup-container > .promo-box" ); const promoElemClose = promoElem.querySelector(".close"); ok(promoElemClose.hasAttribute("aria-label"), "Button has an a11y name"); // check that dismissing the promo sets the pref info("Clicking the promo close button: " + promoElemClose); EventUtils.sendMouseEvent({ type: "click" }, promoElemClose); info("Check the promo box got hidden"); BrowserTestUtils.is_hidden(promoElem); ok( SpecialPowers.getBoolPref(MOBILE_PROMO_DISMISSED_PREF), "Promo pref is updated when close is clicked" ); }); await tearDown(sandbox); }); add_task(async function test_mobile_promo_windows() { // make sure interacting with the promo and success confirmation in one window // also updates the others const sandbox = await setupWithDesktopDevices(); await withFirefoxView({}, async browser => { // ensure last tab fetch was just now so we don't get the loading state await touchLastTabFetch(); Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForElementVisible(browser, ".synced-tabs-container"); is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected"); info("checking mobile promo is visible"); checkMobilePromo(browser, { mobilePromo: true, mobileConfirmation: false, }); info( "opening new window, pref is: " + SpecialPowers.getBoolPref("browser.tabs.firefox-view") ); info("Got window, now opening Firefox View in it"); await withFirefoxView( { openNewWindow: true, resetFlowManager: false }, async win2Browser => { info("In withFirefoxView taskFn for win2"); // promo should be visible in the 2nd window too info("check mobile promo is visible in the new window"); checkMobilePromo(win2Browser, { mobilePromo: true, mobileConfirmation: false, }); // add the mobile device to get the success confirmation in both instances info("add a mobile device and send device_connected notification"); gMockFxaDevices.push({ id: 3, name: "Mobile Device", type: "mobile", }); Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated"); is( fxAccounts.device.recentDeviceList?.length, 3, "3 devices connected" ); // Wait for the async refreshDevices(), // which should result in the promo being hidden info("waiting for the confirmation box to be visible"); await waitForElementVisible( win2Browser, "#tab-pickup-container > .promo-box", false ); for (let fxviewBrowser of [browser, win2Browser]) { info( "checking promo is hidden and confirmation is visible in each window" ); checkMobilePromo(fxviewBrowser, { mobilePromo: false, mobileConfirmation: true, }); } // dismiss the confirmation and check its gone from both instances const confirmBox = win2Browser.contentWindow.document.querySelector( "#tab-pickup-container > .confirmation-message-box" ); const closeButton = confirmBox.querySelector(".close"); ok(closeButton.hasAttribute("aria-label"), "Button has an a11y name"); EventUtils.sendMouseEvent( { type: "click" }, closeButton, win2Browser.ownerGlobal ); BrowserTestUtils.is_hidden(confirmBox); for (let fxviewBrowser of [browser, win2Browser]) { checkMobilePromo(fxviewBrowser, { mobilePromo: false, mobileConfirmation: false, }); } } ); }); await tearDown(sandbox); }); async function mockFxaDeviceConnected(win) { // We use an existing tab to navigate to the final "device connected" url // in order to fake the fxa device sync process const url = "https://example.org/pair/auth/complete"; is(win.gBrowser.tabs.length, 3, "Tabs strip should contain three tabs"); BrowserTestUtils.loadURIString(win.gBrowser.selectedTab.linkedBrowser, url); await BrowserTestUtils.browserLoaded( win.gBrowser.selectedTab.linkedBrowser, null, url ); is( win.gBrowser.selectedTab.linkedBrowser.currentURI.filePath, "/pair/auth/complete", "/pair/auth/complete is the selected tab" ); } add_task(async function test_close_device_connected_tab() { // test that when a device has been connected to sync we close // that tab after the user is directed back to firefox view // Ensure we are in the correct state to start the task. TabsSetupFlowManager.resetInternalState(); await SpecialPowers.pushPrefEnv({ set: [["identity.fxaccounts.remote.root", "https://example.org/"]], }); let win = await BrowserTestUtils.openNewBrowserWindow(); let fxViewTab = await openFirefoxViewTab(win); await waitForVisibleSetupStep(win.gBrowser, { expectedVisible: "#tabpickup-steps-view1", }); let actionButton = win.gBrowser.contentWindow.document.querySelector( "#tabpickup-steps-view1 button.primary" ); // initiate the sign in flow from Firefox View, to check that didFxaTabOpen is set let tabSwitched = BrowserTestUtils.waitForEvent( win.gBrowser, "TabSwitchDone" ); actionButton.click(); await tabSwitched; // fake the end point of the device syncing flow await mockFxaDeviceConnected(win); let deviceConnectedTab = win.gBrowser.tabs[2]; // remove the blank tab opened with the browser to check that we don't // close the window when the "Device connected" tab is closed const newTab = win.gBrowser.tabs.find( tab => tab != deviceConnectedTab && tab != fxViewTab ); let removedTab = BrowserTestUtils.waitForTabClosing(newTab); BrowserTestUtils.removeTab(newTab); await removedTab; is(win.gBrowser.tabs.length, 2, "Tabs strip should only contain two tabs"); is( win.gBrowser.selectedTab.linkedBrowser.currentURI.filePath, "/pair/auth/complete", "/pair/auth/complete is the selected tab" ); // we use this instead of BrowserTestUtils.switchTab to get back to the firefox view tab // because this more accurately reflects how this tab is selected - via a custom onmousedown // and command that calls FirefoxViewHandler.openTab (both when the user manually clicks the tab // and when navigating from the fxa Device Connected tab, which also calls FirefoxViewHandler.openTab) await EventUtils.synthesizeMouseAtCenter( win.document.getElementById("firefox-view-button"), { type: "mousedown" }, win ); is(win.gBrowser.tabs.length, 2, "Tabs strip should only contain two tabs"); is( win.gBrowser.tabs[0].linkedBrowser.currentURI.filePath, "firefoxview", "First tab is Firefox view" ); is( win.gBrowser.tabs[1].linkedBrowser.currentURI.filePath, "newtab", "Second tab is about:newtab" ); // now simulate the signed-in state with the prompt to download // and sync mobile const sandbox = setupMocks({ state: UIState.STATUS_SIGNED_IN, fxaDevices: [ { id: 1, name: "This Device", isCurrentDevice: true, type: "desktop", }, ], }); Services.obs.notifyObservers(null, UIState.ON_UPDATE); await waitForVisibleSetupStep(win.gBrowser, { expectedVisible: "#tabpickup-steps-view2", }); actionButton = win.gBrowser.contentWindow.document.querySelector( "#tabpickup-steps-view2 button.primary" ); // initiate the connect device (mobile) flow from Firefox View, to check that didFxaTabOpen is set tabSwitched = BrowserTestUtils.waitForEvent(win.gBrowser, "TabSwitchDone"); actionButton.click(); await tabSwitched; // fake the end point of the device syncing flow await mockFxaDeviceConnected(win); await EventUtils.synthesizeMouseAtCenter( win.document.getElementById("firefox-view-button"), { type: "mousedown" }, win ); is(win.gBrowser.tabs.length, 2, "Tabs strip should only contain two tabs"); is( win.gBrowser.tabs[0].linkedBrowser.currentURI.filePath, "firefoxview", "First tab is Firefox view" ); is( win.gBrowser.tabs[1].linkedBrowser.currentURI.filePath, "newtab", "Second tab is about:newtab" ); // cleanup time await tearDown(sandbox); await BrowserTestUtils.closeWindow(win); });